@actuate-media/cms-core 0.31.0 → 0.32.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/__tests__/api/public-forms.test.d.ts +2 -0
- package/dist/__tests__/api/public-forms.test.d.ts.map +1 -0
- package/dist/__tests__/api/public-forms.test.js +125 -0
- package/dist/__tests__/api/public-forms.test.js.map +1 -0
- package/dist/__tests__/forms/spam.test.d.ts +2 -0
- package/dist/__tests__/forms/spam.test.d.ts.map +1 -0
- package/dist/__tests__/forms/spam.test.js +115 -0
- package/dist/__tests__/forms/spam.test.js.map +1 -0
- package/dist/__tests__/forms/submission-files.test.d.ts +2 -0
- package/dist/__tests__/forms/submission-files.test.d.ts.map +1 -0
- package/dist/__tests__/forms/submission-files.test.js +65 -0
- package/dist/__tests__/forms/submission-files.test.js.map +1 -0
- package/dist/__tests__/forms/submission-pipeline.test.d.ts +2 -0
- package/dist/__tests__/forms/submission-pipeline.test.d.ts.map +1 -0
- package/dist/__tests__/forms/submission-pipeline.test.js +131 -0
- package/dist/__tests__/forms/submission-pipeline.test.js.map +1 -0
- package/dist/__tests__/forms/submission-validation.test.d.ts +2 -0
- package/dist/__tests__/forms/submission-validation.test.d.ts.map +1 -0
- package/dist/__tests__/forms/submission-validation.test.js +108 -0
- package/dist/__tests__/forms/submission-validation.test.js.map +1 -0
- package/dist/api/handler-factory.d.ts.map +1 -1
- package/dist/api/handler-factory.js +2 -2
- package/dist/api/handler-factory.js.map +1 -1
- package/dist/api/handlers.d.ts.map +1 -1
- package/dist/api/handlers.js +275 -56
- package/dist/api/handlers.js.map +1 -1
- package/dist/api/router.d.ts +2 -1
- package/dist/api/router.d.ts.map +1 -1
- package/dist/api/router.js +3 -0
- package/dist/api/router.js.map +1 -1
- package/dist/forms/files.d.ts +38 -0
- package/dist/forms/files.d.ts.map +1 -0
- package/dist/forms/files.js +134 -0
- package/dist/forms/files.js.map +1 -0
- package/dist/forms/index.d.ts +4 -0
- package/dist/forms/index.d.ts.map +1 -1
- package/dist/forms/index.js +4 -0
- package/dist/forms/index.js.map +1 -1
- package/dist/forms/spam.d.ts +33 -0
- package/dist/forms/spam.d.ts.map +1 -0
- package/dist/forms/spam.js +93 -0
- package/dist/forms/spam.js.map +1 -0
- package/dist/forms/submission.d.ts +95 -0
- package/dist/forms/submission.d.ts.map +1 -0
- package/dist/forms/submission.js +282 -0
- package/dist/forms/submission.js.map +1 -0
- package/dist/forms/validation.d.ts +19 -0
- package/dist/forms/validation.d.ts.map +1 -0
- package/dist/forms/validation.js +235 -0
- package/dist/forms/validation.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/security/client-ip.d.ts +11 -21
- package/dist/security/client-ip.d.ts.map +1 -1
- package/dist/security/client-ip.js +38 -0
- package/dist/security/client-ip.js.map +1 -1
- package/package.json +1 -1
package/dist/api/router.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export interface ApiRoute {
|
|
2
|
-
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
2
|
+
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS';
|
|
3
3
|
path: string;
|
|
4
4
|
handler: (request: Request, params: Record<string, string>) => Promise<Response>;
|
|
5
5
|
middleware?: ApiMiddleware[];
|
|
@@ -13,6 +13,7 @@ export interface ApiRouter {
|
|
|
13
13
|
put(path: string, handler: ApiRoute['handler']): void;
|
|
14
14
|
patch(path: string, handler: ApiRoute['handler']): void;
|
|
15
15
|
delete(path: string, handler: ApiRoute['handler']): void;
|
|
16
|
+
options(path: string, handler: ApiRoute['handler']): void;
|
|
16
17
|
handle(request: Request): Promise<Response>;
|
|
17
18
|
}
|
|
18
19
|
/** Create a public REST API router for Actuate CMS content. */
|
package/dist/api/router.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../src/api/router.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,CAAA;
|
|
1
|
+
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../src/api/router.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,GAAG,SAAS,CAAA;IAC/D,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;IAChF,UAAU,CAAC,EAAE,aAAa,EAAE,CAAA;CAC7B;AAED,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,QAAQ,CAAC,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;AAElG,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,QAAQ,EAAE,CAAA;IAClB,GAAG,CAAC,UAAU,EAAE,aAAa,GAAG,IAAI,CAAA;IACpC,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,CAAC,SAAS,CAAC,GAAG,IAAI,CAAA;IACrD,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,CAAC,SAAS,CAAC,GAAG,IAAI,CAAA;IACtD,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,CAAC,SAAS,CAAC,GAAG,IAAI,CAAA;IACrD,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,CAAC,SAAS,CAAC,GAAG,IAAI,CAAA;IACvD,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,CAAC,SAAS,CAAC,GAAG,IAAI,CAAA;IACxD,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,CAAC,SAAS,CAAC,GAAG,IAAI,CAAA;IACzD,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;CAC5C;AAED,+DAA+D;AAC/D,wBAAgB,eAAe,IAAI,SAAS,CA2D3C"}
|
package/dist/api/router.js
CHANGED
|
@@ -25,6 +25,9 @@ export function createApiRouter() {
|
|
|
25
25
|
delete(path, handler) {
|
|
26
26
|
addRoute('DELETE', path, handler);
|
|
27
27
|
},
|
|
28
|
+
options(path, handler) {
|
|
29
|
+
addRoute('OPTIONS', path, handler);
|
|
30
|
+
},
|
|
28
31
|
async handle(request) {
|
|
29
32
|
const url = new URL(request.url);
|
|
30
33
|
const method = request.method.toUpperCase();
|
package/dist/api/router.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.js","sourceRoot":"","sources":["../../src/api/router.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"router.js","sourceRoot":"","sources":["../../src/api/router.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { FormField, FormSubmissionFile } from './types.js';
|
|
2
|
+
/** Conservative default allowlist for form attachments (images + common docs). */
|
|
3
|
+
export declare const FORM_UPLOAD_ALLOWED_MIME_TYPES: readonly string[];
|
|
4
|
+
export declare const DEFAULT_MAX_FILE_BYTES: number;
|
|
5
|
+
export interface IncomingFile {
|
|
6
|
+
fieldKey: string;
|
|
7
|
+
fileName: string;
|
|
8
|
+
mimeType: string;
|
|
9
|
+
size: number;
|
|
10
|
+
buffer: Buffer;
|
|
11
|
+
}
|
|
12
|
+
export interface FileValidationError {
|
|
13
|
+
fieldKey: string;
|
|
14
|
+
message: string;
|
|
15
|
+
}
|
|
16
|
+
export interface ValidateFilesResult {
|
|
17
|
+
ok: boolean;
|
|
18
|
+
errors: FileValidationError[];
|
|
19
|
+
}
|
|
20
|
+
export interface ValidateFilesOptions {
|
|
21
|
+
/** Global ceiling; per-field `maxFileSizeMB` can only be smaller. */
|
|
22
|
+
maxSizeBytes?: number;
|
|
23
|
+
/** Global MIME allowlist; per-field `fileTypes` can only narrow it. */
|
|
24
|
+
allowedMimeTypes?: readonly string[];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Validate every incoming file against its field's rules. Returns all errors
|
|
28
|
+
* (not just the first) so the submitter can fix everything in one round-trip.
|
|
29
|
+
*/
|
|
30
|
+
export declare function validateSubmissionFiles(fields: FormField[], incoming: IncomingFile[], options?: ValidateFilesOptions): ValidateFilesResult;
|
|
31
|
+
/**
|
|
32
|
+
* Upload validated files via the storage adapter and persist metadata rows.
|
|
33
|
+
* Must be called *after* the submission row exists (files reference it). When
|
|
34
|
+
* no storage adapter is configured the upload is skipped and the file is
|
|
35
|
+
* dropped with a warning rather than failing the whole submission.
|
|
36
|
+
*/
|
|
37
|
+
export declare function persistSubmissionFiles(formId: string, submissionId: string, files: IncomingFile[]): Promise<FormSubmissionFile[]>;
|
|
38
|
+
//# sourceMappingURL=files.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"files.d.ts","sourceRoot":"","sources":["../../src/forms/files.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAI/D,kFAAkF;AAClF,eAAO,MAAM,8BAA8B,EAAE,SAAS,MAAM,EAW3D,CAAA;AAED,eAAO,MAAM,sBAAsB,QAAmB,CAAA;AAEtD,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,OAAO,CAAA;IACX,MAAM,EAAE,mBAAmB,EAAE,CAAA;CAC9B;AAED,MAAM,WAAW,oBAAoB;IACnC,qEAAqE;IACrE,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,uEAAuE;IACvE,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CACrC;AAOD;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,SAAS,EAAE,EACnB,QAAQ,EAAE,YAAY,EAAE,EACxB,OAAO,GAAE,oBAAyB,GACjC,mBAAmB,CA+CrB;AAED;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,YAAY,EAAE,GACpB,OAAO,CAAC,kBAAkB,EAAE,CAAC,CA2D/B"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-upload handling for form submissions.
|
|
3
|
+
*
|
|
4
|
+
* Validation is defense-in-depth: declared MIME allowlist + magic-byte sniffing
|
|
5
|
+
* (so a renamed `.exe` claiming `image/png` is rejected) + a size cap. Files are
|
|
6
|
+
* never stored inline in the DB — they go through the configured storage adapter
|
|
7
|
+
* and we persist only metadata + the storage key needed to mint a download link.
|
|
8
|
+
*/
|
|
9
|
+
import { getDB } from '../db.js';
|
|
10
|
+
import { checkMagicBytes, validateMimeType } from '../security/upload.js';
|
|
11
|
+
/** Conservative default allowlist for form attachments (images + common docs). */
|
|
12
|
+
export const FORM_UPLOAD_ALLOWED_MIME_TYPES = [
|
|
13
|
+
'image/jpeg',
|
|
14
|
+
'image/png',
|
|
15
|
+
'image/gif',
|
|
16
|
+
'image/webp',
|
|
17
|
+
'image/avif',
|
|
18
|
+
'application/pdf',
|
|
19
|
+
'text/plain',
|
|
20
|
+
'text/csv',
|
|
21
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
22
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
23
|
+
];
|
|
24
|
+
export const DEFAULT_MAX_FILE_BYTES = 10 * 1024 * 1024; // 10 MB
|
|
25
|
+
function sanitizeFileName(name) {
|
|
26
|
+
const base = name.split(/[/\\]/).pop() ?? 'file';
|
|
27
|
+
return base.replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 120) || 'file';
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Validate every incoming file against its field's rules. Returns all errors
|
|
31
|
+
* (not just the first) so the submitter can fix everything in one round-trip.
|
|
32
|
+
*/
|
|
33
|
+
export function validateSubmissionFiles(fields, incoming, options = {}) {
|
|
34
|
+
const errors = [];
|
|
35
|
+
const fieldByKey = new Map(fields.map((f) => [f.key, f]));
|
|
36
|
+
const globalMax = options.maxSizeBytes ?? DEFAULT_MAX_FILE_BYTES;
|
|
37
|
+
const globalAllowed = options.allowedMimeTypes ?? FORM_UPLOAD_ALLOWED_MIME_TYPES;
|
|
38
|
+
for (const file of incoming) {
|
|
39
|
+
const field = fieldByKey.get(file.fieldKey);
|
|
40
|
+
if (!field || field.type !== 'file') {
|
|
41
|
+
errors.push({ fieldKey: file.fieldKey, message: 'Unexpected file upload.' });
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const rules = field.validationRules ?? {};
|
|
45
|
+
const maxBytes = rules.maxFileSizeMB
|
|
46
|
+
? Math.min(globalMax, rules.maxFileSizeMB * 1024 * 1024)
|
|
47
|
+
: globalMax;
|
|
48
|
+
if (file.size > maxBytes) {
|
|
49
|
+
errors.push({
|
|
50
|
+
fieldKey: file.fieldKey,
|
|
51
|
+
message: `${field.label || field.key} exceeds the ${Math.round(maxBytes / (1024 * 1024))}MB limit.`,
|
|
52
|
+
});
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const allowed = rules.fileTypes && rules.fileTypes.length > 0
|
|
56
|
+
? rules.fileTypes.filter((t) => globalAllowed.includes(t))
|
|
57
|
+
: globalAllowed;
|
|
58
|
+
if (!validateMimeType(file.mimeType, allowed)) {
|
|
59
|
+
errors.push({
|
|
60
|
+
fieldKey: file.fieldKey,
|
|
61
|
+
message: `${field.label || field.key} has an unsupported file type "${file.mimeType || 'unknown'}".`,
|
|
62
|
+
});
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const magic = checkMagicBytes(file.buffer, file.mimeType);
|
|
66
|
+
if (!magic.valid) {
|
|
67
|
+
errors.push({
|
|
68
|
+
fieldKey: file.fieldKey,
|
|
69
|
+
message: `${field.label || field.key} contents do not match the declared type.`,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return { ok: errors.length === 0, errors };
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Upload validated files via the storage adapter and persist metadata rows.
|
|
77
|
+
* Must be called *after* the submission row exists (files reference it). When
|
|
78
|
+
* no storage adapter is configured the upload is skipped and the file is
|
|
79
|
+
* dropped with a warning rather than failing the whole submission.
|
|
80
|
+
*/
|
|
81
|
+
export async function persistSubmissionFiles(formId, submissionId, files) {
|
|
82
|
+
if (files.length === 0)
|
|
83
|
+
return [];
|
|
84
|
+
const { getStorageAdapter } = await import('../storage/index.js');
|
|
85
|
+
const storage = getStorageAdapter();
|
|
86
|
+
if (!storage) {
|
|
87
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
88
|
+
console.warn('[Actuate CMS] Form upload received but no storage adapter is configured; file dropped.');
|
|
89
|
+
}
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
const db = getDB();
|
|
93
|
+
const created = [];
|
|
94
|
+
for (const file of files) {
|
|
95
|
+
const safeName = sanitizeFileName(file.fileName);
|
|
96
|
+
const storageKey = `actuate/forms/${formId}/${submissionId}/${Date.now()}-${safeName}`;
|
|
97
|
+
let publicUrl = null;
|
|
98
|
+
try {
|
|
99
|
+
publicUrl = await storage.upload(storageKey, file.buffer, file.mimeType);
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
103
|
+
console.error('[Actuate CMS] Form file upload failed:', err);
|
|
104
|
+
}
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const row = await db.formSubmissionFile.create({
|
|
108
|
+
data: {
|
|
109
|
+
submissionId,
|
|
110
|
+
fieldKey: file.fieldKey,
|
|
111
|
+
fileName: safeName,
|
|
112
|
+
mimeType: file.mimeType,
|
|
113
|
+
sizeBytes: file.size,
|
|
114
|
+
storageKey,
|
|
115
|
+
publicUrl,
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
created.push({
|
|
119
|
+
id: String(row.id),
|
|
120
|
+
submissionId,
|
|
121
|
+
fieldKey: file.fieldKey,
|
|
122
|
+
mediaAssetId: row.mediaAssetId ?? null,
|
|
123
|
+
fileName: safeName,
|
|
124
|
+
mimeType: file.mimeType,
|
|
125
|
+
sizeBytes: file.size,
|
|
126
|
+
storageKey,
|
|
127
|
+
publicUrl,
|
|
128
|
+
secureDownloadUrl: null,
|
|
129
|
+
createdAt: row.createdAt instanceof Date ? row.createdAt.toISOString() : new Date().toISOString(),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return created;
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=files.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"files.js","sourceRoot":"","sources":["../../src/forms/files.ts"],"names":[],"mappings":"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"}
|
package/dist/forms/index.d.ts
CHANGED
|
@@ -6,4 +6,8 @@ export { INPUT_FIELD_TYPES, NON_INPUT_FIELD_TYPES } from './types.js';
|
|
|
6
6
|
export type { FormFieldType, PiiClassification, FormFieldOption, FieldValidationRules, ConditionalOperator, ConditionalRule, ConditionalLogic, FieldWidth, FormField, FormSchemaVersion, FormWebhookConfig, FormWebhookEvent, FormNotificationSettings, SpamProtectionStrategy, FormSpamSettings, FormEmbedSettings, FormStatus, FormDefinition, SpamStatus, FormSubmissionFile, FormSubmission, FetchFormsParams, FetchFormEntriesParams, FormStats, FormsSidebarCount, EntryCounts, } from './types.js';
|
|
7
7
|
export * from './service.js';
|
|
8
8
|
export * from './entries.js';
|
|
9
|
+
export { validateAndNormalizeSubmission, type FieldError, type SubmissionValidationResult, } from './validation.js';
|
|
10
|
+
export { scoreSubmissionSpam, type SpamScoreInput, type SpamScoreResult } from './spam.js';
|
|
11
|
+
export { validateSubmissionFiles, persistSubmissionFiles, FORM_UPLOAD_ALLOWED_MIME_TYPES, DEFAULT_MAX_FILE_BYTES, type IncomingFile, type ValidateFilesResult, } from './files.js';
|
|
12
|
+
export { processSubmission, buildPublicForm, SubmissionValidationError, type SubmissionContext, type SubmissionResult, type SubmissionConfirmation, type PublicForm, type PublicFormField, } from './submission.js';
|
|
9
13
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/forms/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,gBAAgB,EAChB,oBAAoB,EACpB,aAAa,EACb,2BAA2B,GAC5B,MAAM,kBAAkB,CAAA;AACzB,YAAY,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAEvD,OAAO,EACL,cAAc,EACd,eAAe,EACf,cAAc,EACd,kBAAkB,EAClB,sBAAsB,EACtB,yBAAyB,GAC1B,MAAM,gBAAgB,CAAA;AACvB,YAAY,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AAE3E,OAAO,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AACrE,YAAY,EACV,aAAa,EACb,iBAAiB,EACjB,eAAe,EACf,oBAAoB,EACpB,mBAAmB,EACnB,eAAe,EACf,gBAAgB,EAChB,UAAU,EACV,SAAS,EACT,iBAAiB,EACjB,iBAAiB,EACjB,gBAAgB,EAChB,wBAAwB,EACxB,sBAAsB,EACtB,gBAAgB,EAChB,iBAAiB,EACjB,UAAU,EACV,cAAc,EACd,UAAU,EACV,kBAAkB,EAClB,cAAc,EACd,gBAAgB,EAChB,sBAAsB,EACtB,SAAS,EACT,iBAAiB,EACjB,WAAW,GACZ,MAAM,YAAY,CAAA;AAEnB,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/forms/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,gBAAgB,EAChB,oBAAoB,EACpB,aAAa,EACb,2BAA2B,GAC5B,MAAM,kBAAkB,CAAA;AACzB,YAAY,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAEvD,OAAO,EACL,cAAc,EACd,eAAe,EACf,cAAc,EACd,kBAAkB,EAClB,sBAAsB,EACtB,yBAAyB,GAC1B,MAAM,gBAAgB,CAAA;AACvB,YAAY,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AAE3E,OAAO,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AACrE,YAAY,EACV,aAAa,EACb,iBAAiB,EACjB,eAAe,EACf,oBAAoB,EACpB,mBAAmB,EACnB,eAAe,EACf,gBAAgB,EAChB,UAAU,EACV,SAAS,EACT,iBAAiB,EACjB,iBAAiB,EACjB,gBAAgB,EAChB,wBAAwB,EACxB,sBAAsB,EACtB,gBAAgB,EAChB,iBAAiB,EACjB,UAAU,EACV,cAAc,EACd,UAAU,EACV,kBAAkB,EAClB,cAAc,EACd,gBAAgB,EAChB,sBAAsB,EACtB,SAAS,EACT,iBAAiB,EACjB,WAAW,GACZ,MAAM,YAAY,CAAA;AAEnB,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA;AAE5B,OAAO,EACL,8BAA8B,EAC9B,KAAK,UAAU,EACf,KAAK,0BAA0B,GAChC,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,mBAAmB,EAAE,KAAK,cAAc,EAAE,KAAK,eAAe,EAAE,MAAM,WAAW,CAAA;AAC1F,OAAO,EACL,uBAAuB,EACvB,sBAAsB,EACtB,8BAA8B,EAC9B,sBAAsB,EACtB,KAAK,YAAY,EACjB,KAAK,mBAAmB,GACzB,MAAM,YAAY,CAAA;AACnB,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,yBAAyB,EACzB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,sBAAsB,EAC3B,KAAK,UAAU,EACf,KAAK,eAAe,GACrB,MAAM,iBAAiB,CAAA"}
|
package/dist/forms/index.js
CHANGED
|
@@ -3,4 +3,8 @@ export { trackFormStart, trackFormSubmit, trackFormError, pushDataLayerEvent, ge
|
|
|
3
3
|
export { INPUT_FIELD_TYPES, NON_INPUT_FIELD_TYPES } from './types.js';
|
|
4
4
|
export * from './service.js';
|
|
5
5
|
export * from './entries.js';
|
|
6
|
+
export { validateAndNormalizeSubmission, } from './validation.js';
|
|
7
|
+
export { scoreSubmissionSpam } from './spam.js';
|
|
8
|
+
export { validateSubmissionFiles, persistSubmissionFiles, FORM_UPLOAD_ALLOWED_MIME_TYPES, DEFAULT_MAX_FILE_BYTES, } from './files.js';
|
|
9
|
+
export { processSubmission, buildPublicForm, SubmissionValidationError, } from './submission.js';
|
|
6
10
|
//# sourceMappingURL=index.js.map
|
package/dist/forms/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/forms/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,gBAAgB,EAChB,oBAAoB,EACpB,aAAa,EACb,2BAA2B,GAC5B,MAAM,kBAAkB,CAAA;AAGzB,OAAO,EACL,cAAc,EACd,eAAe,EACf,cAAc,EACd,kBAAkB,EAClB,sBAAsB,EACtB,yBAAyB,GAC1B,MAAM,gBAAgB,CAAA;AAGvB,OAAO,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AA8BrE,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/forms/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,gBAAgB,EAChB,oBAAoB,EACpB,aAAa,EACb,2BAA2B,GAC5B,MAAM,kBAAkB,CAAA;AAGzB,OAAO,EACL,cAAc,EACd,eAAe,EACf,cAAc,EACd,kBAAkB,EAClB,sBAAsB,EACtB,yBAAyB,GAC1B,MAAM,gBAAgB,CAAA;AAGvB,OAAO,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AA8BrE,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA;AAE5B,OAAO,EACL,8BAA8B,GAG/B,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,mBAAmB,EAA6C,MAAM,WAAW,CAAA;AAC1F,OAAO,EACL,uBAAuB,EACvB,sBAAsB,EACtB,8BAA8B,EAC9B,sBAAsB,GAGvB,MAAM,YAAY,CAAA;AACnB,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,yBAAyB,GAM1B,MAAM,iBAAiB,CAAA"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spam scoring for public form submissions.
|
|
3
|
+
*
|
|
4
|
+
* Each enabled strategy contributes a reason; any reason flags the submission
|
|
5
|
+
* as `spam` so it is hidden from the default inbox while still being persisted
|
|
6
|
+
* (operators can review the spam folder and recover false positives). The
|
|
7
|
+
* honeypot is always checked when a honeypot field exists — it is free and has
|
|
8
|
+
* no false-positive risk for real users.
|
|
9
|
+
*
|
|
10
|
+
* This module is pure (no I/O) so it is trivially unit-testable and cannot leak
|
|
11
|
+
* PII or make outbound calls.
|
|
12
|
+
*/
|
|
13
|
+
import type { FormField, FormSpamSettings, SpamProtectionStrategy, SpamStatus } from './types.js';
|
|
14
|
+
export interface SpamScoreInput {
|
|
15
|
+
fields: FormField[];
|
|
16
|
+
/** Raw (pre-validation) values — needed so the honeypot value is still visible. */
|
|
17
|
+
rawValues: Record<string, unknown>;
|
|
18
|
+
settings?: FormSpamSettings;
|
|
19
|
+
/** Site-wide default strategies merged with the per-form strategy list. */
|
|
20
|
+
defaultStrategies?: SpamProtectionStrategy[];
|
|
21
|
+
senderEmail?: string | null;
|
|
22
|
+
/** Milliseconds between form render and submit, when the client reports it. */
|
|
23
|
+
elapsedMs?: number | null;
|
|
24
|
+
/** Request `Origin` header (preferred) and `Referer` fallback for allowlisting. */
|
|
25
|
+
origin?: string | null;
|
|
26
|
+
referrer?: string | null;
|
|
27
|
+
}
|
|
28
|
+
export interface SpamScoreResult {
|
|
29
|
+
status: SpamStatus;
|
|
30
|
+
reasons: string[];
|
|
31
|
+
}
|
|
32
|
+
export declare function scoreSubmissionSpam(input: SpamScoreInput): SpamScoreResult;
|
|
33
|
+
//# sourceMappingURL=spam.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spam.d.ts","sourceRoot":"","sources":["../../src/forms/spam.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAEjG,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,SAAS,EAAE,CAAA;IACnB,mFAAmF;IACnF,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAClC,QAAQ,CAAC,EAAE,gBAAgB,CAAA;IAC3B,2EAA2E;IAC3E,iBAAiB,CAAC,EAAE,sBAAsB,EAAE,CAAA;IAC5C,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,+EAA+E;IAC/E,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,mFAAmF;IACnF,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,UAAU,CAAA;IAClB,OAAO,EAAE,MAAM,EAAE,CAAA;CAClB;AAgCD,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,cAAc,GAAG,eAAe,CA+D1E"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
function hostOf(value) {
|
|
2
|
+
if (!value)
|
|
3
|
+
return null;
|
|
4
|
+
try {
|
|
5
|
+
return new URL(value).hostname.toLowerCase();
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function normalizeHost(domain) {
|
|
12
|
+
return domain
|
|
13
|
+
.trim()
|
|
14
|
+
.toLowerCase()
|
|
15
|
+
.replace(/^https?:\/\//, '')
|
|
16
|
+
.replace(/\/.*$/, '')
|
|
17
|
+
.replace(/^\*\./, '');
|
|
18
|
+
}
|
|
19
|
+
/** Concatenate user-supplied string values for keyword scanning. */
|
|
20
|
+
function textBlob(fields, rawValues) {
|
|
21
|
+
const parts = [];
|
|
22
|
+
for (const field of fields) {
|
|
23
|
+
if (field.type === 'honeypot' || field.type === 'divider')
|
|
24
|
+
continue;
|
|
25
|
+
const v = rawValues[field.key];
|
|
26
|
+
if (typeof v === 'string')
|
|
27
|
+
parts.push(v);
|
|
28
|
+
else if (Array.isArray(v))
|
|
29
|
+
parts.push(v.map((x) => String(x)).join(' '));
|
|
30
|
+
}
|
|
31
|
+
return parts.join(' \n ').toLowerCase();
|
|
32
|
+
}
|
|
33
|
+
export function scoreSubmissionSpam(input) {
|
|
34
|
+
const reasons = [];
|
|
35
|
+
const settings = input.settings ?? {};
|
|
36
|
+
const strategies = new Set([
|
|
37
|
+
...(input.defaultStrategies ?? []),
|
|
38
|
+
...(settings.strategies ?? []),
|
|
39
|
+
]);
|
|
40
|
+
// Honeypot — always evaluated when a honeypot field/key exists. A populated
|
|
41
|
+
// honeypot means an automated client filled a field that is hidden from
|
|
42
|
+
// humans via CSS.
|
|
43
|
+
const honeypotKey = settings.honeypotFieldKey ?? input.fields.find((f) => f.type === 'honeypot')?.key;
|
|
44
|
+
if (honeypotKey) {
|
|
45
|
+
const v = input.rawValues[honeypotKey];
|
|
46
|
+
const filled = typeof v === 'string' ? v.trim() !== '' : v != null && v !== false;
|
|
47
|
+
if (filled)
|
|
48
|
+
reasons.push('honeypot');
|
|
49
|
+
}
|
|
50
|
+
// Time-to-submit — humans take a few seconds; instant submits are bots.
|
|
51
|
+
if (strategies.has('timeToSubmit') &&
|
|
52
|
+
typeof settings.minSubmitSeconds === 'number' &&
|
|
53
|
+
settings.minSubmitSeconds > 0 &&
|
|
54
|
+
typeof input.elapsedMs === 'number' &&
|
|
55
|
+
input.elapsedMs >= 0) {
|
|
56
|
+
if (input.elapsedMs < settings.minSubmitSeconds * 1000)
|
|
57
|
+
reasons.push('too_fast');
|
|
58
|
+
}
|
|
59
|
+
// Email / domain blocklist.
|
|
60
|
+
if (strategies.has('blocklist')) {
|
|
61
|
+
const email = (input.senderEmail ?? '').trim().toLowerCase();
|
|
62
|
+
if (email) {
|
|
63
|
+
const blockedEmails = (settings.blockedEmails ?? []).map((e) => e.trim().toLowerCase());
|
|
64
|
+
if (blockedEmails.includes(email))
|
|
65
|
+
reasons.push('blocked_email');
|
|
66
|
+
const domain = email.split('@')[1] ?? '';
|
|
67
|
+
const blockedDomains = (settings.blockedDomains ?? []).map((d) => normalizeHost(d));
|
|
68
|
+
if (domain && blockedDomains.includes(domain))
|
|
69
|
+
reasons.push('blocked_domain');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Keyword blocklist.
|
|
73
|
+
if (strategies.has('keyword') && (settings.blockedKeywords?.length ?? 0) > 0) {
|
|
74
|
+
const blob = textBlob(input.fields, input.rawValues);
|
|
75
|
+
for (const keyword of settings.blockedKeywords ?? []) {
|
|
76
|
+
const k = keyword.trim().toLowerCase();
|
|
77
|
+
if (k && blob.includes(k)) {
|
|
78
|
+
reasons.push('blocked_keyword');
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// Domain allowlist — the submitting origin must be on the allowlist.
|
|
84
|
+
if (strategies.has('domainAllowlist') && (settings.allowedDomains?.length ?? 0) > 0) {
|
|
85
|
+
const host = hostOf(input.origin) ?? hostOf(input.referrer);
|
|
86
|
+
const allowed = (settings.allowedDomains ?? []).map((d) => normalizeHost(d));
|
|
87
|
+
const ok = host != null && allowed.some((a) => host === a || host.endsWith(`.${a}`));
|
|
88
|
+
if (!ok)
|
|
89
|
+
reasons.push('origin_not_allowed');
|
|
90
|
+
}
|
|
91
|
+
return { status: reasons.length > 0 ? 'spam' : 'clean', reasons };
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=spam.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spam.js","sourceRoot":"","sources":["../../src/forms/spam.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { type FieldError } from './validation.js';
|
|
2
|
+
import { type IncomingFile } from './files.js';
|
|
3
|
+
import type { FormDefinition, FormField, SpamStatus } from './types.js';
|
|
4
|
+
export interface SubmissionConfirmation {
|
|
5
|
+
type: 'message' | 'redirect';
|
|
6
|
+
message?: string;
|
|
7
|
+
redirectUrl?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface SubmissionContext {
|
|
10
|
+
/** Raw field values from the request (object keyed by field key). */
|
|
11
|
+
values: Record<string, unknown>;
|
|
12
|
+
/** Already-validated file uploads to attach after the row is created. */
|
|
13
|
+
files?: IncomingFile[];
|
|
14
|
+
ip?: string;
|
|
15
|
+
userAgent?: string | null;
|
|
16
|
+
referrer?: string | null;
|
|
17
|
+
origin?: string | null;
|
|
18
|
+
pageUrl?: string | null;
|
|
19
|
+
/** Opaque attribution blob captured client-side; stored as-is. */
|
|
20
|
+
attribution?: Record<string, unknown> | null;
|
|
21
|
+
/** Milliseconds between render and submit (for the time-trap spam check). */
|
|
22
|
+
elapsedMs?: number | null;
|
|
23
|
+
}
|
|
24
|
+
export interface SubmissionResult {
|
|
25
|
+
submissionId: string;
|
|
26
|
+
entryNumber: number | null;
|
|
27
|
+
spamStatus: SpamStatus;
|
|
28
|
+
confirmation: SubmissionConfirmation;
|
|
29
|
+
}
|
|
30
|
+
/** Thrown when field validation fails; mapped to HTTP 422 by the API layer. */
|
|
31
|
+
export declare class SubmissionValidationError extends Error {
|
|
32
|
+
fieldErrors: FieldError[];
|
|
33
|
+
constructor(fieldErrors: FieldError[]);
|
|
34
|
+
}
|
|
35
|
+
/** Resolve the field schema the submission should be validated against. */
|
|
36
|
+
declare function resolveFields(form: FormDefinition): Promise<{
|
|
37
|
+
fields: FormField[];
|
|
38
|
+
versionId: string | null;
|
|
39
|
+
}>;
|
|
40
|
+
/**
|
|
41
|
+
* Process a validated request into a persisted submission. Throws
|
|
42
|
+
* `SubmissionValidationError` when field validation fails so the caller can
|
|
43
|
+
* return a structured 422.
|
|
44
|
+
*/
|
|
45
|
+
export declare function processSubmission(form: FormDefinition, ctx: SubmissionContext): Promise<SubmissionResult>;
|
|
46
|
+
export interface PublicFormField {
|
|
47
|
+
id: string;
|
|
48
|
+
key: string;
|
|
49
|
+
label: string;
|
|
50
|
+
type: string;
|
|
51
|
+
required: boolean;
|
|
52
|
+
placeholder?: string;
|
|
53
|
+
helpText?: string;
|
|
54
|
+
defaultValue?: unknown;
|
|
55
|
+
options?: {
|
|
56
|
+
label: string;
|
|
57
|
+
value: string;
|
|
58
|
+
}[];
|
|
59
|
+
width?: string;
|
|
60
|
+
sortOrder: number;
|
|
61
|
+
validationRules?: Record<string, unknown>;
|
|
62
|
+
conditionalLogic?: unknown;
|
|
63
|
+
}
|
|
64
|
+
export interface PublicForm {
|
|
65
|
+
id: string;
|
|
66
|
+
name: string;
|
|
67
|
+
slug: string;
|
|
68
|
+
description?: string;
|
|
69
|
+
fields: PublicFormField[];
|
|
70
|
+
submitPath: string;
|
|
71
|
+
successMessage?: string;
|
|
72
|
+
redirectUrl?: string;
|
|
73
|
+
spam: {
|
|
74
|
+
honeypotFieldKey?: string;
|
|
75
|
+
minSubmitSeconds?: number;
|
|
76
|
+
};
|
|
77
|
+
captcha?: {
|
|
78
|
+
provider: string;
|
|
79
|
+
siteKey: string;
|
|
80
|
+
};
|
|
81
|
+
schemaVersionId?: string | null;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Project a form definition into the public, embeddable schema. Strips every
|
|
85
|
+
* server-only concern (access rules, notification recipients, webhook secrets,
|
|
86
|
+
* blocklists, encryption flags) so nothing sensitive leaks to the browser.
|
|
87
|
+
*/
|
|
88
|
+
export declare function buildPublicForm(form: FormDefinition, fields: FormField[], schemaVersionId: string | null, options?: {
|
|
89
|
+
captcha?: {
|
|
90
|
+
provider: string;
|
|
91
|
+
siteKey: string;
|
|
92
|
+
};
|
|
93
|
+
}): PublicForm;
|
|
94
|
+
export { resolveFields as resolveSubmissionFields };
|
|
95
|
+
//# sourceMappingURL=submission.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"submission.d.ts","sourceRoot":"","sources":["../../src/forms/submission.ts"],"names":[],"mappings":"AAmBA,OAAO,EAAkC,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAEjF,OAAO,EAA0B,KAAK,YAAY,EAAE,MAAM,YAAY,CAAA;AACtE,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAA0B,UAAU,EAAE,MAAM,YAAY,CAAA;AAI/F,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,SAAS,GAAG,UAAU,CAAA;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,WAAW,iBAAiB;IAChC,qEAAqE;IACrE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC/B,yEAAyE;IACzE,KAAK,CAAC,EAAE,YAAY,EAAE,CAAA;IACtB,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,kEAAkE;IAClE,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IAC5C,6EAA6E;IAC7E,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC1B;AAED,MAAM,WAAW,gBAAgB;IAC/B,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,UAAU,EAAE,UAAU,CAAA;IACtB,YAAY,EAAE,sBAAsB,CAAA;CACrC;AAED,+EAA+E;AAC/E,qBAAa,yBAA0B,SAAQ,KAAK;IAClD,WAAW,EAAE,UAAU,EAAE,CAAA;gBACb,WAAW,EAAE,UAAU,EAAE;CAKtC;AAUD,2EAA2E;AAC3E,iBAAe,aAAa,CAC1B,IAAI,EAAE,cAAc,GACnB,OAAO,CAAC;IAAE,MAAM,EAAE,SAAS,EAAE,CAAC;IAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC,CAQ5D;AAmHD;;;;GAIG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,cAAc,EACpB,GAAG,EAAE,iBAAiB,GACrB,OAAO,CAAC,gBAAgB,CAAC,CAkF3B;AAID,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAA;IACV,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,OAAO,CAAA;IACjB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,OAAO,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;IACjB,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACzC,gBAAgB,CAAC,EAAE,OAAO,CAAA;CAC3B;AAED,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,MAAM,EAAE,eAAe,EAAE,CAAA;IACzB,UAAU,EAAE,MAAM,CAAA;IAClB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,IAAI,EAAE;QACJ,gBAAgB,CAAC,EAAE,MAAM,CAAA;QACzB,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAC1B,CAAA;IACD,OAAO,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAA;IAC/C,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAChC;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,cAAc,EACpB,MAAM,EAAE,SAAS,EAAE,EACnB,eAAe,EAAE,MAAM,GAAG,IAAI,EAC9B,OAAO,GAAE;IAAE,OAAO,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAA;CAAO,GAChE,UAAU,CAwCZ;AAQD,OAAO,EAAE,aAAa,IAAI,uBAAuB,EAAE,CAAA"}
|