@zanii/residency 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 +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -5
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +19 -5
package/dist/index.d.ts
CHANGED
|
@@ -61,6 +61,9 @@ export declare function verifyResidency(claim: ResidencyClaim, opts: {
|
|
|
61
61
|
* Across an ordered sequence of verified regions (each `{ ts, region }`, oldest first),
|
|
62
62
|
* find every point where the region left the declared allow-set. Each crossing names the
|
|
63
63
|
* ts and the region it moved to — the evidence for "when did it leave, and to where."
|
|
64
|
+
* Data is presumed to originate in-jurisdiction, so a sequence that STARTS outside the
|
|
65
|
+
* allow-set emits an `exit` at its first event — "no crossings" never masks "was never
|
|
66
|
+
* inside." Events with a missing/unparseable ts are ignored.
|
|
64
67
|
*/
|
|
65
68
|
export declare function detectCrossings(sequence: Array<{
|
|
66
69
|
ts: string;
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA6BA,6FAA6F;AAC7F,MAAM,WAAW,iBAAiB;IAChC,sEAAsE;IACtE,MAAM,EAAE,MAAM,CAAC;IACf,yFAAyF;IACzF,MAAM,EAAE,MAAM,CAAC;IACf,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,2FAA2F;AAC3F,MAAM,MAAM,cAAc,GAAG,CAAC,WAAW,EAAE,iBAAiB,KAAK,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC;AAEhK,oFAAoF;AACpF,eAAO,MAAM,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAIzD,CAAC;AAEF,MAAM,WAAW,cAAc;IAC7B,CAAC,EAAE,CAAC,CAAC;IACL,IAAI,EAAE,iBAAiB,CAAC;IACxB,kFAAkF;IAClF,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,wFAAwF;IACxF,YAAY,EAAE,MAAM,CAAC;IACrB,0EAA0E;IAC1E,kBAAkB,EAAE,iBAAiB,CAAC;IACtC,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,cAAc;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,iBAAiB,EAAE,iBAAiB,CAAC;IACrC,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA6BA,6FAA6F;AAC7F,MAAM,WAAW,iBAAiB;IAChC,sEAAsE;IACtE,MAAM,EAAE,MAAM,CAAC;IACf,yFAAyF;IACzF,MAAM,EAAE,MAAM,CAAC;IACf,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,2FAA2F;AAC3F,MAAM,MAAM,cAAc,GAAG,CAAC,WAAW,EAAE,iBAAiB,KAAK,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC;AAEhK,oFAAoF;AACpF,eAAO,MAAM,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAIzD,CAAC;AAEF,MAAM,WAAW,cAAc;IAC7B,CAAC,EAAE,CAAC,CAAC;IACL,IAAI,EAAE,iBAAiB,CAAC;IACxB,kFAAkF;IAClF,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,wFAAwF;IACxF,YAAY,EAAE,MAAM,CAAC;IACrB,0EAA0E;IAC1E,kBAAkB,EAAE,iBAAiB,CAAC;IACtC,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,cAAc;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,iBAAiB,EAAE,iBAAiB,CAAC;IACrC,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAMD,mGAAmG;AACnG,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,cAAc,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAa/G;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,CAEvD;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,OAAO,CAAC;IACZ,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,+EAA+E;IAC/E,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,kFAAkF;IAClF,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,KAAK,EAAE,cAAc,EACrB,IAAI,EAAE;IAAE,cAAc,EAAE,MAAM,EAAE,CAAC;IAAC,YAAY,EAAE,cAAc,CAAA;CAAE,GAC/D,OAAO,CAAC,cAAc,CAAC,CAiBzB;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,EAC/C,cAAc,EAAE,MAAM,EAAE,GACvB,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,GAAG,QAAQ,CAAA;CAAE,CAAC,CAcrE;AAED,wGAAwG;AACxG,wBAAgB,gBAAgB,CAAC,eAAe,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAEnF"}
|
package/dist/index.js
CHANGED
|
@@ -32,13 +32,15 @@ export const JURISDICTION_PRESETS = {
|
|
|
32
32
|
GCC: ['AE', 'SA', 'BH', 'KW', 'OM', 'QA', 'me-central-1', 'me-south-1'],
|
|
33
33
|
EU: ['AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR', 'DE', 'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT', 'NL', 'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE', 'eu-central-1', 'eu-west-1', 'eu-west-3', 'eu-north-1'],
|
|
34
34
|
};
|
|
35
|
+
// strict ISO check — Date.parse alone accepts 'Aug 20 2026', which Python's verifier rejects
|
|
36
|
+
const validTs = (ts) => typeof ts === 'string' && /^\d{4}-\d{2}-\d{2}T/.test(ts) && !Number.isNaN(Date.parse(ts));
|
|
35
37
|
/** Build a residency claim for a touch. Record it (or attach to a `@zanii/data-custody` touch). */
|
|
36
38
|
export function buildResidencyClaim(input) {
|
|
37
39
|
if (!input.jurisdiction)
|
|
38
40
|
throw new Error('jurisdiction is required');
|
|
39
41
|
if (!input.regionAttestation?.region)
|
|
40
42
|
throw new Error('regionAttestation.region is required');
|
|
41
|
-
if (
|
|
43
|
+
if (!validTs(input.ts))
|
|
42
44
|
throw new Error('ts must be an ISO timestamp');
|
|
43
45
|
const claim = {
|
|
44
46
|
v: 1,
|
|
@@ -64,7 +66,13 @@ export async function verifyResidency(claim, opts) {
|
|
|
64
66
|
if (claim?.v !== 1 || claim.type !== 'residency.claim') {
|
|
65
67
|
return { ok: false, reasons: ['not a residency claim (v1)'], region: null, crossed: false };
|
|
66
68
|
}
|
|
67
|
-
|
|
69
|
+
let verified;
|
|
70
|
+
try {
|
|
71
|
+
verified = await opts.verifyRegion(claim.region_attestation);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return { ok: false, reasons: ['the injected region verifier threw'], region: null, crossed: false };
|
|
75
|
+
}
|
|
68
76
|
if (!verified) {
|
|
69
77
|
return { ok: false, reasons: ['region signal failed the injected verifier'], region: null, crossed: false };
|
|
70
78
|
}
|
|
@@ -77,14 +85,19 @@ export async function verifyResidency(claim, opts) {
|
|
|
77
85
|
* Across an ordered sequence of verified regions (each `{ ts, region }`, oldest first),
|
|
78
86
|
* find every point where the region left the declared allow-set. Each crossing names the
|
|
79
87
|
* ts and the region it moved to — the evidence for "when did it leave, and to where."
|
|
88
|
+
* Data is presumed to originate in-jurisdiction, so a sequence that STARTS outside the
|
|
89
|
+
* allow-set emits an `exit` at its first event — "no crossings" never masks "was never
|
|
90
|
+
* inside." Events with a missing/unparseable ts are ignored.
|
|
80
91
|
*/
|
|
81
92
|
export function detectCrossings(sequence, allowedRegions) {
|
|
82
93
|
const crossings = [];
|
|
83
|
-
let inside =
|
|
84
|
-
const ordered =
|
|
94
|
+
let inside = true; // presumed in-jurisdiction until a signal says otherwise
|
|
95
|
+
const ordered = sequence
|
|
96
|
+
.filter((s) => validTs(s?.ts))
|
|
97
|
+
.sort((a, b) => Date.parse(a.ts) - Date.parse(b.ts));
|
|
85
98
|
for (const s of ordered) {
|
|
86
99
|
const now = allowedRegions.includes(s.region);
|
|
87
|
-
if (
|
|
100
|
+
if (now !== inside) {
|
|
88
101
|
crossings.push({ ts: s.ts, region: s.region, direction: now ? 'return' : 'exit' });
|
|
89
102
|
}
|
|
90
103
|
inside = now;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAenD,oFAAoF;AACpF,MAAM,CAAC,MAAM,oBAAoB,GAA6B;IAC5D,GAAG,EAAE,CAAC,IAAI,EAAE,cAAc,EAAE,YAAY,CAAC;IACzC,GAAG,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,cAAc,EAAE,YAAY,CAAC;IACvE,EAAE,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,cAAc,EAAE,WAAW,EAAE,WAAW,EAAE,YAAY,CAAC;CAC/N,CAAC;AAqBF,mGAAmG;AACnG,MAAM,UAAU,mBAAmB,CAAC,KAAqB;IACvD,IAAI,CAAC,KAAK,CAAC,YAAY;QAAE,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAC;IACrE,IAAI,CAAC,KAAK,CAAC,iBAAiB,EAAE,MAAM;QAAE,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;IAC9F,IAAI,
|
|
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/residency",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Prove where an agent's data physically stayed - bind a region attestation to each data touch, confirm the attested region is inside a declared jurisdiction, and emit a receipt the moment it crosses a border. Answers 'prove it never left the UAE / the EU.' The region signal is INJECTED (cloud-provider region / TEE geolocation) - we verify the binding, you supply the geo trust root; the proof is only as strong as that source.",
|
|
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
|
@@ -66,11 +66,15 @@ export interface ResidencyInput {
|
|
|
66
66
|
ref?: string;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
// strict ISO check — Date.parse alone accepts 'Aug 20 2026', which Python's verifier rejects
|
|
70
|
+
const validTs = (ts: unknown): boolean =>
|
|
71
|
+
typeof ts === 'string' && /^\d{4}-\d{2}-\d{2}T/.test(ts) && !Number.isNaN(Date.parse(ts));
|
|
72
|
+
|
|
69
73
|
/** Build a residency claim for a touch. Record it (or attach to a `@zanii/data-custody` touch). */
|
|
70
74
|
export function buildResidencyClaim(input: ResidencyInput): { target: string; payload: Record<string, unknown> } {
|
|
71
75
|
if (!input.jurisdiction) throw new Error('jurisdiction is required');
|
|
72
76
|
if (!input.regionAttestation?.region) throw new Error('regionAttestation.region is required');
|
|
73
|
-
if (
|
|
77
|
+
if (!validTs(input.ts)) throw new Error('ts must be an ISO timestamp');
|
|
74
78
|
const claim: ResidencyClaim = {
|
|
75
79
|
v: 1,
|
|
76
80
|
type: 'residency.claim',
|
|
@@ -108,7 +112,12 @@ export async function verifyResidency(
|
|
|
108
112
|
if (claim?.v !== 1 || claim.type !== 'residency.claim') {
|
|
109
113
|
return { ok: false, reasons: ['not a residency claim (v1)'], region: null, crossed: false };
|
|
110
114
|
}
|
|
111
|
-
|
|
115
|
+
let verified: { region: string; source: string } | null;
|
|
116
|
+
try {
|
|
117
|
+
verified = await opts.verifyRegion(claim.region_attestation);
|
|
118
|
+
} catch {
|
|
119
|
+
return { ok: false, reasons: ['the injected region verifier threw'], region: null, crossed: false };
|
|
120
|
+
}
|
|
112
121
|
if (!verified) {
|
|
113
122
|
return { ok: false, reasons: ['region signal failed the injected verifier'], region: null, crossed: false };
|
|
114
123
|
}
|
|
@@ -121,17 +130,22 @@ export async function verifyResidency(
|
|
|
121
130
|
* Across an ordered sequence of verified regions (each `{ ts, region }`, oldest first),
|
|
122
131
|
* find every point where the region left the declared allow-set. Each crossing names the
|
|
123
132
|
* ts and the region it moved to — the evidence for "when did it leave, and to where."
|
|
133
|
+
* Data is presumed to originate in-jurisdiction, so a sequence that STARTS outside the
|
|
134
|
+
* allow-set emits an `exit` at its first event — "no crossings" never masks "was never
|
|
135
|
+
* inside." Events with a missing/unparseable ts are ignored.
|
|
124
136
|
*/
|
|
125
137
|
export function detectCrossings(
|
|
126
138
|
sequence: Array<{ ts: string; region: string }>,
|
|
127
139
|
allowedRegions: string[],
|
|
128
140
|
): Array<{ ts: string; region: string; direction: 'exit' | 'return' }> {
|
|
129
141
|
const crossings: Array<{ ts: string; region: string; direction: 'exit' | 'return' }> = [];
|
|
130
|
-
let inside
|
|
131
|
-
const ordered =
|
|
142
|
+
let inside = true; // presumed in-jurisdiction until a signal says otherwise
|
|
143
|
+
const ordered = sequence
|
|
144
|
+
.filter((s) => validTs(s?.ts))
|
|
145
|
+
.sort((a, b) => Date.parse(a.ts) - Date.parse(b.ts));
|
|
132
146
|
for (const s of ordered) {
|
|
133
147
|
const now = allowedRegions.includes(s.region);
|
|
134
|
-
if (
|
|
148
|
+
if (now !== inside) {
|
|
135
149
|
crossings.push({ ts: s.ts, region: s.region, direction: now ? 'return' : 'exit' });
|
|
136
150
|
}
|
|
137
151
|
inside = now;
|