safe-formdata 0.1.4 → 0.2.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/README.md CHANGED
@@ -224,27 +224,25 @@ const { data, issues } = parse(formData);
224
224
  ### Result
225
225
 
226
226
  ```ts
227
- export interface ParseResult {
228
- data: Record<string, string | File> | null;
229
- issues: ParseIssue[];
230
- }
227
+ export type ParseResult =
228
+ | { data: Record<string, string | File>; issues: [] }
229
+ | { data: null; issues: [ParseIssue, ...ParseIssue[]] };
231
230
  ```
232
231
 
233
232
  - `data` is non-null only when no boundary violations are detected
234
233
  - `data` is always a flat object; no structural inference is performed
235
- - `issues` must always be checked by the caller
234
+ - Use `data !== null` to narrow the type; `issues` is `[]` on success and non-empty on failure
236
235
 
237
236
  ### Issues
238
237
 
239
238
  ```ts
240
239
  export interface ParseIssue {
241
240
  code: "invalid_key" | "forbidden_key" | "duplicate_key";
242
- path: string[];
243
- key?: unknown;
241
+ key: string;
244
242
  }
245
243
  ```
246
244
 
247
- - `path` is always empty and exists only for compatibility
245
+ - `key` is the original FormData key that caused the issue
248
246
  - Issues are informational and are never thrown
249
247
 
250
248
  ---
package/dist/index.d.ts CHANGED
@@ -2,8 +2,7 @@ type IssueCode = "invalid_key" | "forbidden_key" | "duplicate_key";
2
2
 
3
3
  interface ParseIssue {
4
4
  code: IssueCode;
5
- path: readonly [];
6
- key?: unknown;
5
+ key: string;
7
6
  }
8
7
 
