@lionrapid/core 0.6.1 → 0.6.2

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.
Files changed (42) hide show
  1. package/dist/browser/auto-init.d.ts +8 -3
  2. package/dist/browser/auto-init.d.ts.map +1 -1
  3. package/dist/browser/auto-init.js +34 -26
  4. package/dist/browser/auto-init.js.map +1 -1
  5. package/dist/browser/lionrapid.auto.dev.js +264 -24
  6. package/dist/browser/lionrapid.auto.dev.js.map +2 -2
  7. package/dist/browser/lionrapid.auto.js +264 -24
  8. package/dist/browser/lionrapid.auto.js.map +2 -2
  9. package/dist/browser/lionrapid.auto.min.js +4 -4
  10. package/dist/browser/lionrapid.auto.min.js.map +3 -3
  11. package/dist/browser/lionrapid.esm.js +238 -2
  12. package/dist/browser/lionrapid.esm.js.map +2 -2
  13. package/dist/browser/lionrapid.esm.min.js +3 -3
  14. package/dist/browser/lionrapid.esm.min.js.map +3 -3
  15. package/dist/browser/lionrapid.iife.dev.js +238 -2
  16. package/dist/browser/lionrapid.iife.dev.js.map +2 -2
  17. package/dist/browser/lionrapid.iife.js +238 -2
  18. package/dist/browser/lionrapid.iife.js.map +2 -2
  19. package/dist/browser/lionrapid.iife.min.js +3 -3
  20. package/dist/browser/lionrapid.iife.min.js.map +3 -3
  21. package/dist/browser/lionrapid.umd.js +238 -2
  22. package/dist/browser/lionrapid.umd.js.map +2 -2
  23. package/dist/browser/lionrapid.umd.min.js +3 -3
  24. package/dist/browser/lionrapid.umd.min.js.map +3 -3
  25. package/dist/browser/storage/cache-storage-repository.d.ts +65 -0
  26. package/dist/browser/storage/cache-storage-repository.d.ts.map +1 -1
  27. package/dist/browser/storage/cache-storage-repository.js +77 -7
  28. package/dist/browser/storage/cache-storage-repository.js.map +1 -1
  29. package/dist/browser/storage/localstorage-repository.d.ts +62 -0
  30. package/dist/browser/storage/localstorage-repository.d.ts.map +1 -1
  31. package/dist/browser/storage/localstorage-repository.js +71 -4
  32. package/dist/browser/storage/localstorage-repository.js.map +1 -1
  33. package/dist/config/repository.config.d.ts +17 -0
  34. package/dist/config/repository.config.d.ts.map +1 -1
  35. package/dist/index.d.ts +1 -1
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js.map +1 -1
  38. package/dist/repositories/network-repository.d.ts +42 -1
  39. package/dist/repositories/network-repository.d.ts.map +1 -1
  40. package/dist/repositories/network-repository.js +149 -1
  41. package/dist/repositories/network-repository.js.map +1 -1
  42. package/package.json +1 -1
