@zanii/subprocessor 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -52,22 +52,36 @@ export interface SubprocessorGraph {
52
52
  to: string;
53
53
  ts: string;
54
54
  }>;
55
+ /** Structurally broken transfers (missing from/to/agreement/ts) — never silently dropped. */
56
+ malformed: Array<{
57
+ index: number;
58
+ reason: string;
59
+ }>;
55
60
  }
56
61
  /**
57
62
  * Build the onward-transfer graph and check it against the **declared** subprocessor
58
63
  * allow-list. `undeclared` is the load-bearing output: any receiver not on the list is a
59
64
  * provable Art. 28 violation. `missing_agreement_id` flags dpa/scc transfers with no id.
60
- * Pure — pass the transfers you recorded (from the ledger).
65
+ * Pass `controller` (the data controller/origin) to also flag undeclared SENDERS — without
66
+ * it, an undeclared `from` is indistinguishable from the origin and goes unchecked.
67
+ * Pure — pass the transfers you recorded (from the ledger). Never throws; broken entries
68
+ * land in `malformed`.
61
69
  */
62
- export declare function subprocessorGraph(transfers: Transfer[], declared: string[]): SubprocessorGraph;
70
+ export declare function subprocessorGraph(transfers: Transfer[], declared: string[], opts?: {
71
+ controller?: string;
72
+ }): SubprocessorGraph;
63
73
  export interface TransferCheck {
64
74
  ok: boolean;
65
75
  reasons: string[];
66
76
  }
67
77
  /**
68
78
  * The compliance verdict for one data object's transfers: every receiver is declared, and
69
- * every dpa/scc transfer carries an agreement id. `ok` means the recorded onward chain is
70
- * clean; it does not assert no off-record transfer happened (see the limit).
79
+ * every dpa/scc transfer carries an agreement id. Pass `controller` to also require every
80
+ * SENDER to be the controller or declared (Art. 28 covers possession, not just receipt).
81
+ * `ok` means the recorded onward chain is clean; it does not assert no off-record transfer
82
+ * happened (see the limit). Never throws — a malformed transfer fails the verdict.
71
83
  */
72
- export declare function verifyTransfers(transfers: Transfer[], declared: string[]): TransferCheck;
84
+ export declare function verifyTransfers(transfers: Transfer[], declared: string[], opts?: {
85
+ controller?: string;
86
+ }): TransferCheck;
73
87
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAuBA,MAAM,WAAW,SAAS;IACxB,0EAA0E;IAC1E,IAAI,EAAE,gBAAgB,GAAG,KAAK,GAAG,KAAK,GAAG,OAAO,CAAC;IACjD,0CAA0C;IAC1C,EAAE,CAAC,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,QAAQ;IACvB,CAAC,EAAE,CAAC,CAAC;IACL,IAAI,EAAE,uBAAuB,CAAC;IAC9B,4EAA4E;IAC5E,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,uEAAuE;IACvE,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,SAAS,CAAC;IACrB,qEAAqE;IACrE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iFAAiF;IACjF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,SAAS,CAAC;IACrB,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,mGAAmG;AACnG,wBAAgB,aAAa,CAAC,KAAK,EAAE,aAAa,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAiBxG;AAED,wBAAgB,YAAY,CAAC,CAAC,EAAE,QAAQ,GAAG,MAAM,CAEhD;AAED,MAAM,WAAW,iBAAiB;IAChC,wDAAwD;IACxD,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,yBAAyB;IACzB,KAAK,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,SAAS,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC7E,wEAAwE;IACxE,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,sEAAsE;IACtE,oBAAoB,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACzD;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,QAAQ,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,iBAAiB,CAoB9F;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,OAAO,CAAC;IACZ,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,QAAQ,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,aAAa,CAMxF"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAuBA,MAAM,WAAW,SAAS;IACxB,0EAA0E;IAC1E,IAAI,EAAE,gBAAgB,GAAG,KAAK,GAAG,KAAK,GAAG,OAAO,CAAC;IACjD,0CAA0C;IAC1C,EAAE,CAAC,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,QAAQ;IACvB,CAAC,EAAE,CAAC,CAAC;IACL,IAAI,EAAE,uBAAuB,CAAC;IAC9B,4EAA4E;IAC5E,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,uEAAuE;IACvE,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,SAAS,CAAC;IACrB,qEAAqE;IACrE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iFAAiF;IACjF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,SAAS,CAAC;IACrB,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAQD,mGAAmG;AACnG,wBAAgB,aAAa,CAAC,KAAK,EAAE,aAAa,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAiBxG;AAED,wBAAgB,YAAY,CAAC,CAAC,EAAE,QAAQ,GAAG,MAAM,CAEhD;AAED,MAAM,WAAW,iBAAiB;IAChC,wDAAwD;IACxD,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,yBAAyB;IACzB,KAAK,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,SAAS,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC7E,wEAAwE;IACxE,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,sEAAsE;IACtE,oBAAoB,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACxD,6FAA6F;IAC7F,SAAS,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACrD;AAWD;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,QAAQ,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,iBAAiB,CA6B9H;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,OAAO,CAAC;IACZ,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,QAAQ,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,aAAa,CAOxH"}
package/dist/index.js CHANGED
@@ -20,15 +20,18 @@
20
20
  * And an agreement id is a reference, not a guarantee the counterparty honored it.
