@superblocksteam/shared 0.9614.2 → 0.9615.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.
- package/dist/database-lifecycle/appDatabaseCompatibility.d.ts +5 -3
- package/dist/database-lifecycle/appDatabaseCompatibility.d.ts.map +1 -1
- package/dist/database-lifecycle/appDatabaseCompatibility.js.map +1 -1
- package/dist/database-lifecycle/appDatabaseImport.d.ts +70 -0
- package/dist/database-lifecycle/appDatabaseImport.d.ts.map +1 -0
- package/dist/database-lifecycle/appDatabaseImport.js +111 -0
- package/dist/database-lifecycle/appDatabaseImport.js.map +1 -0
- package/dist/database-lifecycle/appDatabaseImport.test.d.ts +2 -0
- package/dist/database-lifecycle/appDatabaseImport.test.d.ts.map +1 -0
- package/dist/database-lifecycle/appDatabaseImport.test.js +145 -0
- package/dist/database-lifecycle/appDatabaseImport.test.js.map +1 -0
- package/dist/database-lifecycle/appDatabaseStatus.d.ts +79 -0
- package/dist/database-lifecycle/appDatabaseStatus.d.ts.map +1 -0
- package/dist/database-lifecycle/appDatabaseStatus.js +28 -0
- package/dist/database-lifecycle/appDatabaseStatus.js.map +1 -0
- package/dist/database-lifecycle/appDatabaseStatus.test.d.ts +2 -0
- package/dist/database-lifecycle/appDatabaseStatus.test.d.ts.map +1 -0
- package/dist/database-lifecycle/appDatabaseStatus.test.js +88 -0
- package/dist/database-lifecycle/appDatabaseStatus.test.js.map +1 -0
- package/dist/database-lifecycle/dataImport.d.ts +53 -0
- package/dist/database-lifecycle/dataImport.d.ts.map +1 -0
- package/dist/database-lifecycle/dataImport.js +173 -0
- package/dist/database-lifecycle/dataImport.js.map +1 -0
- package/dist/database-lifecycle/dataImport.test.d.ts +2 -0
- package/dist/database-lifecycle/dataImport.test.d.ts.map +1 -0
- package/dist/database-lifecycle/dataImport.test.js +84 -0
- package/dist/database-lifecycle/dataImport.test.js.map +1 -0
- package/dist/database-lifecycle/index.d.ts +101 -5
- package/dist/database-lifecycle/index.d.ts.map +1 -1
- package/dist/database-lifecycle/index.js +55 -6
- package/dist/database-lifecycle/index.js.map +1 -1
- package/dist/database-lifecycle/index.test.js +18 -6
- package/dist/database-lifecycle/index.test.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/plugins/templates/workflow.d.ts.map +1 -1
- package/dist/plugins/templates/workflow.js +1 -3
- package/dist/plugins/templates/workflow.js.map +1 -1
- package/dist/socket/protocol.d.ts +19 -2
- package/dist/socket/protocol.d.ts.map +1 -1
- package/dist/socket/protocol.js.map +1 -1
- package/dist/types/ai/index.d.ts +39 -0
- package/dist/types/ai/index.d.ts.map +1 -1
- package/dist/types/ai/index.js +8 -1
- package/dist/types/ai/index.js.map +1 -1
- package/dist/types/billing/usageSource.d.ts +1 -0
- package/dist/types/billing/usageSource.d.ts.map +1 -1
- package/dist/types/billing/usageSource.js +19 -1
- package/dist/types/billing/usageSource.js.map +1 -1
- package/dist/types/event/index.d.ts +6 -1
- package/dist/types/event/index.d.ts.map +1 -1
- package/dist/types/event/index.js +5 -0
- package/dist/types/event/index.js.map +1 -1
- package/dist/types/policyGate/index.d.ts +11 -0
- package/dist/types/policyGate/index.d.ts.map +1 -1
- package/dist/types/policyGate/index.js +9 -0
- package/dist/types/policyGate/index.js.map +1 -1
- package/dist/utils/attachment-delivery.js +1 -1
- package/dist/utils/attachment-delivery.js.map +1 -1
- package/dist/utils/attachment-delivery.test.js +11 -0
- package/dist/utils/attachment-delivery.test.js.map +1 -1
- package/dist/utils/attachment-upload.d.ts +15 -0
- package/dist/utils/attachment-upload.d.ts.map +1 -1
- package/dist/utils/attachment-upload.js +44 -2
- package/dist/utils/attachment-upload.js.map +1 -1
- package/dist/utils/attachment-upload.test.js +37 -1
- package/dist/utils/attachment-upload.test.js.map +1 -1
- package/dist/utils/attachment-validation.d.ts +18 -1
- package/dist/utils/attachment-validation.d.ts.map +1 -1
- package/dist/utils/attachment-validation.js +38 -5
- package/dist/utils/attachment-validation.js.map +1 -1
- package/dist/utils/attachment-validation.test.js +22 -3
- package/dist/utils/attachment-validation.test.js.map +1 -1
- package/dist-esm/database-lifecycle/appDatabaseCompatibility.d.ts +5 -3
- package/dist-esm/database-lifecycle/appDatabaseCompatibility.d.ts.map +1 -1
- package/dist-esm/database-lifecycle/appDatabaseCompatibility.js.map +1 -1
- package/dist-esm/database-lifecycle/appDatabaseImport.d.ts +70 -0
- package/dist-esm/database-lifecycle/appDatabaseImport.d.ts.map +1 -0
- package/dist-esm/database-lifecycle/appDatabaseImport.js +104 -0
- package/dist-esm/database-lifecycle/appDatabaseImport.js.map +1 -0
- package/dist-esm/database-lifecycle/appDatabaseImport.test.d.ts +2 -0
- package/dist-esm/database-lifecycle/appDatabaseImport.test.d.ts.map +1 -0
- package/dist-esm/database-lifecycle/appDatabaseImport.test.js +143 -0
- package/dist-esm/database-lifecycle/appDatabaseImport.test.js.map +1 -0
- package/dist-esm/database-lifecycle/appDatabaseStatus.d.ts +79 -0
- package/dist-esm/database-lifecycle/appDatabaseStatus.d.ts.map +1 -0
- package/dist-esm/database-lifecycle/appDatabaseStatus.js +23 -0
- package/dist-esm/database-lifecycle/appDatabaseStatus.js.map +1 -0
- package/dist-esm/database-lifecycle/appDatabaseStatus.test.d.ts +2 -0
- package/dist-esm/database-lifecycle/appDatabaseStatus.test.d.ts.map +1 -0
- package/dist-esm/database-lifecycle/appDatabaseStatus.test.js +86 -0
- package/dist-esm/database-lifecycle/appDatabaseStatus.test.js.map +1 -0
- package/dist-esm/database-lifecycle/dataImport.d.ts +53 -0
- package/dist-esm/database-lifecycle/dataImport.d.ts.map +1 -0
- package/dist-esm/database-lifecycle/dataImport.js +165 -0
- package/dist-esm/database-lifecycle/dataImport.js.map +1 -0
- package/dist-esm/database-lifecycle/dataImport.test.d.ts +2 -0
- package/dist-esm/database-lifecycle/dataImport.test.d.ts.map +1 -0
- package/dist-esm/database-lifecycle/dataImport.test.js +82 -0
- package/dist-esm/database-lifecycle/dataImport.test.js.map +1 -0
- package/dist-esm/database-lifecycle/index.d.ts +101 -5
- package/dist-esm/database-lifecycle/index.d.ts.map +1 -1
- package/dist-esm/database-lifecycle/index.js +36 -5
- package/dist-esm/database-lifecycle/index.js.map +1 -1
- package/dist-esm/database-lifecycle/index.test.js +19 -7
- package/dist-esm/database-lifecycle/index.test.js.map +1 -1
- package/dist-esm/index.d.ts +1 -0
- package/dist-esm/index.d.ts.map +1 -1
- package/dist-esm/index.js +1 -0
- package/dist-esm/index.js.map +1 -1
- package/dist-esm/plugins/templates/workflow.d.ts.map +1 -1
- package/dist-esm/plugins/templates/workflow.js +1 -3
- package/dist-esm/plugins/templates/workflow.js.map +1 -1
- package/dist-esm/socket/protocol.d.ts +19 -2
- package/dist-esm/socket/protocol.d.ts.map +1 -1
- package/dist-esm/socket/protocol.js.map +1 -1
- package/dist-esm/types/ai/index.d.ts +39 -0
- package/dist-esm/types/ai/index.d.ts.map +1 -1
- package/dist-esm/types/ai/index.js +6 -0
- package/dist-esm/types/ai/index.js.map +1 -1
- package/dist-esm/types/billing/usageSource.d.ts +1 -0
- package/dist-esm/types/billing/usageSource.d.ts.map +1 -1
- package/dist-esm/types/billing/usageSource.js +18 -0
- package/dist-esm/types/billing/usageSource.js.map +1 -1
- package/dist-esm/types/event/index.d.ts +6 -1
- package/dist-esm/types/event/index.d.ts.map +1 -1
- package/dist-esm/types/event/index.js +5 -0
- package/dist-esm/types/event/index.js.map +1 -1
- package/dist-esm/types/policyGate/index.d.ts +11 -0
- package/dist-esm/types/policyGate/index.d.ts.map +1 -1
- package/dist-esm/types/policyGate/index.js +8 -0
- package/dist-esm/types/policyGate/index.js.map +1 -1
- package/dist-esm/utils/attachment-delivery.js +2 -2
- package/dist-esm/utils/attachment-delivery.js.map +1 -1
- package/dist-esm/utils/attachment-delivery.test.js +11 -0
- package/dist-esm/utils/attachment-delivery.test.js.map +1 -1
- package/dist-esm/utils/attachment-upload.d.ts +15 -0
- package/dist-esm/utils/attachment-upload.d.ts.map +1 -1
- package/dist-esm/utils/attachment-upload.js +41 -1
- package/dist-esm/utils/attachment-upload.js.map +1 -1
- package/dist-esm/utils/attachment-upload.test.js +38 -2
- package/dist-esm/utils/attachment-upload.test.js.map +1 -1
- package/dist-esm/utils/attachment-validation.d.ts +18 -1
- package/dist-esm/utils/attachment-validation.d.ts.map +1 -1
- package/dist-esm/utils/attachment-validation.js +38 -5
- package/dist-esm/utils/attachment-validation.js.map +1 -1
- package/dist-esm/utils/attachment-validation.test.js +23 -4
- package/dist-esm/utils/attachment-validation.test.js.map +1 -1
- package/package.json +1 -1
- package/src/database-lifecycle/appDatabaseCompatibility.ts +6 -3
- package/src/database-lifecycle/appDatabaseImport.test.ts +185 -0
- package/src/database-lifecycle/appDatabaseImport.ts +205 -0
- package/src/database-lifecycle/appDatabaseStatus.test.ts +105 -0
- package/src/database-lifecycle/appDatabaseStatus.ts +95 -0
- package/src/database-lifecycle/dataImport.test.ts +95 -0
- package/src/database-lifecycle/dataImport.ts +244 -0
- package/src/database-lifecycle/index.test.ts +20 -6
- package/src/database-lifecycle/index.ts +203 -7
- package/src/index.ts +1 -0
- package/src/plugins/templates/workflow.ts +1 -3
- package/src/socket/protocol.ts +14 -2
- package/src/types/ai/index.ts +39 -0
- package/src/types/billing/usageSource.ts +20 -0
- package/src/types/event/index.ts +6 -1
- package/src/types/policyGate/index.ts +14 -0
- package/src/utils/attachment-delivery.test.ts +14 -0
- package/src/utils/attachment-delivery.ts +2 -2
- package/src/utils/attachment-upload.test.ts +48 -1
- package/src/utils/attachment-upload.ts +48 -1
- package/src/utils/attachment-validation.test.ts +35 -3
- package/src/utils/attachment-validation.ts +52 -6
|
@@ -112,3 +112,23 @@ const POOL_LIKE_EXHAUSTIVE: Record<UsageSource, boolean> = {
|
|
|
112
112
|
export const POOL_LIKE_USAGE_SOURCES: readonly UsageSource[] = Object.entries(POOL_LIKE_EXHAUSTIVE)
|
|
113
113
|
.filter(([, isPoolLike]) => isPoolLike)
|
|
114
114
|
.map(([source]) => source as UsageSource);
|
|
115
|
+
|
|
116
|
+
// Sources the live dollar_commit path actually writes ledger AI debits for.
|
|
117
|
+
// Replacement repricing must use this allowlist: a denylist (`is_byo IS NOT TRUE`)
|
|
118
|
+
// billed `overage_unbilled` (spend-cap usage the live path declined), plus `trial`
|
|
119
|
+
// and `topup`, as net-new money under the replacement contract.
|
|
120
|
+
const DOLLAR_COMMIT_LIVE_BILLED_EXHAUSTIVE: Record<UsageSource, boolean> = {
|
|
121
|
+
byo: false,
|
|
122
|
+
enterprise: false,
|
|
123
|
+
legacy: false,
|
|
124
|
+
on_demand: true,
|
|
125
|
+
overage_unbilled: false,
|
|
126
|
+
pool: true,
|
|
127
|
+
seat: false,
|
|
128
|
+
topup: false,
|
|
129
|
+
trial: false
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export const DOLLAR_COMMIT_LIVE_BILLED_USAGE_SOURCES: readonly UsageSource[] = Object.entries(DOLLAR_COMMIT_LIVE_BILLED_EXHAUSTIVE)
|
|
133
|
+
.filter(([, billed]) => billed)
|
|
134
|
+
.map(([source]) => source as UsageSource);
|
package/src/types/event/index.ts
CHANGED
|
@@ -71,6 +71,7 @@ export const CombinedViewMode = { ...ViewMode, ...ExportViewMode };
|
|
|
71
71
|
export type CombinedViewMode = ViewMode | ExportViewMode;
|
|
72
72
|
|
|
73
73
|
export enum EventAction {
|
|
74
|
+
ABANDON_TURN = 'abandon_turn',
|
|
74
75
|
CREATED = 'created',
|
|
75
76
|
CLONED = 'cloned',
|
|
76
77
|
DELETED = 'deleted',
|
|
@@ -103,6 +104,8 @@ export enum EventAction {
|
|
|
103
104
|
TRIAL_EXTENDED = 'trial_extended',
|
|
104
105
|
PLAN_UPGRADED = 'plan_upgraded',
|
|
105
106
|
PLAN_DOWNGRADED = 'plan_downgraded',
|
|
107
|
+
/** Single plan transition for analytics: carries currentPlan and newPlan. */
|
|
108
|
+
PLAN_CHANGED = 'plan_changed',
|
|
106
109
|
MEMBER_ADDED = 'member_added',
|
|
107
110
|
MEMBER_REMOVED = 'member_removed',
|
|
108
111
|
SENT = 'sent',
|
|
@@ -114,6 +117,7 @@ export enum EventAction {
|
|
|
114
117
|
PULLED = 'pulled',
|
|
115
118
|
PUSHED = 'pushed',
|
|
116
119
|
LOGGED_IN = 'logged_in',
|
|
120
|
+
SESSION_REVERTED = 'session_reverted',
|
|
117
121
|
SESSION_STARTED = 'session_started',
|
|
118
122
|
GRANTED = 'granted',
|
|
119
123
|
DEACTIVATED = 'deactivated',
|
|
@@ -123,7 +127,8 @@ export enum EventAction {
|
|
|
123
127
|
DETECTED = 'detected',
|
|
124
128
|
DISABLED = 'disabled',
|
|
125
129
|
ENABLED = 'enabled',
|
|
126
|
-
PUBLISH_ANYWAY = 'publish_anyway'
|
|
130
|
+
PUBLISH_ANYWAY = 'publish_anyway',
|
|
131
|
+
EDIT_AFTER_CLARK = 'edit_after_clark'
|
|
127
132
|
}
|
|
128
133
|
|
|
129
134
|
export interface UserEvent {
|
|
@@ -74,6 +74,11 @@ export type PolicyGateReadinessItem = {
|
|
|
74
74
|
mode: 'advisory' | 'approval' | 'blocking' | 'blocking_with_exceptions' | 'report_only';
|
|
75
75
|
policyId: string;
|
|
76
76
|
policyVersionId: string;
|
|
77
|
+
/**
|
|
78
|
+
* Latest completed run of this policy on the deployed commit. It is the
|
|
79
|
+
* baseline a diff-scoped scan reviews changes against.
|
|
80
|
+
*/
|
|
81
|
+
priorReviewRunId?: string;
|
|
77
82
|
progress?: PolicyGateScanProgress;
|
|
78
83
|
reviewRunId?: string;
|
|
79
84
|
runnerType?: ReviewPolicyRunnerType;
|
|
@@ -127,6 +132,15 @@ export function areReadinessItemsAckEligible(items: Pick<PolicyGateReadinessItem
|
|
|
127
132
|
);
|
|
128
133
|
}
|
|
129
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Whether a readiness item has reported an outcome. `running` is still
|
|
137
|
+
* deciding, and `missing` is what a poll between two runs returns, not an
|
|
138
|
+
* answer.
|
|
139
|
+
*/
|
|
140
|
+
export function hasPolicyItemReported(item: Pick<PolicyGateReadinessItem, 'status'>): boolean {
|
|
141
|
+
return item.status !== 'missing' && item.status !== 'running';
|
|
142
|
+
}
|
|
143
|
+
|
|
130
144
|
export type PolicyGateReadinessRun = {
|
|
131
145
|
completedAt?: string;
|
|
132
146
|
createdAt?: string;
|
|
@@ -50,3 +50,17 @@ describe('dump attachments', () => {
|
|
|
50
50
|
).toBe(true);
|
|
51
51
|
});
|
|
52
52
|
});
|
|
53
|
+
|
|
54
|
+
describe('spreadsheet attachments', () => {
|
|
55
|
+
test('classifies .xlsx as a reference-only supported upload and leaves legacy Excel unsupported', () => {
|
|
56
|
+
expect(
|
|
57
|
+
classifyAttachmentDelivery({
|
|
58
|
+
fileName: 'budget.xlsx',
|
|
59
|
+
mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
|
60
|
+
})
|
|
61
|
+
).toBe('reference_only');
|
|
62
|
+
expect(isSupportedAttachmentUploadType({ fileName: 'budget.xlsx' })).toBe(true);
|
|
63
|
+
expect(isSupportedAttachmentUploadType({ fileName: 'inventory.xls' })).toBe(false);
|
|
64
|
+
expect(isSupportedAttachmentUploadType({ fileName: 'macros.xlsm' })).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getTextAttachmentType } from './attachment-upload.js';
|
|
1
|
+
import { getTextAttachmentType, isXlsxAttachmentFile } from './attachment-upload.js';
|
|
2
2
|
|
|
3
3
|
export type AttachmentDeliveryMode = 'inline_native' | 'reference_only';
|
|
4
4
|
export type AttachmentArchiveType = 'gz' | 'tgz' | 'zip';
|
|
@@ -76,7 +76,7 @@ export function isDumpAttachment(input: AttachmentTypeInput): boolean {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
export function isReferenceOnlySupportedAttachment(input: AttachmentTypeInput): boolean {
|
|
79
|
-
return detectAttachmentArchiveType(input) !== null || isDumpAttachment(input);
|
|
79
|
+
return detectAttachmentArchiveType(input) !== null || isDumpAttachment(input) || isXlsxAttachmentFile(input);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
export function classifyAttachmentDelivery(input: AttachmentTypeInput): AttachmentDeliveryMode {
|
|
@@ -9,14 +9,60 @@ import {
|
|
|
9
9
|
createUploadedAttachmentFromServer,
|
|
10
10
|
getAttachmentMimeTypeFromFileName,
|
|
11
11
|
getTextAttachmentType,
|
|
12
|
+
isLegacyExcelFile,
|
|
12
13
|
isOfficeXml,
|
|
13
14
|
isTextLikeMimeType,
|
|
15
|
+
isXlsxAttachmentFile,
|
|
14
16
|
normalizeAttachmentMimeType,
|
|
15
17
|
parseUserFacingUploadError,
|
|
16
18
|
toAttachmentUploadDescriptor,
|
|
17
19
|
uploadAttachmentToServerApi
|
|
18
20
|
} from './attachment-upload.js';
|
|
19
21
|
|
|
22
|
+
describe('isXlsxAttachmentFile', () => {
|
|
23
|
+
test('is true for .xlsx only', () => {
|
|
24
|
+
expect(isXlsxAttachmentFile({ fileName: 'a.xlsx' })).toBe(true);
|
|
25
|
+
expect(isXlsxAttachmentFile({ fileName: 'b.xls' })).toBe(false);
|
|
26
|
+
expect(isXlsxAttachmentFile({ fileName: 'c.xlsm' })).toBe(false);
|
|
27
|
+
expect(isXlsxAttachmentFile({ fileName: 'd.xlsb' })).toBe(false);
|
|
28
|
+
expect(isXlsxAttachmentFile({ fileName: 'e.docx' })).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('does not read a legacy .xls as .xlsx even when the MIME type says spreadsheet', () => {
|
|
32
|
+
expect(
|
|
33
|
+
isXlsxAttachmentFile({
|
|
34
|
+
fileName: 'inventory.xls',
|
|
35
|
+
mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
|
36
|
+
})
|
|
37
|
+
).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('isLegacyExcelFile', () => {
|
|
42
|
+
test('is true for the Excel formats that cannot be converted', () => {
|
|
43
|
+
expect(isLegacyExcelFile({ fileName: 'a.xls' })).toBe(true);
|
|
44
|
+
expect(isLegacyExcelFile({ fileName: 'b.XLSM' })).toBe(true);
|
|
45
|
+
expect(isLegacyExcelFile({ fileName: 'c.xlsb' })).toBe(true);
|
|
46
|
+
expect(isLegacyExcelFile({ fileName: 'sheet', mimeType: 'application/vnd.ms-excel' })).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('is false for .xlsx and for files that are not Excel at all', () => {
|
|
50
|
+
expect(isLegacyExcelFile({ fileName: 'a.xlsx' })).toBe(false);
|
|
51
|
+
expect(isLegacyExcelFile({ fileName: 'b.csv' })).toBe(false);
|
|
52
|
+
expect(isLegacyExcelFile({ fileName: 'c.docx' })).toBe(false);
|
|
53
|
+
expect(isLegacyExcelFile({})).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('reports a macro-enabled workbook even when the MIME type claims .xlsx', () => {
|
|
57
|
+
expect(
|
|
58
|
+
isLegacyExcelFile({
|
|
59
|
+
fileName: 'macro.xlsm',
|
|
60
|
+
mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
|
61
|
+
})
|
|
62
|
+
).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
20
66
|
describe('getAttachmentMimeTypeFromFileName', () => {
|
|
21
67
|
test.each([
|
|
22
68
|
['logo.png', 'image/png'],
|
|
@@ -29,7 +75,8 @@ describe('getAttachmentMimeTypeFromFileName', () => {
|
|
|
29
75
|
['server.log', 'text/plain'],
|
|
30
76
|
['rows.csv', 'text/csv'],
|
|
31
77
|
['config.json', 'application/json'],
|
|
32
|
-
['orders_prod.dump', 'application/x-postgresql-dump']
|
|
78
|
+
['orders_prod.dump', 'application/x-postgresql-dump'],
|
|
79
|
+
['budget.xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet']
|
|
33
80
|
])('reads %s as %s', (fileName, expected) => {
|
|
34
81
|
expect(getAttachmentMimeTypeFromFileName(fileName)).toBe(expected);
|
|
35
82
|
});
|
|
@@ -33,6 +33,8 @@ const TEXT_ATTACHMENT_MIME_TYPES = {
|
|
|
33
33
|
|
|
34
34
|
const POSTGRES_DUMP_MIME_TYPES = new Set(['application/vnd.postgresql.dump', 'application/x-postgresql-dump']);
|
|
35
35
|
|
|
36
|
+
export const XLSX_MIME_TYPE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
|
37
|
+
|
|
36
38
|
export const TEXT_LIKE_FILE_EXTENSIONS = [
|
|
37
39
|
'.txt',
|
|
38
40
|
'.text',
|
|
@@ -86,7 +88,8 @@ const MIME_TYPE_FILE_EXTENSIONS: Record<string, string> = {
|
|
|
86
88
|
'text/css': '.css',
|
|
87
89
|
'text/csv': '.csv',
|
|
88
90
|
'text/plain': '.txt',
|
|
89
|
-
'text/yaml': '.yaml'
|
|
91
|
+
'text/yaml': '.yaml',
|
|
92
|
+
[XLSX_MIME_TYPE]: '.xlsx'
|
|
90
93
|
};
|
|
91
94
|
|
|
92
95
|
type UploadAttachmentDataLike = {
|
|
@@ -238,6 +241,47 @@ export function isTextLikeMimeType(mimeType: string): boolean {
|
|
|
238
241
|
);
|
|
239
242
|
}
|
|
240
243
|
|
|
244
|
+
const LEGACY_EXCEL_EXTENSIONS = ['.xls', '.xlsb', '.xlsm'] as const;
|
|
245
|
+
const LEGACY_EXCEL_MIME_TYPES = ['application/vnd.ms-excel'] as const;
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Excel workbooks that Clark converts to CSV before import. Only the modern
|
|
249
|
+
* `.xlsx` format is read; legacy `.xls` and macro-enabled `.xlsm` files stay
|
|
250
|
+
* rejected with the rest of Office.
|
|
251
|
+
*/
|
|
252
|
+
export function isXlsxAttachmentFile(params: { fileName?: string; mimeType?: string | null }): boolean {
|
|
253
|
+
const normalizedFileName = normalizeNonEmptyString(params.fileName)?.toLowerCase();
|
|
254
|
+
if (normalizedFileName?.endsWith('.xlsx') === true) {
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
// Any other extension wins over the MIME type. These bytes are handed to a
|
|
258
|
+
// reader that only understands .xlsx, so a declared MIME type must never let
|
|
259
|
+
// another format through - not just the legacy Excel ones.
|
|
260
|
+
if (normalizedFileName !== undefined && /\.[^.]+$/.test(normalizedFileName)) {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return normalizeNonEmptyString(params.mimeType)?.toLowerCase() === XLSX_MIME_TYPE;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Legacy binary (`.xls`/`.xlsb`) and macro-enabled (`.xlsm`) Excel, which stay rejected. */
|
|
268
|
+
export function isLegacyExcelFile(params: { fileName?: string; mimeType?: string | null }): boolean {
|
|
269
|
+
if (isXlsxAttachmentFile(params)) {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const normalizedFileName = normalizeNonEmptyString(params.fileName)?.toLowerCase();
|
|
274
|
+
if (normalizedFileName && LEGACY_EXCEL_EXTENSIONS.some((extension) => normalizedFileName.endsWith(extension))) {
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
if (normalizedFileName !== undefined && /\.[^.]+$/.test(normalizedFileName)) {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const normalizedMimeType = normalizeNonEmptyString(params.mimeType)?.toLowerCase();
|
|
282
|
+
return normalizedMimeType !== undefined && LEGACY_EXCEL_MIME_TYPES.includes(normalizedMimeType as never);
|
|
283
|
+
}
|
|
284
|
+
|
|
241
285
|
export function isOfficeXml(params: { fileName?: string; mimeType?: string | null }): boolean {
|
|
242
286
|
const normalizedMimeType = normalizeNonEmptyString(params.mimeType)?.toLowerCase();
|
|
243
287
|
if (normalizedMimeType && OFFICE_MIME_SUBSTRINGS.some((mimeType) => normalizedMimeType.includes(mimeType))) {
|
|
@@ -321,6 +365,9 @@ export function getAttachmentMimeTypeFromFileName(fileName?: string): string | u
|
|
|
321
365
|
if (normalizedFileName.endsWith('.jpeg')) {
|
|
322
366
|
return 'image/jpeg';
|
|
323
367
|
}
|
|
368
|
+
if (normalizedFileName.endsWith('.xlsx')) {
|
|
369
|
+
return XLSX_MIME_TYPE;
|
|
370
|
+
}
|
|
324
371
|
if (normalizedFileName.endsWith('.dump')) {
|
|
325
372
|
return 'application/x-postgresql-dump';
|
|
326
373
|
}
|
|
@@ -4,6 +4,7 @@ import { describe, expect, test } from 'vitest';
|
|
|
4
4
|
import {
|
|
5
5
|
AttachmentValidationError,
|
|
6
6
|
MAX_ARCHIVE_ATTACHMENT_BYTES,
|
|
7
|
+
MAX_SPREADSHEET_ATTACHMENT_BYTES,
|
|
7
8
|
MAX_IMAGE_ATTACHMENT_BYTES,
|
|
8
9
|
MAX_PDF_ATTACHMENT_BYTES,
|
|
9
10
|
MAX_TEXT_ATTACHMENT_BYTES,
|
|
@@ -52,7 +53,8 @@ describe('attachment size policy', () => {
|
|
|
52
53
|
['notes.md', 'text/markdown', MAX_TEXT_ATTACHMENT_BYTES],
|
|
53
54
|
['bundle.zip', 'application/zip', MAX_ARCHIVE_ATTACHMENT_BYTES],
|
|
54
55
|
['bundle.tgz', 'application/octet-stream', MAX_ARCHIVE_ATTACHMENT_BYTES],
|
|
55
|
-
['orders_prod.dump', 'application/octet-stream', MAX_ARCHIVE_ATTACHMENT_BYTES]
|
|
56
|
+
['orders_prod.dump', 'application/octet-stream', MAX_ARCHIVE_ATTACHMENT_BYTES],
|
|
57
|
+
['budget.xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', MAX_SPREADSHEET_ATTACHMENT_BYTES]
|
|
56
58
|
])('allows the exact limit and rejects one byte more for %s', (fileName, mimeType, limit) => {
|
|
57
59
|
expect(getAttachmentUploadLimit({ fileName, mimeType })).toBe(limit);
|
|
58
60
|
expect(() => assertAttachmentSize({ fileName, mimeType, sizeBytes: limit })).not.toThrow();
|
|
@@ -82,15 +84,26 @@ describe('attachment size policy', () => {
|
|
|
82
84
|
test('uses the same policy for MIME types and extension fallback without trusting a conflicting extension', () => {
|
|
83
85
|
expect(getAttachmentUploadPolicy({ fileName: 'SPEC.PDF' })).toEqual(getAttachmentUploadPolicy({ mimeType: 'application/pdf' }));
|
|
84
86
|
expect(getAttachmentUploadPolicy({ fileName: 'spec.pdf', mimeType: 'image/png' }).maxBytes).toBe(3_750_000);
|
|
85
|
-
|
|
86
|
-
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('lists limits only for the file families a surface accepts', () => {
|
|
90
|
+
expect(getAttachmentLimitsHelp({})).toBe('PDFs: 4.5 MB each, 100 pages total. Images: 3.75 MB after conversion. Text/code: 5 MiB.');
|
|
91
|
+
expect(getAttachmentLimitsHelp({ spreadsheetAttachmentsEnabled: true })).toBe(
|
|
92
|
+
'PDFs: 4.5 MB each, 100 pages total. Images: 3.75 MB after conversion. Text/code: 5 MiB. Excel workbooks (.xlsx): 5 MiB.'
|
|
93
|
+
);
|
|
94
|
+
expect(getAttachmentLimitsHelp({ archiveAttachmentsEnabled: true })).toBe(
|
|
87
95
|
'PDFs: 4.5 MB each, 100 pages total. Images: 3.75 MB after conversion. Text/code: 5 MiB. Supported archives and .dump files: 50 MiB.'
|
|
88
96
|
);
|
|
97
|
+
expect(getAttachmentLimitsHelp({ archiveAttachmentsEnabled: true, spreadsheetAttachmentsEnabled: true })).toBe(
|
|
98
|
+
'PDFs: 4.5 MB each, 100 pages total. Images: 3.75 MB after conversion. Text/code: 5 MiB. Supported archives and .dump files: 50 MiB. Excel workbooks (.xlsx): 5 MiB.'
|
|
99
|
+
);
|
|
89
100
|
});
|
|
90
101
|
|
|
91
102
|
test('rejects unsupported and Office types', () => {
|
|
92
103
|
expectReason(() => getAttachmentUploadLimit({ fileName: 'logo.svg', mimeType: 'image/svg+xml' }), 'unsupported_type');
|
|
93
104
|
expectReason(() => getAttachmentUploadLimit({ fileName: 'report.docx', mimeType: 'text/plain' }), 'unsupported_type');
|
|
105
|
+
expectReason(() => getAttachmentUploadLimit({ fileName: 'macros.xlsm' }), 'unsupported_type');
|
|
106
|
+
expectReason(() => getAttachmentUploadLimit({ fileName: 'inventory.xls', mimeType: 'application/vnd.ms-excel' }), 'unsupported_type');
|
|
94
107
|
expectReason(() => getAttachmentUploadLimit({ fileName: 'notes.txt', mimeType: 'application/octet-stream' }), 'unsupported_type');
|
|
95
108
|
});
|
|
96
109
|
});
|
|
@@ -133,6 +146,25 @@ describe('attachment content validation', () => {
|
|
|
133
146
|
);
|
|
134
147
|
});
|
|
135
148
|
|
|
149
|
+
test('accepts Excel workbook magic for .xlsx', async () => {
|
|
150
|
+
await expect(
|
|
151
|
+
validateAttachmentBytes({
|
|
152
|
+
fileName: 'budget.xlsx',
|
|
153
|
+
mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
154
|
+
bytes: new Uint8Array([0x50, 0x4b, 0x03, 0x04])
|
|
155
|
+
})
|
|
156
|
+
).resolves.toEqual({ pdfPages: 0 });
|
|
157
|
+
await expectAsyncReason(
|
|
158
|
+
() =>
|
|
159
|
+
validateAttachmentBytes({
|
|
160
|
+
fileName: 'budget.xlsx',
|
|
161
|
+
mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
162
|
+
bytes: encoder.encode('not a workbook')
|
|
163
|
+
}),
|
|
164
|
+
'invalid_content'
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
|
|
136
168
|
test('accepts dump mime type without a .dump filename', async () => {
|
|
137
169
|
const bytes = encoder.encode('PGDMP\x01\x0e\x00');
|
|
138
170
|
await expect(validateAttachmentBytes({ fileName: 'orders', mimeType: 'application/x-postgresql-dump', bytes })).resolves.toEqual({
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { PDFDocument, PDFPageLeaf, PDFPageTree } from 'pdf-lib';
|
|
2
2
|
|
|
3
3
|
import { detectAttachmentArchiveType, isDumpAttachment } from './attachment-delivery.js';
|
|
4
|
-
import { getTextAttachmentType, isOfficeXml } from './attachment-upload.js';
|
|
4
|
+
import { getTextAttachmentType, isOfficeXml, isXlsxAttachmentFile } from './attachment-upload.js';
|
|
5
5
|
|
|
6
6
|
const ATTACHMENT_UPLOAD_POLICIES = {
|
|
7
7
|
archive: {
|
|
@@ -19,6 +19,16 @@ const ATTACHMENT_UPLOAD_POLICIES = {
|
|
|
19
19
|
invalidContentMessage:
|
|
20
20
|
'This database dump could not be read. Re-create it with pg_dump custom format (-Fc) so the file starts with PGDMP, then try again.'
|
|
21
21
|
},
|
|
22
|
+
spreadsheet: {
|
|
23
|
+
// Reading a workbook builds the whole grid in memory: a 6 MB file already
|
|
24
|
+
// costs ~800 MB and a 32 MB one costs ~3 GB, which the editor process
|
|
25
|
+
// cannot absorb. Cap the upload where the conversion stays affordable.
|
|
26
|
+
maxBytes: 5 * 1024 * 1024,
|
|
27
|
+
sizeLabel: '5 MiB',
|
|
28
|
+
label: 'Excel workbook',
|
|
29
|
+
advice: 'Remove unused sheets or split the workbook before attaching it.',
|
|
30
|
+
invalidContentMessage: 'This Excel workbook could not be read. Re-save it as .xlsx without a password and try again.'
|
|
31
|
+
},
|
|
22
32
|
image: {
|
|
23
33
|
maxBytes: 3_750_000,
|
|
24
34
|
sizeLabel: '3.75 MB after conversion',
|
|
@@ -46,11 +56,35 @@ export const MAX_PDF_ATTACHMENT_BYTES = ATTACHMENT_UPLOAD_POLICIES.pdf.maxBytes;
|
|
|
46
56
|
export const MAX_IMAGE_ATTACHMENT_BYTES = ATTACHMENT_UPLOAD_POLICIES.image.maxBytes;
|
|
47
57
|
export const MAX_TEXT_ATTACHMENT_BYTES = ATTACHMENT_UPLOAD_POLICIES.text.maxBytes;
|
|
48
58
|
export const MAX_ARCHIVE_ATTACHMENT_BYTES = ATTACHMENT_UPLOAD_POLICIES.archive.maxBytes;
|
|
59
|
+
export const MAX_SPREADSHEET_ATTACHMENT_BYTES = ATTACHMENT_UPLOAD_POLICIES.spreadsheet.maxBytes;
|
|
49
60
|
export const MAX_PDF_ATTACHMENT_PAGES = 100;
|
|
50
61
|
|
|
51
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Which families of upload-and-reference files a surface accepts. Excel
|
|
64
|
+
* workbooks roll out separately from archives and dumps, so each family has
|
|
65
|
+
* its own switch.
|
|
66
|
+
*/
|
|
67
|
+
export type ReferenceOnlyAttachmentFlags = {
|
|
68
|
+
archiveAttachmentsEnabled?: boolean;
|
|
69
|
+
spreadsheetAttachmentsEnabled?: boolean;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export function getAttachmentLimitsHelp(flags: ReferenceOnlyAttachmentFlags): string {
|
|
52
73
|
const { archive, image, pdf, text } = ATTACHMENT_UPLOAD_POLICIES;
|
|
53
|
-
|
|
74
|
+
const base = `PDFs: ${pdf.sizeLabel} each, ${MAX_PDF_ATTACHMENT_PAGES} pages total. Images: ${image.sizeLabel}. Text/code: ${text.sizeLabel}.`;
|
|
75
|
+
const archivesEnabled = flags.archiveAttachmentsEnabled === true;
|
|
76
|
+
const workbooksEnabled = flags.spreadsheetAttachmentsEnabled === true;
|
|
77
|
+
|
|
78
|
+
if (archivesEnabled && workbooksEnabled) {
|
|
79
|
+
return `${base} Supported archives and .dump files: ${archive.sizeLabel}. Excel workbooks (.xlsx): ${ATTACHMENT_UPLOAD_POLICIES.spreadsheet.sizeLabel}.`;
|
|
80
|
+
}
|
|
81
|
+
if (archivesEnabled) {
|
|
82
|
+
return `${base} Supported archives and .dump files: ${archive.sizeLabel}.`;
|
|
83
|
+
}
|
|
84
|
+
if (workbooksEnabled) {
|
|
85
|
+
return `${base} Excel workbooks (.xlsx): ${ATTACHMENT_UPLOAD_POLICIES.spreadsheet.sizeLabel}.`;
|
|
86
|
+
}
|
|
87
|
+
return base;
|
|
54
88
|
}
|
|
55
89
|
|
|
56
90
|
export type AttachmentValidationReason = 'invalid_content' | 'too_large' | 'too_many_pdf_pages' | 'unsupported_type';
|
|
@@ -60,7 +94,7 @@ const VALIDATION_MESSAGES: Record<AttachmentValidationReason, string> = {
|
|
|
60
94
|
too_large: 'This upload is too large. Reduce its size and try again.',
|
|
61
95
|
too_many_pdf_pages: `Clark supports up to ${MAX_PDF_ATTACHMENT_PAGES} PDF pages per request. Remove another PDF or attach fewer pages.`,
|
|
62
96
|
unsupported_type:
|
|
63
|
-
'This file type is not supported. Use an image, PDF, text/code file, a supported ZIP, GZIP, or TGZ archive, or a Postgres .dump file.'
|
|
97
|
+
'This file type is not supported. Use an image, PDF, text/code file, an Excel workbook (.xlsx), a supported ZIP, GZIP, or TGZ archive, or a Postgres .dump file.'
|
|
64
98
|
};
|
|
65
99
|
|
|
66
100
|
export class AttachmentValidationError extends Error {
|
|
@@ -81,7 +115,7 @@ export type AttachmentTypeInput = {
|
|
|
81
115
|
mimeType?: string | null;
|
|
82
116
|
};
|
|
83
117
|
|
|
84
|
-
type AttachmentKind = 'archive' | 'dump' | 'gif' | 'jpeg' | 'pdf' | 'png' | 'text' | 'webp';
|
|
118
|
+
type AttachmentKind = 'archive' | 'dump' | 'gif' | 'jpeg' | 'pdf' | 'png' | 'spreadsheet' | 'text' | 'webp';
|
|
85
119
|
const MAX_PDF_PAGE_TREE_NODES = 1_000;
|
|
86
120
|
const POSTGRES_DUMP_MAGIC = [0x50, 0x47, 0x44, 0x4d, 0x50] as const;
|
|
87
121
|
|
|
@@ -94,6 +128,9 @@ function normalizedFileName(fileName?: string): string {
|
|
|
94
128
|
}
|
|
95
129
|
|
|
96
130
|
function getAttachmentKind(input: AttachmentTypeInput): AttachmentKind {
|
|
131
|
+
if (isXlsxAttachmentFile(input)) {
|
|
132
|
+
return 'spreadsheet';
|
|
133
|
+
}
|
|
97
134
|
if (isOfficeXml(input)) {
|
|
98
135
|
throw new AttachmentValidationError('unsupported_type');
|
|
99
136
|
}
|
|
@@ -128,7 +165,10 @@ function getAttachmentKind(input: AttachmentTypeInput): AttachmentKind {
|
|
|
128
165
|
/** Resolve the validated upload policy by MIME, falling back to the extension only when MIME is absent or generic. */
|
|
129
166
|
export function getAttachmentUploadPolicy(input: AttachmentTypeInput) {
|
|
130
167
|
const kind = getAttachmentKind(input);
|
|
131
|
-
const policy =
|
|
168
|
+
const policy =
|
|
169
|
+
ATTACHMENT_UPLOAD_POLICIES[
|
|
170
|
+
kind === 'archive' || kind === 'dump' || kind === 'pdf' || kind === 'spreadsheet' || kind === 'text' ? kind : 'image'
|
|
171
|
+
];
|
|
132
172
|
return {
|
|
133
173
|
...policy,
|
|
134
174
|
tooLargeMessage: `This ${policy.label} is too large. The limit is ${policy.sizeLabel}. ${policy.advice}`
|
|
@@ -188,6 +228,11 @@ function validateDump(bytes: Uint8Array): void {
|
|
|
188
228
|
}
|
|
189
229
|
}
|
|
190
230
|
|
|
231
|
+
/** An .xlsx workbook is an OOXML zip, so the zip magic is what has to match. */
|
|
232
|
+
function validateSpreadsheet(bytes: Uint8Array): void {
|
|
233
|
+
if (!isZip(bytes)) throw new AttachmentValidationError('invalid_content');
|
|
234
|
+
}
|
|
235
|
+
|
|
191
236
|
function validateText(bytes: Uint8Array): void {
|
|
192
237
|
if (
|
|
193
238
|
bytes.includes(0) ||
|
|
@@ -260,6 +305,7 @@ export async function validateAttachmentBytes(input: AttachmentTypeInput & { byt
|
|
|
260
305
|
if (kind === 'text') validateText(input.bytes);
|
|
261
306
|
else if (kind === 'archive') validateArchive(input, input.bytes);
|
|
262
307
|
else if (kind === 'dump') validateDump(input.bytes);
|
|
308
|
+
else if (kind === 'spreadsheet') validateSpreadsheet(input.bytes);
|
|
263
309
|
else validateImage(kind, input.bytes);
|
|
264
310
|
return { pdfPages: 0 };
|
|
265
311
|
} catch (error) {
|