fix: allocate share-link layer tokens without collisions

This commit is contained in:
manjunath22466 committed 2026-09-24 16:25:55 +05:30
1 parent ce671ce500
commit 54735a9e6f
15 files changed
+1295 -20

No files matched your search

+206 -14
View File
@@ -1,3 +1,5 @@
import reservationRows from './layerStateTokenReservations.json' with { type: 'json' };
const VALID_DISPOSITIONS = new Set([
'enabled-only',
'enabled+options',
@@ -5,6 +7,10 @@ const VALID_DISPOSITIONS = new Set([
]);
export const LAYER_STATE_VERSION = 2;
export const LAYER_STATE_TOKEN_ALPHABET =
'0123456789abcdefghijklmnopqrstuvwxyz';
const LAYER_STATE_TOKEN_PATTERN = /^[a-z0-9]{1,2}$/;
const NEW_SINGLE_CHARACTER_TOKEN_PATTERN = /^[0-9]$/;
/** Re-check cadence while a shared subject waits for its feed row to arrive. */
const PENDING_TRACKING_POLL_MS = 1_000;
/**
@@ -17,12 +23,13 @@ const PENDING_TRACKING_POLL_MS = 1_000;
*/
const TRACKING_ID_GRAMMAR = /^[0-9a-z~_-]{1,16}$/;
/**
* Ceilings for the untrusted v2 layer fields. Both are far above any legitimate
* payload (16 one-character tokens; a dozen short option assignments), so a
* value past them is malformed or hostile. Reject the WHOLE payload, matching
* the unknown-token rule — never salvage a prefix.
* Ceilings for the untrusted v2 layer fields. The layer ceiling covers the
* complete reserved one-character space plus every two-character base-36
* allocation, while the option ceiling covers a dozen short assignments.
* Reject the WHOLE payload, matching the unknown-token rule — never salvage a
* prefix.
*/
const MAX_ENABLED_LAYERS_CHARS = 64;
const MAX_ENABLED_LAYERS_CHARS = 4_096;
const MAX_LAYER_OPTIONS_CHARS = 512;
export const LAYER_STATE_STORAGE_KEY = 'gev:layer-state:v2';
export const LAYER_RESTORE_ORIGINS = Object.freeze({
@@ -446,6 +453,94 @@ export const SHARE_TRACKING_RESTORE_POLICIES = Object.freeze({
}),
});
/**
* The original single-character assignments are a closed compatibility set.
* Keep their exact mapping pinned by the independent snapshot in the tests.
*/
export const LEGACY_LAYER_STATE_TOKENS = Object.freeze({
'ais-live-vessels': 'a',
'alpr-cameras': 'p',
'bhote-koshi-2026': 'h',
'bhote-koshi-locator': 'z',
bikeshare: 'b',
cctv: 'c',
directions: 'n',
earthquakes: 'e',
'fire-perimeters': '2',
flights: 'f',
'local-dams': 'q',
'local-datacenters': 'd',
'local-firms': 'w',
military: 'm',
'military-awareness': 'g',
'military-installations': 'i',
radio: 'r',
'recent-imagery': '1',
'rocket-launches': 'x',
satellites: 's',
'telegeography-submarine-cables': 'u',
traffic: 't',
transit: 'j',
'weather-cyclones': 'y',
'weather-lightning': 'l',
'weather-radar': 'v',
'weather-satellite': 'o',
wind: 'k',
});
/**
* Permanent token ownership. Existing share links are public authored state,
* so an allocation stays here even if its layer is later removed. The JSON
* ledger is also read directly from the published Git base by the checker.
*/
export function parseLayerStateTokenReservations(rows) {
if (!Array.isArray(rows)) {
throw new Error('Layer-state token ledger must be an array');
}
const reservations = Object.create(null);
const reservedIdsByToken = new Map();
for (const row of rows) {
if (
!Array.isArray(row) ||
row.length !== 2 ||
typeof row[0] !== 'string' ||
typeof row[1] !== 'string'
) {
throw new Error('Invalid layer-state token ledger row');
}
const [id, token] = row;
if (!/^[a-z0-9-]+$/.test(id) || !LAYER_STATE_TOKEN_PATTERN.test(token)) {
throw new Error(`Invalid layer-state token reservation: ${id}`);
}
if (Object.hasOwn(reservations, id)) {
throw new Error(`Duplicate layer-state token reservation id: ${id}`);
}
if (reservedIdsByToken.has(token)) {
throw new Error(`Duplicate layer-state token reservation: ${token}`);
}
if (
(Object.hasOwn(LEGACY_LAYER_STATE_TOKENS, id) &&
token !== LEGACY_LAYER_STATE_TOKENS[id]) ||
(token.length === 1 &&
!NEW_SINGLE_CHARACTER_TOKEN_PATTERN.test(token) &&
LEGACY_LAYER_STATE_TOKENS[id] !== token)
) {
throw new Error(`Legacy layer-state token is immutable: ${id}`);
}
reservations[id] = token;
reservedIdsByToken.set(token, id);
}
for (const [id, token] of Object.entries(LEGACY_LAYER_STATE_TOKENS)) {
if (reservations[id] !== token) {
throw new Error(`Missing or changed legacy layer-state token: ${id}`);
}
}
return Object.freeze(reservations);
}
export const LAYER_STATE_TOKEN_RESERVATIONS =
parseLayerStateTokenReservations(reservationRows);
/**
* Canonical serialization registry. Its order, not runtime registration order,
* owns stable URL ordering.
@@ -579,6 +674,60 @@ export const REGISTERED_LAYER_IDS = Object.freeze(
LAYER_STATE_REGISTRY.map((entry) => entry.id),
);
/** Consume unreserved digits before the two-character base-36 namespace. */
export function nextLayerStateToken(
reservations = LAYER_STATE_TOKEN_RESERVATIONS,
) {
const occupied = new Set(Object.values(reservations || {}));
for (const digit of '0123456789') {
if (!occupied.has(digit)) return digit;
}
for (const first of LAYER_STATE_TOKEN_ALPHABET) {
for (const second of LAYER_STATE_TOKEN_ALPHABET) {
const candidate = `${first}${second}`;
if (!occupied.has(candidate)) return candidate;
}
}
throw new Error('Layer-state token namespace exhausted');
}
function allocationRank(token) {
if (NEW_SINGLE_CHARACTER_TOKEN_PATTERN.test(token)) return Number(token);
if (typeof token !== 'string' || token.length !== 2) return Infinity;
const first = LAYER_STATE_TOKEN_ALPHABET.indexOf(token[0]);
const second = LAYER_STATE_TOKEN_ALPHABET.indexOf(token[1]);
return first < 0 || second < 0 ? Infinity : 10 + first * 36 + second;
}
/** Check a proposed ledger against the published merge-time base. */
export function validateLayerStateAllocations(
baseReservations,
reservations = LAYER_STATE_TOKEN_RESERVATIONS,
) {
if (!baseReservations || typeof baseReservations !== 'object') {
throw new Error('Base layer-state token reservations are required');
}
for (const [id, token] of Object.entries(baseReservations)) {
if (reservations[id] !== token) {
throw new Error(`Published layer-state token changed or removed: ${id}`);
}
}
const newlyReserved = Object.entries(reservations)
.filter(([id]) => !Object.hasOwn(baseReservations, id))
.sort((left, right) => allocationRank(left[1]) - allocationRank(right[1]));
const occupied = { ...baseReservations };
for (const [id, token] of newlyReserved) {
const expected = nextLayerStateToken(occupied);
if (token !== expected) {
throw new Error(
`Layer-state token for ${id} must be the next free token ${expected}`,
);
}
occupied[id] = token;
}
return true;
}
const REGISTRY_BY_ID = new Map(
LAYER_STATE_REGISTRY.map((entry) => [entry.id, entry]),
);
@@ -619,10 +768,39 @@ export function isExplicitLayerStateOrigin(origin) {
}
/** Validate the static registry itself before it is used to seal a manager. */
export function validateLayerStateRegistry(registry = LAYER_STATE_REGISTRY) {
export function validateLayerStateRegistry(
registry = LAYER_STATE_REGISTRY,
reservations = LAYER_STATE_TOKEN_RESERVATIONS,
) {
if (!Array.isArray(registry) || registry.length === 0) {
throw new Error('Layer-state registry must be a non-empty array');
}
if (
!reservations ||
typeof reservations !== 'object' ||
Array.isArray(reservations)
) {
throw new Error('Layer-state token reservations must be an object');
}
const reservedIdsByToken = new Map();
for (const [id, token] of Object.entries(reservations)) {
if (!/^[a-z0-9-]+$/.test(id) || !LAYER_STATE_TOKEN_PATTERN.test(token)) {
throw new Error(`Invalid layer-state token reservation: ${id}`);
}
if (
(Object.hasOwn(LEGACY_LAYER_STATE_TOKENS, id) &&
token !== LEGACY_LAYER_STATE_TOKENS[id]) ||
(token.length === 1 &&
!NEW_SINGLE_CHARACTER_TOKEN_PATTERN.test(token) &&
LEGACY_LAYER_STATE_TOKENS[id] !== token)
) {
throw new Error(`Legacy layer-state token is immutable: ${id}`);
}
if (reservedIdsByToken.has(token)) {
throw new Error(`Duplicate layer-state token reservation: ${token}`);
}
reservedIdsByToken.set(token, id);
}
const ids = new Set();
const tokens = new Set();
for (const entry of registry) {
@@ -633,8 +811,14 @@ export function validateLayerStateRegistry(registry = LAYER_STATE_REGISTRY) {
if (ids.has(entry.id))
throw new Error(`Duplicate layer-state id: ${entry.id}`);
ids.add(entry.id);
if (!/^[a-z0-9]$/.test(entry.token || ''))
if (!LAYER_STATE_TOKEN_PATTERN.test(entry.token || ''))
throw new Error(`Invalid layer-state token: ${entry.id}`);
if (reservations[entry.id] !== entry.token) {
throw new Error(`Unreserved layer-state token: ${entry.id}`);
}
if (reservedIdsByToken.get(entry.token) !== entry.id) {
throw new Error(`Layer-state token reservation mismatch: ${entry.token}`);
}
if (tokens.has(entry.token))
throw new Error(`Duplicate layer-state token: ${entry.token}`);
tokens.add(entry.token);
@@ -756,18 +940,26 @@ export function encodeLayerStateParams(params, state) {
/** Decode v2 fields. Null means that the layer payload is absent. */
export function decodeLayerStateParams(params) {
if (params.get('v') !== String(LAYER_STATE_VERSION) || !params.has('l'))
const layerFields = params.getAll('l');
if (
params.get('v') !== String(LAYER_STATE_VERSION) ||
layerFields.length !== 1
)
return null;
const rawLayers = String(params.get('l') || '');
const rawLayers = layerFields[0];
const rawOptionsField = String(params.get('lo') || '');
// Fail closed on an oversized payload rather than decoding a truncated one.
if (rawLayers.length > MAX_ENABLED_LAYERS_CHARS) return null;
if (rawOptionsField.length > MAX_LAYER_OPTIONS_CHARS) return null;
const layerTokens = rawLayers.split('.').filter(Boolean);
// `l=` is the one valid explicit-empty representation. Any non-empty token
// set containing an unknown member rejects the complete layer payload so a
// typo or future token cannot silently become an authoritative empty set.
if (layerTokens.some((token) => !REGISTRY_BY_TOKEN.has(token))) return null;
const layerTokens = rawLayers ? rawLayers.split('.') : [];
// `l=` is the one valid explicit-empty representation. Reject repeated
// fields, empty members, duplicate members, or unknown members; silently
// removing one would turn a malformed share into a different state.
if (
layerTokens.some((token) => !token || !REGISTRY_BY_TOKEN.has(token)) ||
new Set(layerTokens).size !== layerTokens.length
)
return null;
const enabledLayerIds = layerTokens.map(
(token) => REGISTRY_BY_TOKEN.get(token).id,
);