Summary
normalize_geojson() unconditionally overwrites time, start and stale on the feature it is given, forcing a hard-coded 1 hour expiry. There is no guard on an existing value and no options parameter, so a caller cannot supply its own stale time — anything passed in is silently discarded.
For features that are authored (a user-placed marker, an incident, an operational graphic) rather than imported on a refresh cycle, this means the feature expires 60 minutes after it is created and disappears from ATAK, iTAK, TAK Aware and CloudTAK — including from Data Sync mission listings, even though the mission still holds the UID server-side.
Where
lib/parser/normalize_geojson.ts, lines 88–93 (v14.52.0):
feature.properties.time = new Date().toISOString();
feature.properties.start = new Date().toISOString();
const stale = new Date();
stale.setHours(stale.getHours() + 1);
feature.properties.stale = stale;
No if (!feature.properties.stale) guard, unlike the equivalent logic in CloudTAK's own COT.styleProperties(), which does guard:
if (!properties.stale) {
const currentTime = new Date();
currentTime.setMinutes(currentTime.getMinutes() + 10);
properties.stale = currentTime.toISOString();
}
The exported signature takes no options:
export declare function normalize_geojson(
feature: Static<typeof GeoJSONFeature>
): Promise<Static<typeof Feature>>;
Affected versions
Present in every 14.x I checked — pulled from npm and grepped:
| version |
line |
| 14.0.0 |
stale.setHours(stale.getHours() + 1) |
| 14.24.3 |
stale.setHours(stale.getHours() + 1) |
| 14.40.0 |
stale.setHours(stale.getHours() + 1) |
| 14.52.0 |
stale.setHours(stale.getHours() + 1) |
Observed in production
A marker authored through this path and published into a Data Sync mission, as stored by TAK Server 5.x:
uid fb6ea9d0-549d-467a-877d-d06f9b8f9d44
cot_type a-n-G
how h-g-i-g-o
servertime 2026-08-26 20:36:36-04
stale 2026-08-26 21:36:36-04 <- exactly +1h
Exactly one hour, and not a value the caller chose. The wire CoT carried <archive/> and a valid <marti><dest mission-guid="..."/></marti>; TAK Server accepted it, added the UID to mission_uid, and recorded the mission change. The server continues to serve the event from /Marti/api/missions/{name}/cot after expiry — so the content is intact and it is purely the stale attribute that takes the feature out of every client.
Why this is a problem
stale is a liveness assertion — "this information is no longer valid after T". That is appropriate for a position report or a polled data feed that republishes on a cycle, which is presumably what this default was written for. It is not appropriate for a persisted, user-authored feature: a marker placed on a map is expected to remain until it is moved or deleted, and <archive/> is already being set on the same feature two lines later, which is a direct contradiction — the feature is simultaneously marked "persist me" and "expire me in an hour".
Because the value is forced rather than defaulted, downstream consumers have no way to express a longer-lived feature short of mutating properties.stale after the call and hoping nothing else re-normalizes it.
Suggested fix
Either of:
- Guard the assignment so a caller-supplied value wins, matching
styleProperties():
if (!feature.properties.stale) {
const stale = new Date();
stale.setHours(stale.getHours() + 1);
feature.properties.stale = stale;
}
- Add an options parameter, e.g.
normalize_geojson(feature, { stale }), so the expiry is explicit at the call site.
(1) alone would resolve it and is backward compatible for callers that pass no stale. The same guard is arguably wanted on time/start, which are also overwritten unconditionally.
Environment
@tak-ps/node-cot 14.52.0
- CloudTAK 13.69.0
- TAK Server 5.x, missions via
<marti><dest mission-guid="…"/></marti> on :8089
Summary
normalize_geojson()unconditionally overwritestime,startandstaleon the feature it is given, forcing a hard-coded 1 hour expiry. There is no guard on an existing value and no options parameter, so a caller cannot supply its own stale time — anything passed in is silently discarded.For features that are authored (a user-placed marker, an incident, an operational graphic) rather than imported on a refresh cycle, this means the feature expires 60 minutes after it is created and disappears from ATAK, iTAK, TAK Aware and CloudTAK — including from Data Sync mission listings, even though the mission still holds the UID server-side.
Where
lib/parser/normalize_geojson.ts, lines 88–93 (v14.52.0):No
if (!feature.properties.stale)guard, unlike the equivalent logic in CloudTAK's ownCOT.styleProperties(), which does guard:The exported signature takes no options:
Affected versions
Present in every 14.x I checked — pulled from npm and grepped:
stale.setHours(stale.getHours() + 1)stale.setHours(stale.getHours() + 1)stale.setHours(stale.getHours() + 1)stale.setHours(stale.getHours() + 1)Observed in production
A marker authored through this path and published into a Data Sync mission, as stored by TAK Server 5.x:
Exactly one hour, and not a value the caller chose. The wire CoT carried
<archive/>and a valid<marti><dest mission-guid="..."/></marti>; TAK Server accepted it, added the UID tomission_uid, and recorded the mission change. The server continues to serve the event from/Marti/api/missions/{name}/cotafter expiry — so the content is intact and it is purely thestaleattribute that takes the feature out of every client.Why this is a problem
staleis a liveness assertion — "this information is no longer valid after T". That is appropriate for a position report or a polled data feed that republishes on a cycle, which is presumably what this default was written for. It is not appropriate for a persisted, user-authored feature: a marker placed on a map is expected to remain until it is moved or deleted, and<archive/>is already being set on the same feature two lines later, which is a direct contradiction — the feature is simultaneously marked "persist me" and "expire me in an hour".Because the value is forced rather than defaulted, downstream consumers have no way to express a longer-lived feature short of mutating
properties.staleafter the call and hoping nothing else re-normalizes it.Suggested fix
Either of:
styleProperties():normalize_geojson(feature, { stale }), so the expiry is explicit at the call site.(1) alone would resolve it and is backward compatible for callers that pass no stale. The same guard is arguably wanted on
time/start, which are also overwritten unconditionally.Environment
@tak-ps/node-cot14.52.0<marti><dest mission-guid="…"/></marti>on :8089