21
21
  */
22
22
  import { jcsHash } from '@zanii/core';
23
+ const AGREEMENTS = ['zero-retention', 'dpa', 'scc', 'other'];
24
+ // strict ISO check — Date.parse alone accepts 'Aug 20 2026', which Python's verifier rejects
25
+ const validTs = (ts) => typeof ts === 'string' && /^\d{4}-\d{2}-\d{2}T/.test(ts) && !Number.isNaN(Date.parse(ts));
23
26
  /** Build one onward-transfer record. Record it; the `to` is checked against your declared list. */
24
27
  export function buildTransfer(input) {
25
28
  if (!input.from || !input.to)
26
29
  throw new Error('from and to are required');
27
30
  if (input.from === input.to)
28
31
  throw new Error('a transfer must cross parties (from != to)');
29
- if (!input.agreement?.type)
32
+ if (!AGREEMENTS.includes(input.agreement?.type))
30
33
  throw new Error('agreement.type is required');
31
- if (Number.isNaN(Date.parse(input.ts)))
34
+ if (!validTs(input.ts))
32
35
  throw new Error('ts must be an ISO timestamp');
33
36
  const t = {
34
37
  v: 1,
@@ -49,23 +52,48 @@ export function buildTransfer(input) {
49
52
  export function transferHash(t) {
50
53
  return jcsHash(t);
51
54
  }
55
+ const transferProblem = (t) => {
56
+ if (!t || typeof t !== 'object')
57
+ return 'not a transfer object';
58
+ if (typeof t.from !== 'string' || !t.from)
59
+ return 'missing from';
60
+ if (typeof t.to !== 'string' || !t.to)
61
+ return 'missing to';
62
+ if (typeof t.agreement?.type !== 'string')
63
+ return 'missing agreement';
64
+ if (!validTs(t.ts))
65
+ return 'missing/invalid ts';
66
+ return null;
67
+ };
52
68
  /**
53
69
  * Build the onward-transfer graph and check it against the **declared** subprocessor
54
70
  * allow-list. `undeclared` is the load-bearing output: any receiver not on the list is a
55
71
  * provable Art. 28 violation. `missing_agreement_id` flags dpa/scc transfers with no id.
56
- * Pure — pass the transfers you recorded (from the ledger).
72
+ * Pass `controller` (the data controller/origin) to also flag undeclared SENDERS — without
73
+ * it, an undeclared `from` is indistinguishable from the origin and goes unchecked.
74
+ * Pure — pass the transfers you recorded (from the ledger). Never throws; broken entries
75
+ * land in `malformed`.
57
76
  */
58
- export function subprocessorGraph(transfers, declared) {
77
+ export function subprocessorGraph(transfers, declared, opts) {
59
78
  const parties = new Set();
60
79
  const edges = [];
61
80
  const undeclared = new Set();
62
81
  const missing = [];
63
- for (const t of transfers) {
82
+ const malformed = [];
83
+ for (let i = 0; i < transfers.length; i++) {
84
+ const t = transfers[i];
85
+ const problem = transferProblem(t);
86
+ if (problem) {
87
+ malformed.push({ index: i, reason: problem });
88
+ continue;
89
+ }
64
90
  parties.add(t.from);
65
91
  parties.add(t.to);
66
92
  edges.push({ from: t.from, to: t.to, agreement: t.agreement, ts: t.ts });
67
93
  if (!declared.includes(t.to))
68
94
  undeclared.add(t.to);
95
+ if (opts?.controller !== undefined && t.from !== opts.controller && !declared.includes(t.from))
96
+ undeclared.add(t.from);
69
97
  if ((t.agreement.type === 'dpa' || t.agreement.type === 'scc') && !t.agreement.id) {
70
98
  missing.push({ to: t.to, ts: t.ts });
71
99
  }
@@ -75,16 +103,21 @@ export function subprocessorGraph(transfers, declared) {
75
103
  edges: edges.sort((a, b) => Date.parse(a.ts) - Date.parse(b.ts)),
76
104
  undeclared: [...undeclared].sort(),
77
105
  missing_agreement_id: missing,
106
+ malformed,
78
107
  };
79
108
  }
80
109
  /**
81
110
  * The compliance verdict for one data object's transfers: every receiver is declared, and
82
- * every dpa/scc transfer carries an agreement id. `ok` means the recorded onward chain is
83
- * clean; it does not assert no off-record transfer happened (see the limit).
111
+ * every dpa/scc transfer carries an agreement id. Pass `controller` to also require every
112
+ * SENDER to be the controller or declared (Art. 28 covers possession, not just receipt).
113
+ * `ok` means the recorded onward chain is clean; it does not assert no off-record transfer
114
+ * happened (see the limit). Never throws — a malformed transfer fails the verdict.
84
115
  */
85
- export function verifyTransfers(transfers, declared) {
86
- const g = subprocessorGraph(transfers, declared);
116
+ export function verifyTransfers(transfers, declared, opts) {
117
+ const g = subprocessorGraph(transfers, declared, opts);
87
118
  const reasons = [];
119
+ for (const b of g.malformed)
120
+ reasons.push(`transfer ${b.index}: ${b.reason}`);
88
121
  for (const u of g.undeclared)
89
122
  reasons.push(`data reached '${u}', which is NOT a declared subprocessor`);
90
123
  for (const m of g.missing_agreement_id)
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAoCtC,mGAAmG;AACnG,MAAM,UAAU,aAAa,CAAC,KAAoB;IAChD,IAAI,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAC;IAC1E,IAAI,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,4CAA4C,CAAC,CAAC;IAC3F,IAAI,CAAC,KAAK,CAAC,SAAS,EAAE,IAAI;QAAE,MAAM,IAAI,KAAK,CAAC,4BAA4B,CAAC,CAAC;IAC1E,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,6BAA6B,CAAC,CAAC;IACvF,MAAM,CAAC,GAAa;QAClB,CAAC,EAAE,CAAC;QACJ,IAAI,EAAE,uBAAuB;QAC7B,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,EAAE,EAAE,KAAK,CAAC,EAAE;QACZ,SAAS,EAAE,KAAK,CAAC,SAAS;QAC1B,EAAE,EAAE,KAAK,CAAC,EAAE;KACb,CAAC;IACF,IAAI,KAAK,CAAC,GAAG,KAAK,SAAS;QAAE,CAAC,CAAC,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC;IAC/C,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS;QAAE,CAAC,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;IACxD,IAAI,KAAK,CAAC,QAAQ,KAAK,SAAS;QAAE,CAAC,CAAC,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAC;IAC9D,OAAO,EAAE,MAAM,EAAE,yBAAyB,KAAK,CAAC,SAAS,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,CAAuC,EAAE,CAAC;AACvH,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,CAAW;IACtC,OAAO,OAAO,CAAC,CAAuC,CAAC,CAAC;AAC1D,CAAC;AAaD;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAAC,SAAqB,EAAE,QAAkB;IACzE,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAClC,MAAM,KAAK,GAA+B,EAAE,CAAC;IAC7C,MAAM,UAAU,GAAG,IAAI,GAAG,EAAU,CAAC;IACrC,MAAM,OAAO,GAA8C,EAAE,CAAC;IAC9D,KAAK,MAAM,CAAC,IAAI,SAAS,EAAE,CAAC;QAC1B,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QACpB,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAClB,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,SAAS,EAAE,CAAC,CAAC,SAAS,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACzE,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;YAAE,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QACnD,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,KAAK,KAAK,IAAI,CAAC,CAAC,SAAS,CAAC,IAAI,KAAK,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,EAAE,EAAE,CAAC;YAClF,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACvC,CAAC;IACH,CAAC;IACD,OAAO;QACL,OAAO,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,IAAI,EAAE;QAC5B,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAChE,UAAU,EAAE,CAAC,GAAG,UAAU,CAAC,CAAC,IAAI,EAAE;QAClC,oBAAoB,EAAE,OAAO;KAC9B,CAAC;AACJ,CAAC;AAOD;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,SAAqB,EAAE,QAAkB;IACvE,MAAM,CAAC,GAAG,iBAAiB,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IACjD,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,UAAU;QAAE,OAAO,CAAC,IAAI,CAAC,iBAAiB,CAAC,yCAAyC,CAAC,CAAC;IACxG,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,oBAAoB;QAAE,OAAO,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,EAAE,6CAA6C,CAAC,CAAC;IACpI,OAAO,EAAE,EAAE,EAAE,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC;AAC/C,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zanii/subprocessor",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Prove the onward-transfer chain - which downstream processors actually saw the data, under which agreement (zero-retention, DPA id), tied to the junction/settlement. Answers the exact GDPR Art. 28 question every AI DPA raises: 'you sent my data to OpenAI, who sent it where?' A transfer to a party not in the declared subprocessor list is a provable, flagged violation. Proves recorded transfers; an off-record leak needs an attested enclave.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://ledger.zanii.agency",
@@ -31,7 +31,7 @@
31
31
  "vitest": "^3.0.9"
32
32
  },
33
33
  "dependencies": {
34
- "@zanii/core": "0.4.0"
34
+ "@zanii/core": "^0.5.2"
35
35
  },
36
36
  "scripts": {
37
37
  "build": "tsc -p tsconfig.build.json",
package/src/index.ts CHANGED
@@ -55,12 +55,18 @@ export interface TransferInput {
55
55
  junction?: string;
56
56
  }
57
57
 
58
+ const AGREEMENTS = ['zero-retention', 'dpa', 'scc', 'other'];
59
+
60
+ // strict ISO check — Date.parse alone accepts 'Aug 20 2026', which Python's verifier rejects
61
+ const validTs = (ts: unknown): boolean =>
62
+ typeof ts === 'string' && /^\d{4}-\d{2}-\d{2}T/.test(ts) && !Number.isNaN(Date.parse(ts));
63
+
58
64
  /** Build one onward-transfer record. Record it; the `to` is checked against your declared list. */
59
65
  export function buildTransfer(input: TransferInput): { target: string; payload: Record<string, unknown> } {
60
66
  if (!input.from || !input.to) throw new Error('from and to are required');
61
67
  if (input.from === input.to) throw new Error('a transfer must cross parties (from != to)');
62
- if (!input.agreement?.type) throw new Error('agreement.type is required');
63
- if (Number.isNaN(Date.parse(input.ts))) throw new Error('ts must be an ISO timestamp');
68
+ if (!AGREEMENTS.includes(input.agreement?.type)) throw new Error('agreement.type is required');
69
+ if (!validTs(input.ts)) throw new Error('ts must be an ISO timestamp');
64
70
  const t: Transfer = {
65
71
  v: 1,
66
72
  type: 'subprocessor.transfer',
@@ -88,24 +94,46 @@ export interface SubprocessorGraph {
88
94
  undeclared: string[];
89
95
  /** Transfers with no agreement id where one is required (dpa/scc). */
90
96
  missing_agreement_id: Array<{ to: string; ts: string }>;
97
+ /** Structurally broken transfers (missing from/to/agreement/ts) — never silently dropped. */
98
+ malformed: Array<{ index: number; reason: string }>;
91
99
  }
92
100
 
101
+ const transferProblem = (t: Transfer | undefined): string | null => {
102
+ if (!t || typeof t !== 'object') return 'not a transfer object';
103
+ if (typeof t.from !== 'string' || !t.from) return 'missing from';
104
+ if (typeof t.to !== 'string' || !t.to) return 'missing to';
105
+ if (typeof t.agreement?.type !== 'string') return 'missing agreement';
106
+ if (!validTs(t.ts)) return 'missing/invalid ts';
107
+ return null;
108
+ };
109
+
93
110
  /**
94
111
  * Build the onward-transfer graph and check it against the **declared** subprocessor
95
112
  * allow-list. `undeclared` is the load-bearing output: any receiver not on the list is a
96
113
  * provable Art. 28 violation. `missing_agreement_id` flags dpa/scc transfers with no id.
97
- * Pure — pass the transfers you recorded (from the ledger).
114
+ * Pass `controller` (the data controller/origin) to also flag undeclared SENDERS — without
115
+ * it, an undeclared `from` is indistinguishable from the origin and goes unchecked.
116
+ * Pure — pass the transfers you recorded (from the ledger). Never throws; broken entries
117
+ * land in `malformed`.
98
118
  */
99
- export function subprocessorGraph(transfers: Transfer[], declared: string[]): SubprocessorGraph {
119
+ export function subprocessorGraph(transfers: Transfer[], declared: string[], opts?: { controller?: string }): SubprocessorGraph {
100
120
  const parties = new Set<string>();
101
121
  const edges: SubprocessorGraph['edges'] = [];
102
122
  const undeclared = new Set<string>();
103
123
  const missing: SubprocessorGraph['missing_agreement_id'] = [];
104
- for (const t of transfers) {
124
+ const malformed: SubprocessorGraph['malformed'] = [];
125
+ for (let i = 0; i < transfers.length; i++) {
126
+ const t = transfers[i]!;
127
+ const problem = transferProblem(t);
128
+ if (problem) {
129
+ malformed.push({ index: i, reason: problem });
130
+ continue;
131
+ }
105
132
  parties.add(t.from);
106
133
  parties.add(t.to);
107
134
  edges.push({ from: t.from, to: t.to, agreement: t.agreement, ts: t.ts });
108
135
  if (!declared.includes(t.to)) undeclared.add(t.to);
136
+ if (opts?.controller !== undefined && t.from !== opts.controller && !declared.includes(t.from)) undeclared.add(t.from);
109
137
  if ((t.agreement.type === 'dpa' || t.agreement.type === 'scc') && !t.agreement.id) {
110
138
  missing.push({ to: t.to, ts: t.ts });
111
139
  }
@@ -115,6 +143,7 @@ export function subprocessorGraph(transfers: Transfer[], declared: string[]): Su
115
143
  edges: edges.sort((a, b) => Date.parse(a.ts) - Date.parse(b.ts)),
116
144
  undeclared: [...undeclared].sort(),
117
145
  missing_agreement_id: missing,
146
+ malformed,
118
147
  };
119
148
  }
120
149
 
@@ -125,12 +154,15 @@ export interface TransferCheck {
125
154
 
126
155
  /**
127
156
  * The compliance verdict for one data object's transfers: every receiver is declared, and
128
- * every dpa/scc transfer carries an agreement id. `ok` means the recorded onward chain is
129
- * clean; it does not assert no off-record transfer happened (see the limit).
157
+ * every dpa/scc transfer carries an agreement id. Pass `controller` to also require every
158
+ * SENDER to be the controller or declared (Art. 28 covers possession, not just receipt).
159
+ * `ok` means the recorded onward chain is clean; it does not assert no off-record transfer
160
+ * happened (see the limit). Never throws — a malformed transfer fails the verdict.
130
161
  */
131
- export function verifyTransfers(transfers: Transfer[], declared: string[]): TransferCheck {
132
- const g = subprocessorGraph(transfers, declared);
162
+ export function verifyTransfers(transfers: Transfer[], declared: string[], opts?: { controller?: string }): TransferCheck {
163
+ const g = subprocessorGraph(transfers, declared, opts);
133
164
  const reasons: string[] = [];
165
+ for (const b of g.malformed) reasons.push(`transfer ${b.index}: ${b.reason}`);
134
166
  for (const u of g.undeclared) reasons.push(`data reached '${u}', which is NOT a declared subprocessor`);
135
167
  for (const m of g.missing_agreement_id) reasons.push(`transfer to '${m.to}' at ${m.ts} has no agreement id (dpa/scc requires one)`);
136
168
  return { ok: reasons.length === 0, reasons };