9
8
  type ParseResult = {
@@ -11,9 +10,16 @@ type ParseResult = {
11
10
  issues: [];
12
11
  } | {
13
12
  data: null;
14
- issues: ParseIssue[];
13
+ issues: [ParseIssue, ...ParseIssue[]];
15
14
  };
16
15
 
17
16
  declare function parse(formData: FormData): ParseResult;
18
17
 
19
- export { type IssueCode, type ParseIssue, type ParseResult, parse };
18
+ type SuccessResult = Extract<ParseResult, {
19
+ data: Record<string, string | File>;
20
+ }>;
21
+ type FailureResult = Extract<ParseResult, {
22
+ data: null;
23
+ }>;
24
+
25
+ export { type FailureResult, type IssueCode, type ParseIssue, type ParseResult, type SuccessResult, parse };
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- function e(e,t={}){return{code:e,path:[],...t}}var t=new Set(["__proto__","prototype","constructor"]);function n(n){const o=Object.create(null),s=[],r=new Set;for(const[u,a]of n.entries())"string"==typeof u&&0!==u.length?t.has(u)?s.push(e("forbidden_key",{key:u})):r.has(u)?s.push(e("duplicate_key",{key:u})):(r.add(u),o[u]=a):s.push(e("invalid_key",{key:u}));return s.length>0?{data:null,issues:s}:{data:o,issues:[]}}export{n as parse};//# sourceMappingURL=index.js.map
1
+ function e(e,t){return{code:e,key:t}}var t=new Set(["__proto__","prototype","constructor"]);function n(n){const o=Object.create(null),s=[],r=new Set;for(const[u,a]of n.entries())"string"==typeof u&&0!==u.length?t.has(u)?s.push(e("forbidden_key",u)):r.has(u)?s.push(e("duplicate_key",u)):(r.add(u),o[u]=a):s.push(e("invalid_key",u));const[u,...a]=s;return void 0!==u?{data:null,issues:[u,...a]}:{data:o,issues:[]}}export{n as parse};//# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/issues/createIssue.ts","../src/issues/forbiddenKeys.ts","../src/parse.ts"],"sourcesContent":["import type { IssueCode } from \"#types/IssueCode\";\nimport type { ParseIssue } from \"#types/ParseIssue\";\n\n/**\n * Payload for creating a ParseIssue.\n *\n * @property {unknown} key - The problematic key that triggered the issue (for debugging)\n */\ninterface IssuePayload {\n\tkey?: unknown;\n}\n\n/**\n * Creates a ParseIssue with the specified code and optional payload.\n *\n * This is an internal utility function for generating structured issue reports\n * during FormData parsing. All issues have an empty `path` array, as this parser\n * does not infer structural relationships.\n *\n * @param code - The type of issue (invalid_key, forbidden_key, or duplicate_key)\n * @param payload - Optional payload containing the problematic key\n * @returns A ParseIssue object ready to be added to the issues array\n *\n * @internal\n */\nexport function createIssue(code: IssueCode, payload: IssuePayload = {}): ParseIssue {\n\treturn {\n\t\tcode,\n\t\tpath: [],\n\t\t...payload,\n\t};\n}\n","/**\n * Keys explicitly forbidden to prevent prototype pollution attacks.\n *\n * These keys are reserved properties on `Object.prototype` and must never\n * be allowed in parsed FormData, regardless of their values or context.\n *\n * The forbidden keys are the following.\n * - `__proto__`: Legacy prototype accessor\n * - `prototype`: Function prototype property\n * - `constructor`: Object constructor reference\n *\n * Any FormData entry containing these keys will trigger a `forbidden_key` issue,\n * causing the parse operation to fail with `data: null`.\n *\n * @see {@link https://github.com/roottool/safe-formdata/blob/main/AGENTS.md#prototype-safety AGENTS.md > Security rules > Prototype safety}\n */\nexport const FORBIDDEN_KEYS = new Set<string>([\n\t\"__proto__\",\n\t\"prototype\",\n\t\"constructor\",\n] as const satisfies readonly string[]);\n","import { createIssue } from \"#issues/createIssue\";\nimport { FORBIDDEN_KEYS } from \"#issues/forbiddenKeys\";\nimport type { ParseResult } from \"#types/ParseResult\";\n\n/**\n * Parses FormData into a flat JavaScript object with strict boundary enforcement.\n *\n * This function establishes a security-focused boundary between untrusted FormData input\n * and application logic by:\n * - Detecting duplicate, forbidden, and invalid keys\n * - Treating keys as opaque strings (no structural inference)\n * - Returning null data if any issues are detected (no partial success)\n *\n * @param formData - The FormData instance to parse\n * @returns ParseResult containing either parsed data or issues (never both)\n *\n * @example\n * ```ts\n * const fd = new FormData()\n * fd.append('name', 'alice')\n * const result = parse(fd)\n *\n * if (result.data) {\n * // Success: result.data is { name: 'alice' }\n * } else {\n * // Failure: result.issues contains detected problems\n * }\n * ```\n *\n * @see {@link https://github.com/roottool/safe-formdata/blob/main/AGENTS.md AGENTS.md} for design rules\n */\nexport function parse(formData: FormData): ParseResult {\n\tconst data = Object.create(null) as Record<string, string | File>;\n\tconst issues = [];\n\tconst seenKeys = new Set<string>();\n\n\tfor (const [key, value] of formData.entries()) {\n\t\tif (typeof key !== \"string\" || key.length === 0) {\n\t\t\tissues.push(createIssue(\"invalid_key\", { key }));\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (FORBIDDEN_KEYS.has(key)) {\n\t\t\tissues.push(createIssue(\"forbidden_key\", { key }));\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (seenKeys.has(key)) {\n\t\t\tissues.push(createIssue(\"duplicate_key\", { key }));\n\t\t\tcontinue;\n\t\t}\n\n\t\tseenKeys.add(key);\n\t\tdata[key] = value;\n\t}\n\n\treturn issues.length > 0\n\t\t? {\n\t\t\t\tdata: null,\n\t\t\t\tissues,\n\t\t\t}\n\t\t: {\n\t\t\t\tdata,\n\t\t\t\tissues: [],\n\t\t\t};\n}\n"],"mappings":";AAyBO,SAAS,YAAY,MAAiB,UAAwB,CAAC,GAAe;AACpF,SAAO;AAAA,IACN;AAAA,IACA,MAAM,CAAC;AAAA,IACP,GAAG;AAAA,EACJ;AACD;;;ACfO,IAAM,iBAAiB,oBAAI,IAAY;AAAA,EAC7C;AAAA,EACA;AAAA,EACA;AACD,CAAsC;;;ACW/B,SAAS,MAAM,UAAiC;AACtD,QAAM,OAAO,uBAAO,OAAO,IAAI;AAC/B,QAAM,SAAS,CAAC;AAChB,QAAM,WAAW,oBAAI,IAAY;AAEjC,aAAW,CAAC,KAAK,KAAK,KAAK,SAAS,QAAQ,GAAG;AAC9C,QAAI,OAAO,QAAQ,YAAY,IAAI,WAAW,GAAG;AAChD,aAAO,KAAK,YAAY,eAAe,EAAE,IAAI,CAAC,CAAC;AAC/C;AAAA,IACD;AAEA,QAAI,eAAe,IAAI,GAAG,GAAG;AAC5B,aAAO,KAAK,YAAY,iBAAiB,EAAE,IAAI,CAAC,CAAC;AACjD;AAAA,IACD;AAEA,QAAI,SAAS,IAAI,GAAG,GAAG;AACtB,aAAO,KAAK,YAAY,iBAAiB,EAAE,IAAI,CAAC,CAAC;AACjD;AAAA,IACD;AAEA,aAAS,IAAI,GAAG;AAChB,SAAK,GAAG,IAAI;AAAA,EACb;AAEA,SAAO,OAAO,SAAS,IACpB;AAAA,IACA,MAAM;AAAA,IACN;AAAA,EACD,IACC;AAAA,IACA;AAAA,IACA,QAAQ,CAAC;AAAA,EACV;AACH;","names":[]}
1
+ {"version":3,"sources":["../src/issues/createIssue.ts","../src/issues/forbiddenKeys.ts","../src/parse.ts"],"sourcesContent":["import type { IssueCode } from \"#types/IssueCode\";\nimport type { ParseIssue } from \"#types/ParseIssue\";\n\n/**\n * Creates a ParseIssue with the specified code and key.\n *\n * This is an internal utility function for generating structured issue reports\n * during FormData parsing.\n *\n * @param code - The type of issue (invalid_key, forbidden_key, or duplicate_key)\n * @param key - The FormData key that caused the issue\n * @returns A ParseIssue object ready to be added to the issues array\n *\n * @internal\n */\nexport function createIssue(code: IssueCode, key: string): ParseIssue {\n\treturn { code, key };\n}\n","/**\n * Keys explicitly forbidden to prevent prototype pollution attacks.\n *\n * These keys are reserved properties on `Object.prototype` and must never\n * be allowed in parsed FormData, regardless of their values or context.\n *\n * The forbidden keys are the following.\n * - `__proto__`: Legacy prototype accessor\n * - `prototype`: Function prototype property\n * - `constructor`: Object constructor reference\n *\n * Any FormData entry containing these keys will trigger a `forbidden_key` issue,\n * causing the parse operation to fail with `data: null`.\n *\n * @see {@link https://github.com/roottool/safe-formdata/blob/main/AGENTS.md#prototype-safety AGENTS.md > Security rules > Prototype safety}\n */\nexport const FORBIDDEN_KEYS = new Set<string>([\n\t\"__proto__\",\n\t\"prototype\",\n\t\"constructor\",\n] as const satisfies readonly string[]);\n","import { createIssue } from \"#issues/createIssue\";\nimport { FORBIDDEN_KEYS } from \"#issues/forbiddenKeys\";\nimport type { ParseIssue } from \"#types/ParseIssue\";\nimport type { ParseResult } from \"#types/ParseResult\";\n\n/**\n * Parses FormData into a flat JavaScript object with strict boundary enforcement.\n *\n * This function establishes a security-focused boundary between untrusted FormData input\n * and application logic by:\n * - Detecting duplicate, forbidden, and invalid keys\n * - Treating keys as opaque strings (no structural inference)\n * - Returning null data if any issues are detected (no partial success)\n *\n * @param formData - The FormData instance to parse\n * @returns ParseResult containing either parsed data or issues (never both)\n *\n * @example\n * ```ts\n * const fd = new FormData()\n * fd.append('name', 'alice')\n * const result = parse(fd)\n *\n * if (result.data !== null) {\n * // Success: result.data is { name: 'alice' }\n * } else {\n * // Failure: result.issues contains detected problems\n * }\n * ```\n *\n * @see {@link https://github.com/roottool/safe-formdata/blob/main/AGENTS.md AGENTS.md} for design rules\n */\nexport function parse(formData: FormData): ParseResult {\n\tconst data: Record<string, string | File> = Object.create(null);\n\tconst issues: ParseIssue[] = [];\n\tconst seenKeys = new Set<string>();\n\n\tfor (const [key, value] of formData.entries()) {\n\t\tif (typeof key !== \"string\" || key.length === 0) {\n\t\t\tissues.push(createIssue(\"invalid_key\", key));\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (FORBIDDEN_KEYS.has(key)) {\n\t\t\tissues.push(createIssue(\"forbidden_key\", key));\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (seenKeys.has(key)) {\n\t\t\tissues.push(createIssue(\"duplicate_key\", key));\n\t\t\tcontinue;\n\t\t}\n\n\t\tseenKeys.add(key);\n\t\tdata[key] = value;\n\t}\n\n\t// Destructure to let TypeScript infer [ParseIssue, ...ParseIssue[]] without a type assertion\n\tconst [firstIssue, ...restIssues] = issues;\n\treturn firstIssue !== undefined\n\t\t? {\n\t\t\t\tdata: null,\n\t\t\t\tissues: [firstIssue, ...restIssues],\n\t\t\t}\n\t\t: {\n\t\t\t\tdata,\n\t\t\t\tissues: [],\n\t\t\t};\n}\n"],"mappings":";AAeO,SAAS,YAAY,MAAiB,KAAyB;AACrE,SAAO,EAAE,MAAM,IAAI;AACpB;;;ACDO,IAAM,iBAAiB,oBAAI,IAAY;AAAA,EAC7C;AAAA,EACA;AAAA,EACA;AACD,CAAsC;;;ACY/B,SAAS,MAAM,UAAiC;AACtD,QAAM,OAAsC,uBAAO,OAAO,IAAI;AAC9D,QAAM,SAAuB,CAAC;AAC9B,QAAM,WAAW,oBAAI,IAAY;AAEjC,aAAW,CAAC,KAAK,KAAK,KAAK,SAAS,QAAQ,GAAG;AAC9C,QAAI,OAAO,QAAQ,YAAY,IAAI,WAAW,GAAG;AAChD,aAAO,KAAK,YAAY,eAAe,GAAG,CAAC;AAC3C;AAAA,IACD;AAEA,QAAI,eAAe,IAAI,GAAG,GAAG;AAC5B,aAAO,KAAK,YAAY,iBAAiB,GAAG,CAAC;AAC7C;AAAA,IACD;AAEA,QAAI,SAAS,IAAI,GAAG,GAAG;AACtB,aAAO,KAAK,YAAY,iBAAiB,GAAG,CAAC;AAC7C;AAAA,IACD;AAEA,aAAS,IAAI,GAAG;AAChB,SAAK,GAAG,IAAI;AAAA,EACb;AAGA,QAAM,CAAC,YAAY,GAAG,UAAU,IAAI;AACpC,SAAO,eAAe,SACnB;AAAA,IACA,MAAM;AAAA,IACN,QAAQ,CAAC,YAAY,GAAG,UAAU;AAAA,EACnC,IACC;AAAA,IACA;AAAA,IACA,QAAQ,CAAC;AAAA,EACV;AACH;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "safe-formdata",
3
- "version": "0.1.4",
3
+ "version": "0.2.0",
4
4
  "description": "Boundary-focused FormData parser with strict security guarantees",
5
5
  "keywords": [
6
6
  "boundary",