Skip to content

normalize_geojson() unconditionally overwrites time/start/stale with a hard-coded 1-hour expiry #141

Description

@takwerx

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:

  1. 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;
}
  1. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions