@expofp/config
Advanced tools
+1
-1
| export { applyIntents } from './lib/apply-intents.js'; | ||
| export { getConfig, setConfig } from './lib/config-store.js'; | ||
| export { type DebugSettingDescriptor, toDebugSettings } from './lib/debug-settings.js'; | ||
| export { type LegacyOneShot, type LegacyPrimary, type LegacyUrlQuery, parseLegacyQuery, serializeSelection, type UrlSelection, } from './lib/legacy-url.js'; | ||
| export { type LegacyOneShot, type LegacyPrimary, type LegacyUrlQuery, parseLegacyQuery, serializeSelection, splitPlatformParams, type UrlSelection, } from './lib/legacy-url.js'; | ||
| export { loadConfig } from './lib/load-config.js'; | ||
@@ -6,0 +6,0 @@ export { type StorageLike } from './lib/local-storage-codec.js'; |
+1
-1
| export { applyIntents } from './lib/apply-intents.js'; | ||
| export { getConfig, setConfig } from './lib/config-store.js'; | ||
| export { toDebugSettings } from './lib/debug-settings.js'; | ||
| export { parseLegacyQuery, serializeSelection, } from './lib/legacy-url.js'; | ||
| export { parseLegacyQuery, serializeSelection, splitPlatformParams, } from './lib/legacy-url.js'; | ||
| export { loadConfig } from './lib/load-config.js'; | ||
@@ -6,0 +6,0 @@ export { serializeConfigResources, } from './lib/serialize-config-resources.js'; |
@@ -161,2 +161,17 @@ /** | ||
| /** | ||
| * Split a raw `location.search` into the floor plan's own query and the | ||
| * platform's routing params (`?expo=…` — {@link platformParamKeys}). | ||
| * Raw-segment level (split on `&`, no decoding): platform params travel | ||
| * outside the legacy grammar's single-`encodeURIComponent`-unit payload, and | ||
| * that payload must pass through untouched. The floor plan's single URL | ||
| * writer (`url-sync`) hides the platform half from what the app reads back | ||
| * and re-attaches it verbatim to every URL it writes — a preview URL | ||
| * `/s/<sha>/?expo=…` must survive reload and sharing after any rewrite. | ||
| * When no platform param is present, `search` is returned byte-identical. | ||
| */ | ||
| export declare function splitPlatformParams(search: string): { | ||
| search: string; | ||
| platform: string; | ||
| }; | ||
| /** | ||
| * The selection's canonical query — `''` for none, else `?<payload>` with the | ||
@@ -163,0 +178,0 @@ * whole payload as one `encodeURIComponent` unit (the legacy contract). This is |
@@ -47,2 +47,19 @@ /** | ||
| const legacyCameraKeys = new Set(['centerxy', 'center', 'z', 'bearing', 'zoomtime', 'roll']); | ||
| /** | ||
| * Query keys owned by the DELIVERY PLATFORM, not the floor plan: `?expo=` is | ||
| * the routing param the sha-delivery platform materializes for expo-aware | ||
| * origins, and preview URLs (`/s/<sha>/?expo=…`) carry it literally in the | ||
| * page URL (design note 2026-08-05-sha-delivery-iac.md § 2). Not a grammar | ||
| * extension — a third partition of the query alongside config-owned and | ||
| * one-shot keys: the parser strips these from the residual slug (in their | ||
| * `key=value` form only — a bare slug that merely spells `expo` still | ||
| * resolves via the catalog), and the floor plan's URL writer carries them | ||
| * verbatim across query rewrites ({@link splitPlatformParams}). | ||
| * | ||
| * Unlike config-owned keys, platform params do NOT classify a query as | ||
| * `ownedByConfig`: `?expo=…` alone DESCRIBES the empty selection — browser | ||
| * back onto it must clear the selection like any genuinely empty query, | ||
| * whereas config keys are instructions riding alongside the selection state. | ||
| */ | ||
| const platformParamKeys = new Set(['expo']); | ||
| export function parseLegacyQuery(search) { | ||
@@ -250,4 +267,5 @@ const rawQuery = search.startsWith('?') ? search.slice(1) : search; | ||
| * The slug is the decoded query minus every key another consumer owns: the | ||
| * config layer's keys and intent shortcuts (read by `loadConfig`), and the | ||
| * one-shot keys consumed above. `heatmap`/`type`/`subtype` stay in the slug — | ||
| * config layer's keys and intent shortcuts (read by `loadConfig`), the | ||
| * one-shot keys consumed above, and the platform's routing params. | ||
| * `heatmap`/`type`/`subtype` stay in the slug — | ||
| * heatmap mode historically froze the URL with them in place (its data loader | ||
@@ -275,2 +293,4 @@ * re-reads them), and the select fallback knows not to treat a query carrying | ||
| return false; | ||
| if (platformParamKeys.has(key) && part.includes('=')) | ||
| return false; | ||
| // Carve-out (retirement plan §4.8): `heatmap` is a UrlConfigSchema key | ||
@@ -288,2 +308,29 @@ // now, so the canonical bracketed `heatmap[…]=` form is config-owned and | ||
| } | ||
| /* ── Platform routing params: the writer-side partition ────────────────────── */ | ||
| /** | ||
| * Split a raw `location.search` into the floor plan's own query and the | ||
| * platform's routing params (`?expo=…` — {@link platformParamKeys}). | ||
| * Raw-segment level (split on `&`, no decoding): platform params travel | ||
| * outside the legacy grammar's single-`encodeURIComponent`-unit payload, and | ||
| * that payload must pass through untouched. The floor plan's single URL | ||
| * writer (`url-sync`) hides the platform half from what the app reads back | ||
| * and re-attaches it verbatim to every URL it writes — a preview URL | ||
| * `/s/<sha>/?expo=…` must survive reload and sharing after any rewrite. | ||
| * When no platform param is present, `search` is returned byte-identical. | ||
| */ | ||
| export function splitPlatformParams(search) { | ||
| const raw = search.startsWith('?') ? search.slice(1) : search; | ||
| if (!raw) | ||
| return { search, platform: '' }; | ||
| const segments = raw.split('&'); | ||
| const platform = segments.filter(isPlatformParam); | ||
| if (!platform.length) | ||
| return { search, platform: '' }; | ||
| const app = segments.filter((segment) => !isPlatformParam(segment)); | ||
| return { search: app.length ? '?' + app.join('&') : '', platform: platform.join('&') }; | ||
| } | ||
| function isPlatformParam(segment) { | ||
| const eq = segment.indexOf('='); | ||
| return eq > 0 && platformParamKeys.has(segment.slice(0, eq)); | ||
| } | ||
| /* ── Serialize: selection → location.search ────────────────────────────────── */ | ||
@@ -290,0 +337,0 @@ /** |
@@ -9,4 +9,5 @@ import { type Ref } from '@expofp/resolve'; | ||
| * | ||
| * With a `legacyDataUrlBase` (live events), the version probe settles the | ||
| * `?v=` cache-buster, refs to the sibling drawing / wayfinding files are | ||
| * With a `legacyDataUrlBase` (live events), the manifest's `legacyDataVersion` | ||
| * pin — or, absent one, the version probe — settles the `?v=` cache-buster, | ||
| * refs to the sibling drawing / wayfinding files are | ||
| * derived from it (`data.js` never carries them), and all three legacy files — | ||
@@ -13,0 +14,0 @@ * `data.js` evaluated to its `__data` document like every legacy `.js` file, |
+30
-13
@@ -20,4 +20,5 @@ /// <reference lib="dom" /> | ||
| * | ||
| * With a `legacyDataUrlBase` (live events), the version probe settles the | ||
| * `?v=` cache-buster, refs to the sibling drawing / wayfinding files are | ||
| * With a `legacyDataUrlBase` (live events), the manifest's `legacyDataVersion` | ||
| * pin — or, absent one, the version probe — settles the `?v=` cache-buster, | ||
| * refs to the sibling drawing / wayfinding files are | ||
| * derived from it (`data.js` never carries them), and all three legacy files — | ||
@@ -49,7 +50,6 @@ * `data.js` evaluated to its `__data` document like every legacy `.js` file, | ||
| manifest = validateOrClone(ManifestSchema, await resolve(manifest)); | ||
| // The legacy event-data directory (and the old manifests' version pointer, | ||
| // replaced by the version.json probe) are INPUTS, consumed right here — the | ||
| // effective config carries only the refs derived from them, so neither is | ||
| // merged. | ||
| const { legacyDataUrlBase, legacyDataVersion: _manifestVersion, ...manifestFields } = manifest; | ||
| // The legacy event-data directory and the data-revision pin are INPUTS, | ||
| // consumed right here — the effective config carries only the refs derived | ||
| // from them, so neither is merged. | ||
| const { legacyDataUrlBase, legacyDataVersion: manifestPin, ...manifestFields } = manifest; | ||
| applyConsentAlias(manifestFields); | ||
@@ -60,14 +60,31 @@ assignDefined(config, manifestFields); | ||
| applyConsentAlias(options); | ||
| // every legacy file URL is ?v= cache-busted; a timestamp busts when no | ||
| // version.json exists (TODO: drop once the legacy data directory is gone) | ||
| // every legacy file URL is ?v= cache-busted; a timestamp busts when neither | ||
| // the manifest nor version.json pins a revision (TODO: drop once the legacy | ||
| // data directory is gone) | ||
| let legacyDataVersion = String(Date.now()); | ||
| // live legacy directory: version probe, then the sibling file refs — data.js | ||
| // never carries the payload refs, so both are known before it lands | ||
| // live legacy directory: settle the version (manifest pin, else the | ||
| // version.json probe), then the sibling file refs — data.js never carries | ||
| // the payload refs, so both are known before it lands | ||
| if (legacyDataUrlBase) { | ||
| if (isFromDesignerReferrer()) { | ||
| // The designer previews in-progress edits — the directory's files change | ||
| // without a version bump, so the published version must not pin `?v=`; | ||
| // the unique timestamp stands and every load fetches fresh files. | ||
| // without a version bump, so a published version (the manifest pin and | ||
| // version.json alike) must not pin `?v=`; the unique timestamp stands | ||
| // and every load fetches fresh files. | ||
| log('loadConfig', 'designer referrer — cache-busting with a timestamp'); | ||
| } | ||
| else if (typeof manifestPin === 'string' && manifestPin) { | ||
| // The manifest pins the data revision (get-manifest already probed | ||
| // version.json server-side) — honor it, no client probe: manifest and | ||
| // data stay consistent by construction. | ||
| legacyDataVersion = manifestPin; | ||
| } | ||
| else if (manifestPin === null) { | ||
| // The generator probed and there IS no version.json for this expo — the | ||
| // per-load timestamp stands, and the client must not re-probe (on the | ||
| // cross-origin contour that probe could only fail). Only an ABSENT pin | ||
| // (legacy manifests; the retired `$ref` pointer counts as absent) falls | ||
| // through to the probe below. | ||
| log('loadConfig', 'manifest pins "no version" — cache-busting with a timestamp'); | ||
| } | ||
| else { | ||
@@ -74,0 +91,0 @@ try { |
+4
-4
| { | ||
| "name": "@expofp/config", | ||
| "version": "3.19.0", | ||
| "version": "3.20.0", | ||
| "type": "module", | ||
@@ -32,6 +32,6 @@ "description": "ExpoFP SDK internal: config layer and schemas", | ||
| "zod": "4.4.3", | ||
| "@expofp/schema": "3.19.0", | ||
| "@expofp/resolve": "3.19.0", | ||
| "@expofp/utils": "3.19.0" | ||
| "@expofp/schema": "3.20.0", | ||
| "@expofp/utils": "3.20.0", | ||
| "@expofp/resolve": "3.20.0" | ||
| } | ||
| } |
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
127367
3.67%2603
3.17%+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
Updated
Updated
Updated