aviation-chart 2.0.7 → 2.0.9

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/dist/nav-sdk.d.ts CHANGED
@@ -535,6 +535,7 @@ interface MapSDKOptions {
535
535
  staticUrl?: string;
536
536
  apiUrl?: string;
537
537
  apiVersion?: string;
538
+ licenseKey?: string;
538
539
  spriteUrl?: string;
539
540
  terrainUrl?: string;
540
541
  layers?: Record<string, boolean>;
@@ -833,8 +834,8 @@ interface GeometryFeature {
833
834
  };
834
835
  properties: Record<string, any>;
835
836
  }
836
- declare function searchFeatures(baseUrl: string, apiKey: string | undefined, params: SearchParams, version?: string): Promise<SearchResultItem[]>;
837
- declare function getFeatureGeometry(baseUrl: string, apiKey: string | undefined, params: GeometryParams, version?: string): Promise<GeometryFeature>;
837
+ declare function searchFeatures(baseUrl: string, auth: string | Record<string, string> | undefined, params: SearchParams, version?: string): Promise<SearchResultItem[]>;
838
+ declare function getFeatureGeometry(baseUrl: string, auth: string | Record<string, string> | undefined, params: GeometryParams, version?: string): Promise<GeometryFeature>;
838
839
  declare function getPeriods(baseUrl: string, apiKey?: string, signal?: AbortSignal, version?: string): Promise<string[]>;
839
840
 
840
841
  interface SelectFeatureOptions {
@@ -962,6 +963,7 @@ declare class MapSDK implements IMapSDK {
962
963
  private _landGeojson;
963
964
  private _chartApiUrl;
964
965
  private _apiVersion;
966
+ private _tileCipher;
965
967
  readonly ready: Promise<void>;
966
968
  private _resolveReady;
967
969
  private _rejectReady;
@@ -1049,6 +1051,7 @@ declare class MapSDK implements IMapSDK {
1049
1051
  getPeriod(): string | undefined;
1050
1052
  generateCurrentPeriod(): Promise<string>;
1051
1053
  search(params: SearchParams): Promise<SearchResultItem[]>;
1054
+ private _apiAuth;
1052
1055
  getGeometry(params: GeometryParams): Promise<GeometryFeature>;
1053
1056
  getPeriods(signal?: AbortSignal): Promise<string[]>;
1054
1057
  showGraticule(): void;
@@ -4284,8 +4284,10 @@ class GraticuleManager {
4284
4284
  }
4285
4285
  }
4286
4286
 
4287
- function authHeaders(apiKey) {
4288
- return apiKey ? { "X-Api-Key": apiKey } : {};
4287
+ function authHeaders(auth) {
4288
+ if (!auth)
4289
+ return {};
4290
+ return typeof auth === "string" ? { "X-Api-Key": auth } : auth;
4289
4291
  }
4290
4292
  function apiPrefix$1(baseUrl, version) {
4291
4293
  const base = baseUrl.replace(/\/$/, "");
@@ -4299,14 +4301,15 @@ async function toJSON(res) {
4299
4301
  }
4300
4302
  catch {
4301
4303
  }
4304
+ const detailMsg = detail.trim();
4302
4305
  const msg = res.status === 403
4303
- ? "搜索接口鉴权失败(403):请检查初始化传入的 apiKey 是否正确"
4304
- : `请求失败(${res.status})${detail ? ": " + detail.trim() : ""}`;
4306
+ ? `接口鉴权失败(403):${detailMsg || "请检查 licenseKey / apiKey 是否正确"}`
4307
+ : `请求失败(${res.status})${detailMsg ? ": " + detailMsg : ""}`;
4305
4308
  throw new Error(msg);
4306
4309
  }
4307
4310
  return res.json();
4308
4311
  }
4309
- async function searchFeatures(baseUrl, apiKey, params, version) {
4312
+ async function searchFeatures(baseUrl, auth, params, version) {
4310
4313
  const { q, type, period, limit, signal } = params;
4311
4314
  if (!q || !q.trim()) {
4312
4315
  throw new Error("search: q 参数必填");
@@ -4320,13 +4323,13 @@ async function searchFeatures(baseUrl, apiKey, params, version) {
4320
4323
  qs.set("limit", String(limit));
4321
4324
  const res = await fetch(`${apiPrefix$1(baseUrl, version)}/search?${qs.toString()}`, {
4322
4325
  method: "GET",
4323
- headers: authHeaders(apiKey),
4326
+ headers: authHeaders(auth),
4324
4327
  ...(signal ? { signal } : {}),
4325
4328
  });
4326
4329
  const data = await toJSON(res);
4327
4330
  return data.results ?? [];
4328
4331
  }
4329
- async function getFeatureGeometry(baseUrl, apiKey, params, version) {
4332
+ async function getFeatureGeometry(baseUrl, auth, params, version) {
4330
4333
  const { type, id, period, signal } = params;
4331
4334
  if (!type || !id) {
4332
4335
  throw new Error("getGeometry: type 和 id 参数必填");
@@ -4336,7 +4339,7 @@ async function getFeatureGeometry(baseUrl, apiKey, params, version) {
4336
4339
  qs.set("period", period);
4337
4340
  const res = await fetch(`${apiPrefix$1(baseUrl, version)}/geometry?${qs.toString()}`, {
4338
4341
  method: "GET",
4339
- headers: authHeaders(apiKey),
4342
+ headers: authHeaders(auth),
4340
4343
  ...(signal ? { signal } : {}),
4341
4344
  });
4342
4345
  return toJSON(res);
@@ -7446,13 +7449,15 @@ function getAviationBaseStyle(opts) {
7446
7449
  }
7447
7450
  };
7448
7451
  }
7449
- function navdataTilesUrl(apiUrl, period, version) {
7450
- return `${apiPrefix(apiUrl, version)}/tiles/${period}/{z}/{x}/{y}.pbf`;
7452
+ function navdataTilesUrl(apiUrl, period, version, encrypted) {
7453
+ const seg = encrypted ? "tiles/e" : "tiles";
7454
+ const url = `${apiPrefix(apiUrl, version)}/${seg}/${period}/{z}/{x}/{y}.pbf`;
7455
+ return encrypted ? `kunyu://${url}` : url;
7451
7456
  }
7452
- function getNavdataSource(apiUrl, period, version) {
7457
+ function getNavdataSource(apiUrl, period, version, encrypted) {
7453
7458
  return {
7454
7459
  type: "vector",
7455
- tiles: [navdataTilesUrl(apiUrl, period, version)],
7460
+ tiles: [navdataTilesUrl(apiUrl, period, version, encrypted)],
7456
7461
  minzoom: 0.5,
7457
7462
  maxzoom: 18,
7458
7463
  };
@@ -7591,6 +7596,101 @@ async function fetchGzJson(url, headers = {}) {
7591
7596
  return new Response(stream).json();
7592
7597
  }
7593
7598
 
7599
+ const MAGIC = "KYT1";
7600
+ const IV_LEN = 12;
7601
+ const HEADER_LEN = 6;
7602
+ const FLAG_GZIP = 0x01;
7603
+ function b64ToBytes(b64) {
7604
+ const bin = atob(b64);
7605
+ const out = new Uint8Array(bin.length);
7606
+ for (let i = 0; i < bin.length; i++)
7607
+ out[i] = bin.charCodeAt(i);
7608
+ return out;
7609
+ }
7610
+ async function gunzip(buf) {
7611
+ const ds = new globalThis.DecompressionStream("gzip");
7612
+ const stream = new Blob([buf]).stream().pipeThrough(ds);
7613
+ return await new Response(stream).arrayBuffer();
7614
+ }
7615
+ class TileCipher {
7616
+ constructor(apiUrl, version, licenseKey) {
7617
+ this.periodKeys = new Map();
7618
+ const dot = licenseKey.indexOf(".");
7619
+ if (dot <= 0) {
7620
+ throw new Error("[MapSDK] 无效的 licenseKey,应为 \"customerId.base64Key\"");
7621
+ }
7622
+ this.cid = licenseKey.slice(0, dot);
7623
+ const raw = b64ToBytes(licenseKey.slice(dot + 1));
7624
+ this.customerKeyRaw = raw;
7625
+ this.customerKey = crypto.subtle.importKey("raw", raw, { name: "AES-GCM" }, false, ["decrypt"]);
7626
+ this.wrapBase = apiPrefix(apiUrl, version);
7627
+ }
7628
+ async apiAuthHeaders() {
7629
+ const key = await this.authKey();
7630
+ const ts = Math.floor(Date.now() / 1000);
7631
+ const sigBuf = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(`${this.cid}|${ts}`));
7632
+ const sig = Array.from(new Uint8Array(sigBuf))
7633
+ .map((b) => b.toString(16).padStart(2, "0"))
7634
+ .join("");
7635
+ return { "X-Cid": this.cid, "X-Ts": String(ts), "X-Sig": sig };
7636
+ }
7637
+ authKey() {
7638
+ if (!this.authKeyPromise) {
7639
+ this.authKeyPromise = (async () => {
7640
+ const ck = await crypto.subtle.importKey("raw", this.customerKeyRaw, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
7641
+ const derived = await crypto.subtle.sign("HMAC", ck, new TextEncoder().encode("kunyu-api-auth-v1"));
7642
+ return crypto.subtle.importKey("raw", derived, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
7643
+ })();
7644
+ }
7645
+ return this.authKeyPromise;
7646
+ }
7647
+ periodKey(period) {
7648
+ let p = this.periodKeys.get(period);
7649
+ if (!p) {
7650
+ p = this.fetchPeriodKey(period).catch((e) => {
7651
+ this.periodKeys.delete(period);
7652
+ throw e;
7653
+ });
7654
+ this.periodKeys.set(period, p);
7655
+ }
7656
+ return p;
7657
+ }
7658
+ async fetchPeriodKey(period) {
7659
+ const url = `${this.wrapBase}/tilewrap?cid=${encodeURIComponent(this.cid)}&period=${encodeURIComponent(period)}`;
7660
+ const resp = await fetch(url);
7661
+ if (!resp.ok) {
7662
+ throw new Error(`[MapSDK] 取瓦片密钥失败 (HTTP ${resp.status})`);
7663
+ }
7664
+ const { wrapped } = (await resp.json());
7665
+ const blob = b64ToBytes(wrapped);
7666
+ const iv = blob.slice(0, IV_LEN);
7667
+ const ct = blob.slice(IV_LEN);
7668
+ const ck = await this.customerKey;
7669
+ const rawPeriodKey = await crypto.subtle.decrypt({ name: "AES-GCM", iv: iv }, ck, ct);
7670
+ return crypto.subtle.importKey("raw", rawPeriodKey, { name: "AES-GCM" }, false, ["decrypt"]);
7671
+ }
7672
+ async decrypt(realUrl, envelope) {
7673
+ const m = realUrl.match(/\/tiles\/e\/([^/]+)\//);
7674
+ const period = m?.[1];
7675
+ if (!period)
7676
+ throw new Error("[MapSDK] 无法从 URL 解析期号: " + realUrl);
7677
+ const bytes = new Uint8Array(envelope);
7678
+ const magic = new TextDecoder().decode(bytes.subarray(0, 4));
7679
+ if (bytes.length < HEADER_LEN + IV_LEN || magic !== MAGIC) {
7680
+ throw new Error("[MapSDK] 瓦片信封无效 (magic)");
7681
+ }
7682
+ const flags = bytes[5] ?? 0;
7683
+ const aad = bytes.slice(0, HEADER_LEN);
7684
+ const iv = bytes.slice(HEADER_LEN, HEADER_LEN + IV_LEN);
7685
+ const ct = bytes.slice(HEADER_LEN + IV_LEN);
7686
+ const key = await this.periodKey(period);
7687
+ let plain = await crypto.subtle.decrypt({ name: "AES-GCM", iv: iv, additionalData: aad }, key, ct);
7688
+ if (flags & FLAG_GZIP)
7689
+ plain = await gunzip(plain);
7690
+ return plain;
7691
+ }
7692
+ }
7693
+
7594
7694
  // ICAO aviation chart layer styles — token-driven multi-theme.
7595
7695
  //
7596
7696
  // 单一 buildLayers(T):图层结构只写一份(以 light 为准);凡随主题变化的值
@@ -7914,9 +8014,9 @@ function buildLayers(themeId = 'light') {
7914
8014
  "text-size": ["interpolate", ["linear"], ["zoom"], 4, 0, 5, 10, 9, 12],
7915
8015
  "text-offset": [
7916
8016
  "interpolate", ["linear"], ["zoom"],
7917
- 5, ["literal", [0, -1.0]],
7918
- 9, ["literal", [0, -1.8]],
7919
- 12, ["literal", [0, -2.0]]
8017
+ 5, ["literal", [0, -0.8]],
8018
+ 9, ["literal", [0, -1.2]],
8019
+ 12, ["literal", [0, -1.3]]
7920
8020
  ],
7921
8021
  "icon-allow-overlap": true,
7922
8022
  "icon-ignore-placement": true,
@@ -8185,6 +8285,26 @@ class MapSDK {
8185
8285
  this._baseUrl = options.staticUrl || "https://aips.siniswift.com";
8186
8286
  this._chartApiUrl = (options.apiUrl || DEFAULT_CHART_API_URL).replace(/\/$/, "");
8187
8287
  this._apiVersion = options.apiVersion || undefined;
8288
+ if (options.licenseKey) {
8289
+ try {
8290
+ this._tileCipher = new TileCipher(this._chartApiUrl, this._apiVersion, options.licenseKey);
8291
+ const cipher = this._tileCipher;
8292
+ _mlGlExports.addProtocol("kunyu", async (params) => {
8293
+ const realUrl = String(params.url).replace(/^kunyu:\/\//, "");
8294
+ const resp = await fetch(realUrl, { signal: params.signal });
8295
+ if (!resp.ok)
8296
+ throw new Error(`tile ${resp.status}`);
8297
+ const buf = await resp.arrayBuffer();
8298
+ if (buf.byteLength === 0)
8299
+ return { data: null };
8300
+ const data = await cipher.decrypt(realUrl, buf);
8301
+ return { data };
8302
+ });
8303
+ }
8304
+ catch (e) {
8305
+ this._tileCipher = undefined;
8306
+ }
8307
+ }
8188
8308
  const themeId = this._resolveThemeId(options.theme);
8189
8309
  const themeConfig = CHART_THEMES[themeId] ?? CHART_THEMES.light;
8190
8310
  const style = options.style ??
@@ -8339,7 +8459,7 @@ class MapSDK {
8339
8459
  return;
8340
8460
  }
8341
8461
  if (!this.map.getSource("navdata")) {
8342
- this.map.addSource("navdata", getNavdataSource(this._chartApiUrl, period, this._apiVersion));
8462
+ this.map.addSource("navdata", getNavdataSource(this._chartApiUrl, period, this._apiVersion, !!this._tileCipher));
8343
8463
  }
8344
8464
  const themeId = this._resolveThemeId(this.initialOptions.theme);
8345
8465
  for (const layer of buildLayers(themeId)) {
@@ -8682,7 +8802,7 @@ class MapSDK {
8682
8802
  this.globalConfig.setPeriod(period);
8683
8803
  const navdata = this.map?.getSource("navdata");
8684
8804
  if (navdata?.setTiles) {
8685
- navdata.setTiles([navdataTilesUrl(this._chartApiUrl, period, this._apiVersion)]);
8805
+ navdata.setTiles([navdataTilesUrl(this._chartApiUrl, period, this._apiVersion, !!this._tileCipher)]);
8686
8806
  }
8687
8807
  if (from !== undefined && from !== period) {
8688
8808
  this.map?.fire("periodchange", { from, to: period });
@@ -8694,18 +8814,23 @@ class MapSDK {
8694
8814
  async generateCurrentPeriod() {
8695
8815
  return await this.globalConfig.generateCurrentPeriod();
8696
8816
  }
8697
- search(params) {
8698
- const apiKey = this.globalConfig.getConfig().apiKey;
8817
+ async search(params) {
8818
+ const auth = await this._apiAuth();
8699
8819
  const period = params.period ?? this.getPeriod();
8700
- return searchFeatures(this._chartApiUrl, apiKey, {
8820
+ return searchFeatures(this._chartApiUrl, auth, {
8701
8821
  ...params,
8702
8822
  ...(period ? { period } : {}),
8703
8823
  }, this._apiVersion);
8704
8824
  }
8705
- getGeometry(params) {
8706
- const apiKey = this.globalConfig.getConfig().apiKey;
8825
+ async _apiAuth() {
8826
+ if (this._tileCipher)
8827
+ return this._tileCipher.apiAuthHeaders();
8828
+ return this.globalConfig.getConfig().apiKey;
8829
+ }
8830
+ async getGeometry(params) {
8831
+ const auth = await this._apiAuth();
8707
8832
  const period = params.period ?? this.getPeriod();
8708
- return getFeatureGeometry(this._chartApiUrl, apiKey, {
8833
+ return getFeatureGeometry(this._chartApiUrl, auth, {
8709
8834
  ...params,
8710
8835
  ...(period ? { period } : {}),
8711
8836
  }, this._apiVersion);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aviation-chart",
3
- "version": "2.0.7",
3
+ "version": "2.0.9",
4
4
  "description": "KunYu aviation map SDK",
5
5
  "main": "dist/nav-sdk.esm.js",
6
6
  "module": "dist/nav-sdk.esm.js",