@@ -3989,6 +3989,31 @@ var _NetworkRepository = class _NetworkRepository {
3989
3989
  * and payload can never desync because they live and die together.
3990
3990
  */
3991
3991
  __publicField(this, "namespaceSnapshots", /* @__PURE__ */ new Map());
3992
+ /**
3993
+ * Miss-path flood protection (READINESS before-scale #1: one cold locale
3994
+ * switch measured ~380 requests in 10 s — every t() miss refired a key GET
3995
+ * and a namespace GET on every re-render, forever on animated pages).
3996
+ */
3997
+ /** GETs currently on the wire, keyed by `url|if-none-match` — identical
3998
+ * concurrent requests share one fetch; each awaiter gets a clone. The
3999
+ * If-None-Match value is part of the key so the 304-without-snapshot
4000
+ * unconditional refetch never coalesces onto a conditional flight. */
4001
+ __publicField(this, "inflightGets", /* @__PURE__ */ new Map());
4002
+ /** URL → 404/423 seen recently: repeats are answered locally (same typed
4003
+ * error, no network) until `until`. 423 honors the server's Retry-After.
4004
+ * Cleared entirely by any successful write — discovery sync keeps its
4005
+ * miss → batch → served-back round-trip. */
4006
+ __publicField(this, "negativeCache", /* @__PURE__ */ new Map());
4007
+ /** One-way latch: a 401 anywhere (or a 403 on a READ) means the key is
4008
+ * wrong or unauthorized for this project, and retrying cannot heal it —
4009
+ * after the first one nothing touches the network again for this
4010
+ * instance (a page reload starts fresh). Holds the status that tripped
4011
+ * it so short-circuits answer honestly. */
4012
+ __publicField(this, "authFailedStatus", null);
4013
+ /** Write-only latch (auth-byok Phase 18): a 403 (scope) or 429 (quota) on
4014
+ * a WRITE degrades this instance to read-only — writes stop, silently
4015
+ * and without retries, while reads keep serving the page. */
4016
+ __publicField(this, "writesFailedStatus", null);
3992
4017
  /**
3993
4018
  * Default endpoint templates with placeholder support
3994
4019
  * Placeholders: :key, :locale, :namespace
@@ -3996,6 +4021,7 @@ var _NetworkRepository = class _NetworkRepository {
3996
4021
  __publicField(this, "DEFAULT_ENDPOINTS", {
3997
4022
  getTranslation: "/api/integrations/translations/:key",
3998
4023
  getNamespace: "/api/integrations/translations/:locale/:namespace",
4024
+ getProjectConfig: "/api/integrations/project-config",
3999
4025
  setTranslation: "/api/integrations/sync/:key",
4000
4026
  setNamespace: "/api/integrations/sync/:locale/:namespace",
4001
4027
  setBatch: "/api/integrations/sync/batch",
@@ -4107,6 +4133,40 @@ var _NetworkRepository = class _NetworkRepository {
4107
4133
  this.handleCaughtError(error);
4108
4134
  }
4109
4135
  }
4136
+ /**
4137
+ * Project-level config for this key — the dashboard's language list,
4138
+ * served under the same trust model as translation reads (docs/20,
4139
+ * companion track). Returns null when the server doesn't expose the
4140
+ * endpoint (404 — older server, feature not deployed) or the payload is
4141
+ * unusable: absence means "keep your local fallback", never an error.
4142
+ */
4143
+ async getProjectConfig() {
4144
+ if (!this.config?.options?.baseUrl) {
4145
+ throw new NetworkError("API base URL is not set");
4146
+ }
4147
+ const url = this.buildUrl("getProjectConfig", {});
4148
+ logger.debug(`API GET ProjectConfig: ${url}`);
4149
+ try {
4150
+ const response = await this.makeRequest(url, "GET");
4151
+ if (response.status === 404) {
4152
+ return null;
4153
+ }
4154
+ if (!response.ok) {
4155
+ this.handleHttpError(response, "project-config");
4156
+ }
4157
+ const data = await response.json();
4158
+ if (!data || typeof data.defaultLocale !== "string" || !Array.isArray(data.languages)) {
4159
+ logger.warn("project-config payload unusable \u2014 keeping local fallback");
4160
+ return null;
4161
+ }
4162
+ return {
4163
+ defaultLocale: data.defaultLocale,
4164
+ languages: data.languages.map(String)
4165
+ };
4166
+ } catch (error) {
4167
+ this.handleCaughtError(error);
4168
+ }
4169
+ }
4110
4170
  /**
4111
4171
  * Shared namespace fetch with the canonical 304 contract:
4112
4172
  * - If-None-Match is sent ONLY when a snapshot (version + payload) is in hand.
@@ -4431,7 +4491,87 @@ var _NetworkRepository = class _NetworkRepository {
4431
4491
  /**
4432
4492
  * Make HTTP request with timeout and headers
4433
4493
  */
4434
- async makeRequest(url, method, body, extraHeaders) {
4494
+ makeRequest(url, method, body, extraHeaders) {
4495
+ if (this.authFailedStatus !== null) {
4496
+ return Promise.resolve(
4497
+ new Response(null, {
4498
+ status: this.authFailedStatus,
4499
+ statusText: `auth latched off after ${this.authFailedStatus}`
4500
+ })
4501
+ );
4502
+ }
4503
+ if (method !== "GET") {
4504
+ if (this.writesFailedStatus !== null) {
4505
+ return Promise.resolve(
4506
+ new Response(null, {
4507
+ status: this.writesFailedStatus,
4508
+ statusText: `writes latched off after ${this.writesFailedStatus}`
4509
+ })
4510
+ );
4511
+ }
4512
+ return this.doFetch(url, method, body, extraHeaders).then(
4513
+ (resp) => this.recordOutcome(url, method, resp)
4514
+ );
4515
+ }
4516
+ const negative = this.negativeCache.get(url);
4517
+ if (negative) {
4518
+ if (Date.now() < negative.until) {
4519
+ logger.debug(`Negative-cache hit (${negative.status}): ${url}`);
4520
+ const headers = negative.status === 423 ? { "Retry-After": String(Math.ceil((negative.until - Date.now()) / 1e3)) } : void 0;
4521
+ return Promise.resolve(
4522
+ new Response(null, { status: negative.status, statusText: "negative cache", headers })
4523
+ );
4524
+ }
4525
+ this.negativeCache.delete(url);
4526
+ }
4527
+ const flightKey = `${url}|${extraHeaders?.["If-None-Match"] ?? ""}`;
4528
+ const existing = this.inflightGets.get(flightKey);
4529
+ if (existing) {
4530
+ return existing.then((resp) => this.cloneResponse(resp));
4531
+ }
4532
+ const flight = this.doFetch(url, method, body, extraHeaders).then((resp) => this.recordOutcome(url, method, resp)).finally(() => this.inflightGets.delete(flightKey));
4533
+ this.inflightGets.set(flightKey, flight);
4534
+ return flight.then((resp) => this.cloneResponse(resp));
4535
+ }
4536
+ /** Bookkeeping on every real (non-synthesized) response — runs once per
4537
+ * physical fetch, never once per coalesced awaiter. */
4538
+ recordOutcome(url, method, response) {
4539
+ const status = response.status;
4540
+ if (status === 401 || status === 403 && method === "GET") {
4541
+ if (this.authFailedStatus === null) {
4542
+ this.authFailedStatus = status;
4543
+ console.warn(
4544
+ `[LionRapid] API key rejected (${status}) \u2014 network translations are disabled for this session. Fix the key and reload; retrying cannot heal an auth failure.`
4545
+ );
4546
+ }
4547
+ return response;
4548
+ }
4549
+ if ((status === 403 || status === 429) && method !== "GET") {
4550
+ if (this.writesFailedStatus === null) {
4551
+ this.writesFailedStatus = status;
4552
+ logger.warn(
4553
+ `Write path disabled after ${status} (scope/quota) \u2014 running read-only`
4554
+ );
4555
+ }
4556
+ return response;
4557
+ }
4558
+ if (method === "GET" && (status === 404 || status === 423)) {
4559
+ const retryAfterSec = status === 423 ? Number(response.headers?.get?.("Retry-After")) : NaN;
4560
+ const ttl = Number.isFinite(retryAfterSec) && retryAfterSec > 0 ? retryAfterSec * 1e3 : this.config.options?.negativeCacheTtlMs ?? _NetworkRepository.DEFAULT_NEGATIVE_TTL_MS;
4561
+ this.negativeCache.set(url, { status, until: Date.now() + ttl });
4562
+ } else if (method !== "GET" && response.ok) {
4563
+ this.negativeCache.clear();
4564
+ } else if (method === "GET" && response.ok) {
4565
+ this.negativeCache.delete(url);
4566
+ }
4567
+ return response;
4568
+ }
4569
+ /** Test doubles (and exotic runtimes) hand us plain objects — only real
4570
+ * Responses need cloning so each coalesced awaiter can read the body. */
4571
+ cloneResponse(response) {
4572
+ return typeof response.clone === "function" ? response.clone() : response;
4573
+ }
4574
+ async doFetch(url, method, body, extraHeaders) {
4435
4575
  const controller = new AbortController();
4436
4576
  const timeout = this.config.options?.timeout ?? 1e4;
4437
4577
  const timeoutId = setTimeout(
@@ -4464,6 +4604,7 @@ __publicField(_NetworkRepository, "SNAPSHOT_PREFIX", "lionrapid:ns:");
4464
4604
  /** Legacy bare-version prefix — a version WITHOUT its payload is exactly the
4465
4605
  * desync the snapshot store exists to prevent; entries are deleted on sight. */
4466
4606
  __publicField(_NetworkRepository, "LEGACY_VERSION_PREFIX", "lionrapid:v:");
4607
+ __publicField(_NetworkRepository, "DEFAULT_NEGATIVE_TTL_MS", 6e4);
4467
4608
  var NetworkRepository = _NetworkRepository;
4468
4609
 
4469
4610
  // src/content/validator/ContentValidator.ts
@@ -5062,9 +5203,11 @@ var LocalStorageRepository = class {
5062
5203
  this.config = {
5063
5204
  enabled: config.enabled ?? true,
5064
5205
  priority: config.priority ?? 0,
5206
+ // Use build sequence by default
5065
5207
  options: {
5066
5208
  prefix: config.options?.prefix ?? STORAGE_PREFIX,
5067
5209
  ttl: config.options?.ttl ?? 24 * 60 * 60 * 1e3,
5210
+ // 24 hours default
5068
5211
  maxSize: config.options?.maxSize ?? 500,
5069
5212
  ...config.options
5070
5213
  }
@@ -5077,6 +5220,10 @@ var LocalStorageRepository = class {
5077
5220
  this.migrateIfNeeded();
5078
5221
  }
5079
5222
  }
5223
+ /**
5224
+ * Check if localStorage is available
5225
+ * Returns false in SSR, private browsing, or when quota is 0
5226
+ */
5080
5227
  checkAvailability() {
5081
5228
  if (typeof window === "undefined") {
5082
5229
  return false;
@@ -5090,6 +5237,9 @@ var LocalStorageRepository = class {
5090
5237
  return false;
5091
5238
  }
5092
5239
  }
5240
+ /**
5241
+ * Handle version migrations - clear cache when version changes
5242
+ */
5093
5243
  migrateIfNeeded() {
5094
5244
  try {
5095
5245
  const storedVersion = window.localStorage.getItem(VERSION_KEY);
@@ -5100,6 +5250,10 @@ var LocalStorageRepository = class {
5100
5250
  } catch {
5101
5251
  }
5102
5252
  }
5253
+ /**
5254
+ * Get a translation value by key
5255
+ * Returns null if not found or expired
5256
+ */
5103
5257
  get(key) {
5104
5258
  if (!this.available) return null;
5105
5259
  try {
@@ -5116,6 +5270,10 @@ var LocalStorageRepository = class {
5116
5270
  return null;
5117
5271
  }
5118
5272
  }
5273
+ /**
5274
+ * Set a translation value
5275
+ * Automatically handles quota exceeded by evicting oldest entries
5276
+ */
5119
5277
  set(key, value) {
5120
5278
  if (!this.available) return;
5121
5279
  const fullKey = this.prefix + key;
@@ -5140,6 +5298,9 @@ var LocalStorageRepository = class {
5140
5298
  }
5141
5299
  }
5142
5300
  }
5301
+ /**
5302
+ * Remove a translation by key
5303
+ */
5143
5304
  remove(key) {
5144
5305
  if (!this.available) return;
5145
5306
  try {
@@ -5147,6 +5308,10 @@ var LocalStorageRepository = class {
5147
5308
  } catch {
5148
5309
  }
5149
5310
  }
5311
+ /**
5312
+ * Clear all LionRapid entries from localStorage
5313
+ * Preserves entries from other applications
5314
+ */
5150
5315
  clear() {
5151
5316
  if (!this.available) return;
5152
5317
  try {
@@ -5161,6 +5326,9 @@ var LocalStorageRepository = class {
5161
5326
  } catch {
5162
5327
  }
5163
5328
  }
5329
+ /**
5330
+ * Clear all entries for a specific namespace
5331
+ */
5164
5332
  clearNamespace(locale, namespace) {
5165
5333
  if (!this.available) return;
5166
5334
  const namespaceCacheKey = `@${locale}:${namespace}`;
@@ -5179,6 +5347,9 @@ var LocalStorageRepository = class {
5179
5347
  } catch {
5180
5348
  }
5181
5349
  }
5350
+ /**
5351
+ * Get namespace translations
5352
+ */
5182
5353
  getNamespace(locale, namespace) {
5183
5354
  const namespaceCacheKey = `@${locale}:${namespace}`;
5184
5355
  const value = this.get(namespaceCacheKey);
@@ -5190,10 +5361,16 @@ var LocalStorageRepository = class {
5190
5361
  return null;
5191
5362
  }
5192
5363
  }
5364
+ /**
5365
+ * Set namespace translations
5366
+ */
5193
5367
  setNamespace(locale, namespace, translations) {
5194
5368
  const namespaceCacheKey = `@${locale}:${namespace}`;
5195
5369
  this.set(namespaceCacheKey, JSON.stringify(translations));
5196
5370
  }
5371
+ /**
5372
+ * Get all cache entries for debugging
5373
+ */
5197
5374
  getEntries() {
5198
5375
  const entries = {};
5199
5376
  if (!this.available) return entries;
@@ -5213,12 +5390,21 @@ var LocalStorageRepository = class {
5213
5390
  }
5214
5391
  return entries;
5215
5392
  }
5393
+ /**
5394
+ * Get repository configuration
5395
+ */
5216
5396
  getConfig() {
5217
5397
  return this.config;
5218
5398
  }
5399
+ /**
5400
+ * Check if localStorage is available
5401
+ */
5219
5402
  isAvailable() {
5220
5403
  return this.available;
5221
5404
  }
5405
+ /**
5406
+ * Get the number of entries in the cache
5407
+ */
5222
5408
  getEntryCount() {
5223
5409
  if (!this.available) return 0;
5224
5410
  let count = 0;
@@ -5233,12 +5419,18 @@ var LocalStorageRepository = class {
5233
5419
  }
5234
5420
  return count;
5235
5421
  }
5422
+ /**
5423
+ * Check if error is a quota exceeded error
5424
+ */
5236
5425
  isQuotaError(error) {
5237
5426
  if (error instanceof DOMException) {
5238
5427
  return error.name === "QuotaExceededError" || error.name === "NS_ERROR_DOM_QUOTA_REACHED";
5239
5428
  }
5240
5429
  return false;
5241
5430
  }
5431
+ /**
5432
+ * Evict oldest 25% of entries when quota is exceeded
5433
+ */
5242
5434
  evictOldest() {
5243
5435
  if (!this.available) return;
5244
5436
  try {
@@ -5266,6 +5458,9 @@ var LocalStorageRepository = class {
5266
5458
  } catch {
5267
5459
  }
5268
5460
  }
5461
+ /**
5462
+ * Get cache statistics
5463
+ */
5269
5464
  getCacheStats() {
5270
5465
  const size = this.getEntryCount();
5271
5466
  const usage = this.maxSize > 0 ? `${Math.round(size / this.maxSize * 100)}%` : "0%";
@@ -5279,7 +5474,7 @@ var LocalStorageRepository = class {
5279
5474
 
5280
5475
  // src/browser/storage/cache-storage-repository.ts
5281
5476
  var CACHE_NAME = "lionrapid-v1";
5282
- var CACHE_PREFIX = "lionrapid:";
5477
+ var CACHE_PREFIX = "https://cache.lionrapid.internal/";
5283
5478
  var CacheStorageRepository = class {
5284
5479
  constructor(config = {}) {
5285
5480
  __publicField(this, "name", "CacheStorageRepository");
@@ -5294,6 +5489,7 @@ var CacheStorageRepository = class {
5294
5489
  options: {
5295
5490
  cacheName: config.options?.cacheName ?? CACHE_NAME,
5296
5491
  ttl: config.options?.ttl ?? 7 * 24 * 60 * 60 * 1e3,
5492
+ // 7 days default
5297
5493
  ...config.options
5298
5494
  }
5299
5495
  };
@@ -5301,9 +5497,17 @@ var CacheStorageRepository = class {
5301
5497
  this.defaultTTL = this.config.options?.ttl ?? 7 * 24 * 60 * 60 * 1e3;
5302
5498
  this.available = this.checkAvailability();
5303
5499
  }
5500
+ /**
5501
+ * Check if Cache API is available
5502
+ * Returns false in SSR, old browsers, or insecure contexts
5503
+ */
5304
5504
  checkAvailability() {
5305
5505
  return typeof caches !== "undefined";
5306
5506
  }
5507
+ /**
5508
+ * Get a translation value by key
5509
+ * @throws {TranslationNotFoundError} When translation doesn't exist or is expired
5510
+ */
5307
5511
  async get(key) {
5308
5512
  if (!this.available) {
5309
5513
  throw new TranslationNotFoundError(key);
@@ -5327,6 +5531,9 @@ var CacheStorageRepository = class {
5327
5531
  throw new TranslationNotFoundError(key);
5328
5532
  }
5329
5533
  }
5534
+ /**
5535
+ * Set a translation value
5536
+ */
5330
5537
  async set(key, value) {
5331
5538
  if (!this.available) return;
5332
5539
  try {
@@ -5345,6 +5552,9 @@ var CacheStorageRepository = class {
5345
5552
  } catch {
5346
5553
  }
5347
5554
  }
5555
+ /**
5556
+ * Remove a translation by key
5557
+ */
5348
5558
  async remove(key) {
5349
5559
  if (!this.available) return;
5350
5560
  try {
@@ -5353,6 +5563,9 @@ var CacheStorageRepository = class {
5353
5563
  } catch {
5354
5564
  }
5355
5565
  }
5566
+ /**
5567
+ * Clear all LionRapid entries from the cache
5568
+ */
5356
5569
  async clear() {
5357
5570
  if (!this.available) return;
5358
5571
  try {
@@ -5364,6 +5577,9 @@ var CacheStorageRepository = class {
5364
5577
  } catch {
5365
5578
  }
5366
5579
  }
5580
+ /**
5581
+ * Clear all entries for a specific namespace
5582
+ */
5367
5583
  async clearNamespace(locale, namespace) {
5368
5584
  if (!this.available) return;
5369
5585
  const namespaceCacheKey = `@${locale}:${namespace}`;
@@ -5381,6 +5597,10 @@ var CacheStorageRepository = class {
5381
5597
  } catch {
5382
5598
  }
5383
5599
  }
5600
+ /**
5601
+ * Get namespace translations
5602
+ * @throws {TranslationNotFoundError} When namespace doesn't exist
5603
+ */
5384
5604
  async getNamespace(locale, namespace) {
5385
5605
  const namespaceCacheKey = `@${locale}:${namespace}`;
5386
5606
  const value = await this.get(namespaceCacheKey);
@@ -5391,16 +5611,29 @@ var CacheStorageRepository = class {
5391
5611
  throw new TranslationNotFoundError(namespaceCacheKey);
5392
5612
  }
5393
5613
  }
5614
+ /**
5615
+ * Set namespace translations
5616
+ */
5394
5617
  async setNamespace(locale, namespace, translations) {
5395
5618
  const namespaceCacheKey = `@${locale}:${namespace}`;
5396
5619
  await this.set(namespaceCacheKey, JSON.stringify(translations));
5397
5620
  }
5621
+ /**
5622
+ * Get repository configuration
5623
+ */
5398
5624
  getConfig() {
5399
5625
  return this.config;
5400
5626
  }
5627
+ /**
5628
+ * Check if Cache API is available
5629
+ */
5401
5630
  isAvailable() {
5402
5631
  return this.available;
5403
5632
  }
5633
+ /**
5634
+ * Get all cached entries (for debugging)
5635
+ * Returns an object mapping keys to values
5636
+ */
5404
5637
  async getEntries() {
5405
5638
  const entries = {};
5406
5639
  if (!this.available) return entries;
@@ -5423,6 +5656,9 @@ var CacheStorageRepository = class {
5423
5656
  }
5424
5657
  return entries;
5425
5658
  }
5659
+ /**
5660
+ * Get cache statistics
5661
+ */
5426
5662
  async getCacheStats() {
5427
5663
  if (!this.available) {
5428
5664
  return { size: 0, cacheName: this.cacheName };