@teamscala/service-boot 0.23.463 → 0.23.465
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/package.json
CHANGED
package/src/proposal-gate.ts
CHANGED
|
@@ -178,6 +178,19 @@ export const proposalGateConfig: ContentGateConfig = {
|
|
|
178
178
|
headerData.row2Value = "";
|
|
179
179
|
headerData.row3Value = "";
|
|
180
180
|
}
|
|
181
|
+
// Neutralize the CRM contact record too. The v8 adapter ignores select
|
|
182
|
+
// (the full record returns), so without this the gate floor's hydration
|
|
183
|
+
// payload ships the recipient's whole contact row — first/last/full
|
|
184
|
+
// name, emails, phones, CRM ids — to every unauthorised visitor. The
|
|
185
|
+
// company fields stay: the header's Organisation row shows the company,
|
|
186
|
+
// which is brand chrome, not recipient identity.
|
|
187
|
+
const contact = resolved.contact as Record<string, unknown> | undefined;
|
|
188
|
+
if (contact) {
|
|
189
|
+
const keep = new Set(["id", "company_id", "company_name", "company_website"]);
|
|
190
|
+
for (const key of Object.keys(contact)) {
|
|
191
|
+
if (!keep.has(key)) contact[key] = "";
|
|
192
|
+
}
|
|
193
|
+
}
|
|
181
194
|
},
|
|
182
195
|
};
|
|
183
196
|
|
|
@@ -41,6 +41,48 @@ export interface ResolveRequestGraphArgs {
|
|
|
41
41
|
configFingerprint?: string;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Clone a loader graph for the cache, degrading loudly rather than silently.
|
|
46
|
+
*
|
|
47
|
+
* ONE CLONE, TWO CALLERS. The cold path and the background-refresh path both
|
|
48
|
+
* store a graph and both must survive a non-cloneable value in it — a function,
|
|
49
|
+
* a Proxy, a live client handle. They previously had SEPARATE implementations:
|
|
50
|
+
* the cold path carried this fallback ladder, the refresh path called
|
|
51
|
+
* structuredClone bare. So a graph that the cold path stored happily threw on
|
|
52
|
+
* every subsequent refresh, the cache never updated, and the entry aged out into
|
|
53
|
+
* a full in-request resolve. Two copies of one decision, and the copy nobody
|
|
54
|
+
* hardened is the one that fired.
|
|
55
|
+
*
|
|
56
|
+
* Returns undefined when the graph cannot be cloned at all; the caller then
|
|
57
|
+
* declines to cache that key. Both fallbacks log, so the offending source stays
|
|
58
|
+
* observable and fixable — never a silent fail-open
|
|
59
|
+
* (`protective-layers-fail-explicitly`).
|
|
60
|
+
*/
|
|
61
|
+
function cloneForCache(
|
|
62
|
+
graph: unknown,
|
|
63
|
+
graphKey: string,
|
|
64
|
+
serviceSlug: string,
|
|
65
|
+
): unknown | undefined {
|
|
66
|
+
const log = getAppLogger();
|
|
67
|
+
try {
|
|
68
|
+
return structuredClone(graph);
|
|
69
|
+
} catch (err) {
|
|
70
|
+
log.warn(
|
|
71
|
+
`[scala-install:${serviceSlug}] graph-cache structuredClone failed for ${graphKey} — falling back to JSON-clone (non-cloneable value in loader graph): ${err instanceof Error ? err.message : err}`,
|
|
72
|
+
);
|
|
73
|
+
try {
|
|
74
|
+
// JSON-clone preserves the data graph, dropping the functions and
|
|
75
|
+
// undefined a loader graph should not be carrying anyway.
|
|
76
|
+
return JSON.parse(JSON.stringify(graph)) as unknown;
|
|
77
|
+
} catch (err2) {
|
|
78
|
+
log.warn(
|
|
79
|
+
`[scala-install:${serviceSlug}] graph-cache JSON-clone also failed for ${graphKey} — cache disabled for this key (observable): ${err2 instanceof Error ? err2.message : err2}`,
|
|
80
|
+
);
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
44
86
|
export async function resolveRequestGraph(
|
|
45
87
|
args: ResolveRequestGraphArgs,
|
|
46
88
|
): Promise<Record<string, unknown>> {
|
|
@@ -102,7 +144,23 @@ export async function resolveRequestGraph(
|
|
|
102
144
|
`primary resource "${primaryType}" missing — skip cache update for ${graphKey}`,
|
|
103
145
|
);
|
|
104
146
|
}
|
|
105
|
-
|
|
147
|
+
// SAME DEFENSIVE CLONE AS THE COLD PATH. This used to call
|
|
148
|
+
// structuredClone bare, so a non-cloneable value in the loader
|
|
149
|
+
// graph threw on EVERY background refresh: refreshInBackground
|
|
150
|
+
// caught it, kept the stale entry, and logged one line — so the
|
|
151
|
+
// cache could never update, expired at staleTtlMs, and the next
|
|
152
|
+
// request re-resolved the whole graph in-request. Measured
|
|
153
|
+
// 2026-09-03 on the live proposal page: "The object can not be
|
|
154
|
+
// cloned" every ~13 minutes on the same graphKey, indefinitely.
|
|
155
|
+
// The cold path was hardened for exactly this and the refresh
|
|
156
|
+
// path was not — one defect, two clone sites, one protected.
|
|
157
|
+
const refreshed = cloneForCache(freshGraph, graphKey, serviceSlug);
|
|
158
|
+
if (refreshed === undefined) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
`graph for ${graphKey} is not cloneable — skip cache update`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
return refreshed;
|
|
106
164
|
},
|
|
107
165
|
),
|
|
108
166
|
);
|
|
@@ -125,22 +183,7 @@ export async function resolveRequestGraph(
|
|
|
125
183
|
// graph shouldn't carry) so the cache stays effective; both fallbacks
|
|
126
184
|
// are LOGGED so the offending non-cloneable source is observable +
|
|
127
185
|
// fixable, never silently disabling the cache again.
|
|
128
|
-
|
|
129
|
-
try {
|
|
130
|
-
cloned = structuredClone(resolved);
|
|
131
|
-
} catch (err) {
|
|
132
|
-
log.warn(
|
|
133
|
-
`[scala-install:${serviceSlug}] graph-cache structuredClone failed for ${graphKey} — falling back to JSON-clone (non-cloneable value in loader graph): ${err instanceof Error ? err.message : err}`,
|
|
134
|
-
);
|
|
135
|
-
try {
|
|
136
|
-
cloned = JSON.parse(JSON.stringify(resolved));
|
|
137
|
-
} catch (err2) {
|
|
138
|
-
log.warn(
|
|
139
|
-
`[scala-install:${serviceSlug}] graph-cache JSON-clone also failed for ${graphKey} — cache disabled for this key (observable): ${err2 instanceof Error ? err2.message : err2}`,
|
|
140
|
-
);
|
|
141
|
-
cloned = undefined;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
186
|
+
const cloned = cloneForCache(resolved, graphKey, serviceSlug);
|
|
144
187
|
if (cloned !== undefined) {
|
|
145
188
|
graphCache.set(graphKey, cloned);
|
|
146
189
|
}
|