@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 +19 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +42 -9
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +41 -9
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
|
-
*
|
|
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[]
|
|
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. `
|
|
70
|
-
*
|
|
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[]
|
|
84
|
+
export declare function verifyTransfers(transfers: Transfer[], declared: string[], opts?: {
|
|
85
|
+
controller?: string;
|
|
86
|
+
}): TransferCheck;
|
|
73
87
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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 (
|
|
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
|
-
*
|
|
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
|
-
|
|
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. `
|
|
83
|
-
*
|
|
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;
|
|
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
|
|
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.
|
|
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 (
|
|
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
|
-
*
|
|
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
|
-
|
|
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. `
|
|
129
|
-
*
|
|
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 };
|