@peopl-health/nexus 5.44.0-dev.6210 → 5.44.0-dev.6227
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.
|
@@ -9,7 +9,8 @@ const PHI_CATEGORIES = Object.freeze(['PERSONA', 'TELEFONO', 'EMAIL', 'ID']);
|
|
|
9
9
|
const tokenMapSchema = new mongoose.Schema({
|
|
10
10
|
code: { type: String, required: true },
|
|
11
11
|
encode: { type: Map, of: String, default: () => new Map() },
|
|
12
|
-
decode: { type: Map, of: String, default: () => new Map() }
|
|
12
|
+
decode: { type: Map, of: String, default: () => new Map() },
|
|
13
|
+
seq: { type: Number }
|
|
13
14
|
}, { timestamps: true });
|
|
14
15
|
|
|
15
16
|
tokenMapSchema.index({ code: 1 }, { unique: true });
|
|
@@ -24,12 +24,23 @@ const preferredSurface = (current, candidate) => {
|
|
|
24
24
|
return richer > 0 ? candidate : current;
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
-
const
|
|
27
|
+
const CLAIM_ATTEMPTS = 3;
|
|
28
|
+
|
|
29
|
+
const NEXT_INDEX_PIPELINE = [{
|
|
30
|
+
$set: {
|
|
31
|
+
seq: {
|
|
32
|
+
$add: [
|
|
33
|
+
{ $ifNull: ['$seq', { $size: { $objectToArray: { $ifNull: ['$encode', {}] } } }] },
|
|
34
|
+
1,
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
}];
|
|
28
39
|
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
if (cached) return cached;
|
|
40
|
+
const tokenCache = new MapCache({ maxSize: 500, ttl: 60 * 1000 });
|
|
41
|
+
const inFlight = new Map();
|
|
32
42
|
|
|
43
|
+
const readSubmap = async (code) => {
|
|
33
44
|
const doc = await getTokenMap().findOne({ code }).lean();
|
|
34
45
|
const submap = {
|
|
35
46
|
encode: new Map(Object.entries(doc?.encode || {})),
|
|
@@ -39,32 +50,34 @@ const getSubmap = async (code) => {
|
|
|
39
50
|
return submap;
|
|
40
51
|
};
|
|
41
52
|
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
|
|
53
|
+
const getSubmap = async (code) => {
|
|
54
|
+
const cached = tokenCache.get(code);
|
|
55
|
+
if (cached) return cached;
|
|
56
|
+
if (!inFlight.has(code)) {
|
|
57
|
+
inFlight.set(code, readSubmap(code).finally(() => inFlight.delete(code)));
|
|
58
|
+
}
|
|
59
|
+
return inFlight.get(code);
|
|
60
|
+
};
|
|
45
61
|
|
|
46
|
-
|
|
47
|
-
|
|
62
|
+
const nextIndex = async (code) => {
|
|
63
|
+
const doc = await getTokenMap().findOneAndUpdate({ code }, NEXT_INDEX_PIPELINE, {
|
|
64
|
+
upsert: true, new: true, projection: { seq: 1 }, lean: true,
|
|
65
|
+
});
|
|
66
|
+
return doc.seq;
|
|
67
|
+
};
|
|
48
68
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
);
|
|
59
|
-
if (!res || (res.modifiedCount === 0 && !res.upsertedCount)) {
|
|
60
|
-
logger.error('[allocateToken] Mapping write modified 0 docs', { code, category, token });
|
|
61
|
-
throw new Error(`[allocateToken] Mapping write modified 0 docs for token ${token}`);
|
|
62
|
-
}
|
|
63
|
-
return token;
|
|
69
|
+
const rememberSurface = (code, token, plaintext) => {
|
|
70
|
+
const live = tokenCache.get(code);
|
|
71
|
+
if (live && plaintext !== undefined) live.decode.set(token, plaintext);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const rememberMapping = (code, normalized, token, plaintext) => {
|
|
75
|
+
const live = tokenCache.get(code);
|
|
76
|
+
if (live) live.encode.set(normalized, token);
|
|
77
|
+
rememberSurface(code, token, plaintext);
|
|
64
78
|
};
|
|
65
79
|
|
|
66
|
-
const upgradeSurface = async (code, token,
|
|
67
|
-
const current = submap.decode.get(token);
|
|
80
|
+
const upgradeSurface = async (code, token, current, plaintext) => {
|
|
68
81
|
const preferred = preferredSurface(current, plaintext);
|
|
69
82
|
if (preferred === current) return;
|
|
70
83
|
|
|
@@ -75,13 +88,55 @@ const upgradeSurface = async (code, token, plaintext, submap) => {
|
|
|
75
88
|
);
|
|
76
89
|
|
|
77
90
|
if (res?.modifiedCount) {
|
|
78
|
-
|
|
91
|
+
rememberSurface(code, token, preferred);
|
|
79
92
|
logger.debug('[upgradeSurface] Decode surface upgraded', { code, token });
|
|
80
93
|
return;
|
|
81
94
|
}
|
|
82
95
|
tokenCache.delete(code);
|
|
83
96
|
};
|
|
84
97
|
|
|
98
|
+
const claimToken = async (code, plaintext, category, normalized) => {
|
|
99
|
+
const token = `<<${category}_${await nextIndex(code)}>>`;
|
|
100
|
+
const res = await getTokenMap().updateOne(
|
|
101
|
+
{
|
|
102
|
+
code,
|
|
103
|
+
[`encode.${normalized}`]: { $exists: false },
|
|
104
|
+
[`decode.${token}`]: { $exists: false },
|
|
105
|
+
},
|
|
106
|
+
{ $set: { [`encode.${normalized}`]: token, [`decode.${token}`]: plaintext } },
|
|
107
|
+
);
|
|
108
|
+
return res?.modifiedCount ? token : null;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const adoptClaimedToken = async (code, plaintext, normalized) => {
|
|
112
|
+
const doc = await getTokenMap().findOne({ code }).lean();
|
|
113
|
+
const winner = doc?.encode?.[normalized];
|
|
114
|
+
if (!winner) return null;
|
|
115
|
+
|
|
116
|
+
rememberMapping(code, normalized, winner, doc.decode?.[winner]);
|
|
117
|
+
await upgradeSurface(code, winner, doc.decode?.[winner], plaintext);
|
|
118
|
+
return winner;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const allocateToken = async (code, plaintext, category, normalized) => {
|
|
122
|
+
for (let attempt = 1; attempt <= CLAIM_ATTEMPTS; attempt += 1) {
|
|
123
|
+
let token = null;
|
|
124
|
+
try {
|
|
125
|
+
token = await claimToken(code, plaintext, category, normalized);
|
|
126
|
+
} catch (err) {
|
|
127
|
+
if (err?.code !== 11000 || attempt === CLAIM_ATTEMPTS) throw err;
|
|
128
|
+
}
|
|
129
|
+
if (token) {
|
|
130
|
+
rememberMapping(code, normalized, token, plaintext);
|
|
131
|
+
return token;
|
|
132
|
+
}
|
|
133
|
+
const winner = await adoptClaimedToken(code, plaintext, normalized);
|
|
134
|
+
if (winner) return winner;
|
|
135
|
+
logger.error('[allocateToken] Token claim failed, retrying', { code, category, attempt });
|
|
136
|
+
}
|
|
137
|
+
throw new Error(`[allocateToken] Could not claim a ${category} token for ${code}`);
|
|
138
|
+
};
|
|
139
|
+
|
|
85
140
|
const encode = async (code, plaintext, category) => {
|
|
86
141
|
if (!PHI_CATEGORIES.includes(category)) {
|
|
87
142
|
throw new Error(`[encode] Unknown category: ${category}`);
|
|
@@ -96,11 +151,11 @@ const encode = async (code, plaintext, category) => {
|
|
|
96
151
|
const submap = await getSubmap(code);
|
|
97
152
|
const existing = submap.encode.get(normalized);
|
|
98
153
|
if (existing) {
|
|
99
|
-
await upgradeSurface(code, existing,
|
|
154
|
+
await upgradeSurface(code, existing, submap.decode.get(existing), String(plaintext));
|
|
100
155
|
return existing;
|
|
101
156
|
}
|
|
102
157
|
|
|
103
|
-
const token = await allocateToken(code, String(plaintext), category, normalized
|
|
158
|
+
const token = await allocateToken(code, String(plaintext), category, normalized);
|
|
104
159
|
logger.debug('[encode] Allocated', { code, category, token });
|
|
105
160
|
return token;
|
|
106
161
|
};
|