security headers, notifications, GDPR consent, masked fields

This commit is contained in:
2026-08-21 19:03:14 +02:00
parent 81fa409513
commit 05b3dc8cb1
37 changed files with 569 additions and 14 deletions
+9 -2
View File
@@ -14,7 +14,7 @@ import { validateSubmission } from './validateSubmission.js';
* which goes out over panelSmtpAdapter. Storing the submission is enough.
*
* server-only: touches the Turnstile secret.
*/ export async function submitForm({ data, formId, ip, maxPerMinute = 5, payload, turnstileToken }) {
*/ export async function submitForm({ consentFieldName, data, formId, ip, maxPerMinute = 5, payload, turnstileToken }) {
// 1. Rate limit — cheapest gate, drops a flood before any real work.
if (maxPerMinute > 0 && ip) {
if (!checkRateLimit({
@@ -43,7 +43,7 @@ import { validateSubmission } from './validateSubmission.js';
}
// 3. Validate against the form's schema. A public endpoint can't trust the
// shape of `data` — drop unknown keys, enforce required, cap length.
const validation = await validateSubmission(payload, formId, data);
const validation = await validateSubmission(payload, formId, data, consentFieldName);
if (!validation.ok) {
if (validation.reason === 'not_found') {
return {
@@ -51,6 +51,13 @@ import { validateSubmission } from './validateSubmission.js';
success: false
};
}
if (validation.reason === 'consent') {
return {
field: validation.field,
reason: 'consent',
success: false
};
}
return {
reason: 'validation',
success: false,
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1 +1 @@
{"version":3,"sources":["../../../src/modules/forms/types.ts"],"sourcesContent":["import type { CollectionConfig, Field } from 'payload'\n\n/**\n * Receives the collection's default fields and returns the final list — add,\n * remove, or reorder. Same shape the form-builder uses.\n */\nexport type FormsFieldsOverride = (args: { defaultFields: Field[] }) => Field[]\n\n/**\n * Overrides for a forms-related collection: replace the fields and/or any\n * other collection setting (admin, access, hooks…).\n */\nexport type FormsCollectionOverrides = {\n fields?: FormsFieldsOverride\n} & Partial<Omit<CollectionConfig, 'fields'>>\n\n/**\n * Forms configuration — mirrors the fields a client enables in the\n * form-builder plugin. Kept minimal; the plugin passes these through.\n */\nexport type FormsOption = {\n /** Field types available in the form builder. Sensible defaults applied. */\n fields?: {\n checkbox?: boolean\n email?: boolean\n message?: boolean\n number?: boolean\n payment?: boolean\n select?: boolean\n text?: boolean\n textarea?: boolean\n }\n /**\n * Override the forms collection. The plugin stays opinion-free about what a\n * form needs beyond its fields — a client that wants, say, a per-form\n * notification address adds it here:\n *\n * formOverrides: {\n * fields: ({ defaultFields }) => [\n * ...defaultFields,\n * { name: 'notificationEmail', type: 'email' },\n * ],\n * }\n */\n formOverrides?: FormsCollectionOverrides\n /** Override the form-submissions collection (same shape). */\n formSubmissionOverrides?: FormsCollectionOverrides\n /** Collections a form can redirect to (e.g. ['pages']). */\n redirectRelationships?: string[]\n}\n"],"names":[],"mappings":"AAgBA;;;CAGC,GACD,WA6BC"}
{"version":3,"sources":["../../../src/modules/forms/types.ts"],"sourcesContent":["import type { CollectionConfig, Field } from 'payload'\n\n/**\n * Receives the collection's default fields and returns the final list — add,\n * remove, or reorder. Same shape the form-builder uses.\n */\nexport type FormsFieldsOverride = (args: { defaultFields: Field[] }) => Field[]\n\n/**\n * Overrides for a forms-related collection: replace the fields and/or any\n * other collection setting (admin, access, hooks…).\n */\nexport type FormsCollectionOverrides = {\n fields?: FormsFieldsOverride\n} & Partial<Omit<CollectionConfig, 'fields'>>\n\n/**\n * Forms configuration — mirrors the fields a client enables in the\n * form-builder plugin. Kept minimal; the plugin passes these through.\n */\nexport type FormsOption = {\n /**\n * Name of the checkbox field treated as a GDPR consent gate. A form field\n * with this name must be checked for submission to succeed — enforced\n * server-side in submitForm. Defaults to 'consent'.\n */\n consentFieldName?: string\n /** Field types available in the form builder. Sensible defaults applied. */\n fields?: {\n checkbox?: boolean\n email?: boolean\n message?: boolean\n number?: boolean\n payment?: boolean\n select?: boolean\n text?: boolean\n textarea?: boolean\n }\n /**\n * Override the forms collection. The plugin stays opinion-free about what a\n * form needs beyond its fields — a client that wants, say, a per-form\n * notification address adds it here:\n *\n * formOverrides: {\n * fields: ({ defaultFields }) => [\n * ...defaultFields,\n * { name: 'notificationEmail', type: 'email' },\n * ],\n * }\n */\n formOverrides?: FormsCollectionOverrides\n /** Override the form-submissions collection (same shape). */\n formSubmissionOverrides?: FormsCollectionOverrides\n /** Collections a form can redirect to (e.g. ['pages']). */\n redirectRelationships?: string[]\n}\n"],"names":[],"mappings":"AAgBA;;;CAGC,GACD,WAmCC"}
+22 -2
View File
@@ -12,6 +12,14 @@
'g-recaptcha-response'
]);
/** Hard ceiling on a single field's length, independent of the form config. */ const MAX_FIELD_LENGTH = 5000;
/**
* Default name for a GDPR consent field. A checkbox with this name is treated
* as a consent gate: it MUST be checked for the submission to go through,
* enforced here server-side regardless of how the field was configured in the
* panel (so an editor can't weaken it by forgetting `required` or, worse,
* pre-ticking it with defaultValue: true — which GDPR forbids). Configurable
* via FormsOption.consentFieldName.
*/ const DEFAULT_CONSENT_FIELD = 'consent';
/**
* Checks submitted data against the form's own definition, rather than trusting
* whatever arrived.
@@ -24,7 +32,7 @@
*
* Returns the loaded form on success so the caller doesn't fetch it twice, and
* a code + offending field on failure so the frontend can point at it.
*/ export async function validateSubmission(payload, formId, data) {
*/ export async function validateSubmission(payload, formId, data, consentFieldName = DEFAULT_CONSENT_FIELD) {
let form;
try {
form = await payload.findByID({
@@ -47,7 +55,19 @@
for (const field of fields){
const value = data[field.name];
const isBlank = value == null || typeof value === 'string' && value.trim() === '' || value === false;
if (field.required && isBlank) {
// GDPR consent gate: a field matching the consent name must be truthy
// (checked). Enforced independently of `required`, so it can't be weakened
// in the panel. This is the one field where server-side enforcement is the
// legal guarantee — the frontend can't bypass it, the editor can't misset it.
if (field.name === consentFieldName) {
if (value !== true) {
return {
field: field.name,
ok: false,
reason: 'consent'
};
}
} else if (field.required && isBlank) {
return {
field: field.name,
kind: 'required',
File diff suppressed because one or more lines are too long
+17
View File
@@ -0,0 +1,17 @@
/**
* Built-in English fallbacks, used per field when the Notifications global
* leaves a text empty. Same philosophy as consent FALLBACK: the site works out
* of the box, editors override per locale as needed.
*/ export const NOTIFICATION_FALLBACK = {
form: {
success: 'Thank you — your message has been sent.',
error: 'Something went wrong. Please try again later.',
rateLimited: 'Too many attempts. Please wait a moment and try again.',
turnstile: 'Captcha verification failed. Please try again.',
validation: 'Please check the {field} field and try again.',
consent: 'Please accept the privacy policy to continue.',
notFound: 'This form is no longer available.'
}
};
//# sourceMappingURL=defaults.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"sources":["../../../src/modules/notifications/defaults.ts"],"sourcesContent":["import type { NotificationTexts } from './types.js'\n\n/**\n * Built-in English fallbacks, used per field when the Notifications global\n * leaves a text empty. Same philosophy as consent FALLBACK: the site works out\n * of the box, editors override per locale as needed.\n */\nexport const NOTIFICATION_FALLBACK: NotificationTexts = {\n form: {\n success: 'Thank you — your message has been sent.',\n error: 'Something went wrong. Please try again later.',\n rateLimited: 'Too many attempts. Please wait a moment and try again.',\n turnstile: 'Captcha verification failed. Please try again.',\n validation: 'Please check the {field} field and try again.',\n consent: 'Please accept the privacy policy to continue.',\n notFound: 'This form is no longer available.',\n },\n}\n"],"names":["NOTIFICATION_FALLBACK","form","success","error","rateLimited","turnstile","validation","consent","notFound"],"mappings":"AAEA;;;;CAIC,GACD,OAAO,MAAMA,wBAA2C;IACtDC,MAAM;QACJC,SAAS;QACTC,OAAO;QACPC,aAAa;QACbC,WAAW;QACXC,YAAY;QACZC,SAAS;QACTC,UAAU;IACZ;AACF,EAAC"}
+27
View File
@@ -0,0 +1,27 @@
import { getGlobal } from '../payload/index.js';
import { NOTIFICATION_FALLBACK } from './defaults.js';
/**
* Resolves notification texts from the Notifications global, falling back to
* English defaults per field. Mirrors getConsentTexts: one read, per-field
* fallback, locale-aware. The frontend maps a submitForm result code to the
* matching text and styles it however it likes (toast, inline, banner).
*/ export async function getNotificationTexts({ locale, payload }) {
const g = await getGlobal(payload, 'notifications', {
locale
});
const f = g.form ?? {};
const fb = NOTIFICATION_FALLBACK.form;
return {
form: {
consent: f.consent || fb.consent,
error: f.error || fb.error,
notFound: f.notFound || fb.notFound,
rateLimited: f.rateLimited || fb.rateLimited,
success: f.success || fb.success,
turnstile: f.turnstile || fb.turnstile,
validation: f.validation || fb.validation
}
};
}
//# sourceMappingURL=getNotificationTexts.js.map
@@ -0,0 +1 @@
{"version":3,"sources":["../../../src/modules/notifications/getNotificationTexts.ts"],"sourcesContent":["import type { BasePayload } from 'payload'\n\nimport type { NotificationsData, NotificationTexts } from './types.js'\n\nimport { getGlobal } from '../payload/index.js'\nimport { NOTIFICATION_FALLBACK } from './defaults.js'\n\ntype GetNotificationTextsArgs = {\n /** Active locale — selects the language variant of each text. */\n locale?: string\n payload: BasePayload\n}\n\n/**\n * Resolves notification texts from the Notifications global, falling back to\n * English defaults per field. Mirrors getConsentTexts: one read, per-field\n * fallback, locale-aware. The frontend maps a submitForm result code to the\n * matching text and styles it however it likes (toast, inline, banner).\n */\nexport async function getNotificationTexts({\n locale,\n payload,\n}: GetNotificationTextsArgs): Promise<NotificationTexts> {\n const g = await getGlobal<NotificationsData>(payload, 'notifications', { locale })\n\n const f = g.form ?? {}\n const fb = NOTIFICATION_FALLBACK.form\n\n return {\n form: {\n consent: f.consent || fb.consent,\n error: f.error || fb.error,\n notFound: f.notFound || fb.notFound,\n rateLimited: f.rateLimited || fb.rateLimited,\n success: f.success || fb.success,\n turnstile: f.turnstile || fb.turnstile,\n validation: f.validation || fb.validation,\n },\n }\n}\n"],"names":["getGlobal","NOTIFICATION_FALLBACK","getNotificationTexts","locale","payload","g","f","form","fb","consent","error","notFound","rateLimited","success","turnstile","validation"],"mappings":"AAIA,SAASA,SAAS,QAAQ,sBAAqB;AAC/C,SAASC,qBAAqB,QAAQ,gBAAe;AAQrD;;;;;CAKC,GACD,OAAO,eAAeC,qBAAqB,EACzCC,MAAM,EACNC,OAAO,EACkB;IACzB,MAAMC,IAAI,MAAML,UAA6BI,SAAS,iBAAiB;QAAED;IAAO;IAEhF,MAAMG,IAAID,EAAEE,IAAI,IAAI,CAAC;IACrB,MAAMC,KAAKP,sBAAsBM,IAAI;IAErC,OAAO;QACLA,MAAM;YACJE,SAASH,EAAEG,OAAO,IAAID,GAAGC,OAAO;YAChCC,OAAOJ,EAAEI,KAAK,IAAIF,GAAGE,KAAK;YAC1BC,UAAUL,EAAEK,QAAQ,IAAIH,GAAGG,QAAQ;YACnCC,aAAaN,EAAEM,WAAW,IAAIJ,GAAGI,WAAW;YAC5CC,SAASP,EAAEO,OAAO,IAAIL,GAAGK,OAAO;YAChCC,WAAWR,EAAEQ,SAAS,IAAIN,GAAGM,SAAS;YACtCC,YAAYT,EAAES,UAAU,IAAIP,GAAGO,UAAU;QAC3C;IACF;AACF"}
+5
View File
@@ -0,0 +1,5 @@
export { NOTIFICATION_FALLBACK } from './defaults.js';
export { getNotificationTexts } from './getNotificationTexts.js';
export { resolveFormMessage } from './resolveFormMessage.js';
//# sourceMappingURL=index.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"sources":["../../../src/modules/notifications/index.ts"],"sourcesContent":["export { NOTIFICATION_FALLBACK } from './defaults.js'\nexport { getNotificationTexts } from './getNotificationTexts.js'\nexport { resolveFormMessage } from './resolveFormMessage.js'\nexport type { FormNotificationTexts, NotificationsData, NotificationTexts } from './types.js'\n"],"names":["NOTIFICATION_FALLBACK","getNotificationTexts","resolveFormMessage"],"mappings":"AAAA,SAASA,qBAAqB,QAAQ,gBAAe;AACrD,SAASC,oBAAoB,QAAQ,4BAA2B;AAChE,SAASC,kBAAkB,QAAQ,0BAAyB"}
+35
View File
@@ -0,0 +1,35 @@
/**
* Maps a submitForm result to the user-facing message, interpolating {field}
* for validation errors. This is the bridge the frontend uses: it gets a result
* code from submitForm and the resolved texts from getNotificationTexts, and
* this turns them into one string to display. Keeping the mapping here means the
* frontend never hard-codes messages or knows about result codes.
*
* Never surfaces raw backend/exception detail — 'error' maps to a friendly
* generic message, not the thrown error's text (which could leak internals).
*/ export function resolveFormMessage(result, texts) {
if (result.success) {
return texts.success;
}
switch(result.reason){
case 'consent':
return texts.consent;
case 'not_found':
return texts.notFound;
case 'rate_limited':
return texts.rateLimited;
case 'turnstile':
return texts.turnstile;
case 'validation':
{
// Interpolate {field} with the offending field name when present.
const field = 'field' in result && result.field ? result.field : '';
return texts.validation.replace('{field}', field);
}
case 'error':
default:
return texts.error;
}
}
//# sourceMappingURL=resolveFormMessage.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"sources":["../../../src/modules/notifications/resolveFormMessage.ts"],"sourcesContent":["import type { SubmitFormResult } from '../forms/index.js'\nimport type { FormNotificationTexts } from './types.js'\n\n/**\n * Maps a submitForm result to the user-facing message, interpolating {field}\n * for validation errors. This is the bridge the frontend uses: it gets a result\n * code from submitForm and the resolved texts from getNotificationTexts, and\n * this turns them into one string to display. Keeping the mapping here means the\n * frontend never hard-codes messages or knows about result codes.\n *\n * Never surfaces raw backend/exception detail — 'error' maps to a friendly\n * generic message, not the thrown error's text (which could leak internals).\n */\nexport function resolveFormMessage(result: SubmitFormResult, texts: FormNotificationTexts): string {\n if (result.success) {return texts.success}\n\n switch (result.reason) {\n case 'consent':\n return texts.consent\n case 'not_found':\n return texts.notFound\n case 'rate_limited':\n return texts.rateLimited\n case 'turnstile':\n return texts.turnstile\n case 'validation': {\n // Interpolate {field} with the offending field name when present.\n const field = 'field' in result && result.field ? result.field : ''\n return texts.validation.replace('{field}', field)\n }\n case 'error':\n default:\n return texts.error\n }\n}\n"],"names":["resolveFormMessage","result","texts","success","reason","consent","notFound","rateLimited","turnstile","field","validation","replace","error"],"mappings":"AAGA;;;;;;;;;CASC,GACD,OAAO,SAASA,mBAAmBC,MAAwB,EAAEC,KAA4B;IACvF,IAAID,OAAOE,OAAO,EAAE;QAAC,OAAOD,MAAMC,OAAO;IAAA;IAEzC,OAAQF,OAAOG,MAAM;QACnB,KAAK;YACH,OAAOF,MAAMG,OAAO;QACtB,KAAK;YACH,OAAOH,MAAMI,QAAQ;QACvB,KAAK;YACH,OAAOJ,MAAMK,WAAW;QAC1B,KAAK;YACH,OAAOL,MAAMM,SAAS;QACxB,KAAK;YAAc;gBACjB,kEAAkE;gBAClE,MAAMC,QAAQ,WAAWR,UAAUA,OAAOQ,KAAK,GAAGR,OAAOQ,KAAK,GAAG;gBACjE,OAAOP,MAAMQ,UAAU,CAACC,OAAO,CAAC,WAAWF;YAC7C;QACA,KAAK;QACL;YACE,OAAOP,MAAMU,KAAK;IACtB;AACF"}
+6
View File
@@ -0,0 +1,6 @@
/**
* Resolved notification texts, ready for the frontend. Grouped per context;
* `form` maps submitForm result codes to user-facing messages.
*/ /** Raw shape read from the Notifications global (all fields optional). */ export { };
//# sourceMappingURL=types.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"sources":["../../../src/modules/notifications/types.ts"],"sourcesContent":["/**\n * Resolved notification texts, ready for the frontend. Grouped per context;\n * `form` maps submitForm result codes to user-facing messages.\n */\nexport type FormNotificationTexts = {\n success: string\n error: string\n rateLimited: string\n turnstile: string\n /** May contain the {field} placeholder — resolve with resolveValidationText. */\n validation: string\n /** Shown when a required GDPR consent checkbox was left unchecked. */\n consent: string\n notFound: string\n}\n\nexport type NotificationTexts = {\n form: FormNotificationTexts\n}\n\n/** Raw shape read from the Notifications global (all fields optional). */\nexport type NotificationsData = {\n form?: Partial<FormNotificationTexts>\n}\n"],"names":[],"mappings":"AAAA;;;CAGC,GAiBD,wEAAwE,GACxE,WAEC"}
+70
View File
@@ -0,0 +1,70 @@
/**
* A single HTTP header, in the shape Next.js next.config headers() expects.
*/
export type SecurityHeader = {
key: string;
value: string;
};
export type BuildSecurityHeadersArgs = {
/**
* Extra headers to append or override. Same-key entries replace the default,
* so you can e.g. add your project's Content-Security-Policy here — CSP is
* intentionally NOT a default because it depends on the project's own
* domains (scripts, images, fonts, analytics). Keep CSP in your project.
*/
additional?: SecurityHeader[];
/**
* X-Frame-Options value. 'DENY' (default) blocks all framing; 'SAMEORIGIN'
* allows same-origin framing. Note: CSP frame-ancestors supersedes this in
* modern browsers, but X-Frame-Options is kept for older ones. Set to null
* to omit (e.g. if you set frame-ancestors in your project CSP).
*/
frameOptions?: 'DENY' | 'SAMEORIGIN' | null;
/**
* Enable HSTS (Strict-Transport-Security). Only takes effect over HTTPS, and
* tells browsers to force HTTPS for `maxAge` seconds. Default true. Turn OFF
* in local/dev over plain HTTP, or you may lock the browser to https on
* localhost. Set the env guard in your next.config (see docs).
*/
hsts?: boolean;
/** Add includeSubDomains to HSTS. Default true. */
hstsIncludeSubDomains?: boolean;
/** HSTS max-age in seconds. Default 63072000 (2 years), the common baseline. */
hstsMaxAge?: number;
/** Add preload to HSTS (only if you'll submit to the preload list). Default false. */
hstsPreload?: boolean;
/**
* Permissions-Policy. Default disables camera, microphone, geolocation. Pass
* your own string to override, or null to omit.
*/
permissionsPolicy?: null | string;
/** Referrer-Policy. Default 'strict-origin-when-cross-origin' (browser default, explicit). */
referrerPolicy?: null | string;
};
/**
* Builds the generic, project-independent security headers every site should
* send: HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy,
* Permissions-Policy. These are identical across projects, so the plugin owns
* the boilerplate; the client spreads the result into next.config's headers().
*
* Content-Security-Policy is deliberately excluded: a useful CSP enumerates the
* exact domains a project loads from (its CDN, analytics, embeds), so it can't
* be generic without being either too loose (useless) or too strict (breaks the
* site). Add your project's CSP via `additional`.
*
* @example
* // next.config.ts
* import { buildSecurityHeaders } from '@intecion/ipal-kit'
* const securityHeaders = buildSecurityHeaders({
* hsts: process.env.NODE_ENV === 'production', // off in dev over http
* additional: [
* { key: 'Content-Security-Policy', value: "default-src 'self'; ..." },
* ],
* })
* const nextConfig = {
* async headers() {
* return [{ source: '/:path*', headers: securityHeaders }]
* },
* }
*/
export declare function buildSecurityHeaders(args?: BuildSecurityHeadersArgs): SecurityHeader[];
+81
View File
@@ -0,0 +1,81 @@
/**
* A single HTTP header, in the shape Next.js next.config headers() expects.
*/ /**
* Builds the generic, project-independent security headers every site should
* send: HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy,
* Permissions-Policy. These are identical across projects, so the plugin owns
* the boilerplate; the client spreads the result into next.config's headers().
*
* Content-Security-Policy is deliberately excluded: a useful CSP enumerates the
* exact domains a project loads from (its CDN, analytics, embeds), so it can't
* be generic without being either too loose (useless) or too strict (breaks the
* site). Add your project's CSP via `additional`.
*
* @example
* // next.config.ts
* import { buildSecurityHeaders } from '@intecion/ipal-kit'
* const securityHeaders = buildSecurityHeaders({
* hsts: process.env.NODE_ENV === 'production', // off in dev over http
* additional: [
* { key: 'Content-Security-Policy', value: "default-src 'self'; ..." },
* ],
* })
* const nextConfig = {
* async headers() {
* return [{ source: '/:path*', headers: securityHeaders }]
* },
* }
*/ export function buildSecurityHeaders(args = {}) {
const { additional = [], frameOptions = 'DENY', hsts = true, hstsIncludeSubDomains = true, hstsMaxAge = 63072000, hstsPreload = false, permissionsPolicy = 'camera=(), microphone=(), geolocation=()', referrerPolicy = 'strict-origin-when-cross-origin' } = args;
const headers = [];
if (hsts) {
const parts = [
`max-age=${hstsMaxAge}`
];
if (hstsIncludeSubDomains) {
parts.push('includeSubDomains');
}
if (hstsPreload) {
parts.push('preload');
}
headers.push({
key: 'Strict-Transport-Security',
value: parts.join('; ')
});
}
if (frameOptions) {
headers.push({
key: 'X-Frame-Options',
value: frameOptions
});
}
// Prevents MIME-type sniffing — always safe, no project specifics.
headers.push({
key: 'X-Content-Type-Options',
value: 'nosniff'
});
if (referrerPolicy) {
headers.push({
key: 'Referrer-Policy',
value: referrerPolicy
});
}
if (permissionsPolicy) {
headers.push({
key: 'Permissions-Policy',
value: permissionsPolicy
});
}
// Merge additional: same-key entries override the defaults above.
for (const extra of additional){
const i = headers.findIndex((h)=>h.key.toLowerCase() === extra.key.toLowerCase());
if (i >= 0) {
headers[i] = extra;
} else {
headers.push(extra);
}
}
return headers;
}
//# sourceMappingURL=buildSecurityHeaders.js.map
File diff suppressed because one or more lines are too long
+2
View File
@@ -0,0 +1,2 @@
export { buildSecurityHeaders } from './buildSecurityHeaders.js';
export type { BuildSecurityHeadersArgs, SecurityHeader } from './buildSecurityHeaders.js';
+3
View File
@@ -0,0 +1,3 @@
export { buildSecurityHeaders } from './buildSecurityHeaders.js';
//# sourceMappingURL=index.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"sources":["../../../src/modules/security/index.ts"],"sourcesContent":["export { buildSecurityHeaders } from './buildSecurityHeaders.js'\nexport type { BuildSecurityHeadersArgs, SecurityHeader } from './buildSecurityHeaders.js'\n"],"names":["buildSecurityHeaders"],"mappings":"AAAA,SAASA,oBAAoB,QAAQ,4BAA2B"}