@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
@@ -3897,6 +3897,31 @@ var _NetworkRepository = class _NetworkRepository {
3897
3897
  * and payload can never desync because they live and die together.
3898
3898
  */
3899
3899
  __publicField(this, "namespaceSnapshots", /* @__PURE__ */ new Map());
3900
+ /**
3901
+ * Miss-path flood protection (READINESS before-scale #1: one cold locale
3902
+ * switch measured ~380 requests in 10 s — every t() miss refired a key GET
3903
+ * and a namespace GET on every re-render, forever on animated pages).
3904
+ */
3905
+ /** GETs currently on the wire, keyed by `url|if-none-match` — identical
3906
+ * concurrent requests share one fetch; each awaiter gets a clone. The
3907
+ * If-None-Match value is part of the key so the 304-without-snapshot
3908
+ * unconditional refetch never coalesces onto a conditional flight. */
3909
+ __publicField(this, "inflightGets", /* @__PURE__ */ new Map());
3910
+ /** URL → 404/423 seen recently: repeats are answered locally (same typed
3911
+ * error, no network) until `until`. 423 honors the server's Retry-After.
3912
+ * Cleared entirely by any successful write — discovery sync keeps its
3913
+ * miss → batch → served-back round-trip. */
3914
+ __publicField(this, "negativeCache", /* @__PURE__ */ new Map());
3915
+ /** One-way latch: a 401 anywhere (or a 403 on a READ) means the key is
3916
+ * wrong or unauthorized for this project, and retrying cannot heal it —
3917
+ * after the first one nothing touches the network again for this
3918
+ * instance (a page reload starts fresh). Holds the status that tripped
3919
+ * it so short-circuits answer honestly. */
3920
+ __publicField(this, "authFailedStatus", null);
3921
+ /** Write-only latch (auth-byok Phase 18): a 403 (scope) or 429 (quota) on
3922
+ * a WRITE degrades this instance to read-only — writes stop, silently
3923
+ * and without retries, while reads keep serving the page. */
3924
+ __publicField(this, "writesFailedStatus", null);
3900
3925
  /**
3901
3926
  * Default endpoint templates with placeholder support
3902
3927
  * Placeholders: :key, :locale, :namespace
@@ -3904,6 +3929,7 @@ var _NetworkRepository = class _NetworkRepository {
3904
3929
  __publicField(this, "DEFAULT_ENDPOINTS", {
3905
3930
  getTranslation: "/api/integrations/translations/:key",
3906
3931
  getNamespace: "/api/integrations/translations/:locale/:namespace",
3932
+ getProjectConfig: "/api/integrations/project-config",
3907
3933
  setTranslation: "/api/integrations/sync/:key",
3908
3934
  setNamespace: "/api/integrations/sync/:locale/:namespace",
3909
3935
  setBatch: "/api/integrations/sync/batch",
@@ -4015,6 +4041,40 @@ var _NetworkRepository = class _NetworkRepository {
4015
4041
  this.handleCaughtError(error);
4016
4042
  }
4017
4043
  }
4044
+ /**
4045
+ * Project-level config for this key — the dashboard's language list,
4046
+ * served under the same trust model as translation reads (docs/20,
4047
+ * companion track). Returns null when the server doesn't expose the
4048
+ * endpoint (404 — older server, feature not deployed) or the payload is
4049
+ * unusable: absence means "keep your local fallback", never an error.
4050
+ */
4051
+ async getProjectConfig() {
4052
+ if (!this.config?.options?.baseUrl) {
4053
+ throw new NetworkError("API base URL is not set");
4054
+ }
4055
+ const url = this.buildUrl("getProjectConfig", {});
4056
+ logger.debug(`API GET ProjectConfig: ${url}`);
4057
+ try {
4058
+ const response = await this.makeRequest(url, "GET");
4059
+ if (response.status === 404) {
4060
+ return null;
4061
+ }
4062
+ if (!response.ok) {
4063
+ this.handleHttpError(response, "project-config");
4064
+ }
4065
+ const data = await response.json();
4066
+ if (!data || typeof data.defaultLocale !== "string" || !Array.isArray(data.languages)) {
4067
+ logger.warn("project-config payload unusable \u2014 keeping local fallback");
4068
+ return null;
4069
+ }
4070
+ return {
4071
+ defaultLocale: data.defaultLocale,
4072
+ languages: data.languages.map(String)
4073
+ };
4074
+ } catch (error) {
4075
+ this.handleCaughtError(error);
4076
+ }
4077
+ }
4018
4078
  /**
4019
4079
  * Shared namespace fetch with the canonical 304 contract:
4020
4080
  * - If-None-Match is sent ONLY when a snapshot (version + payload) is in hand.
@@ -4339,7 +4399,87 @@ var _NetworkRepository = class _NetworkRepository {
4339
4399
  /**
4340
4400
  * Make HTTP request with timeout and headers
4341
4401
  */
4342
- async makeRequest(url, method, body, extraHeaders) {
4402
+ makeRequest(url, method, body, extraHeaders) {
4403
+ if (this.authFailedStatus !== null) {
4404
+ return Promise.resolve(
4405
+ new Response(null, {
4406
+ status: this.authFailedStatus,
4407
+ statusText: `auth latched off after ${this.authFailedStatus}`
4408
+ })
4409
+ );
4410
+ }
4411
+ if (method !== "GET") {
4412
+ if (this.writesFailedStatus !== null) {
4413
+ return Promise.resolve(
4414
+ new Response(null, {
4415
+ status: this.writesFailedStatus,
4416
+ statusText: `writes latched off after ${this.writesFailedStatus}`
4417
+ })
4418
+ );
4419
+ }
4420
+ return this.doFetch(url, method, body, extraHeaders).then(
4421
+ (resp) => this.recordOutcome(url, method, resp)
4422
+ );
4423
+ }
4424
+ const negative = this.negativeCache.get(url);
4425
+ if (negative) {
4426
+ if (Date.now() < negative.until) {
4427
+ logger.debug(`Negative-cache hit (${negative.status}): ${url}`);
4428
+ const headers = negative.status === 423 ? { "Retry-After": String(Math.ceil((negative.until - Date.now()) / 1e3)) } : void 0;
4429
+ return Promise.resolve(
4430
+ new Response(null, { status: negative.status, statusText: "negative cache", headers })
4431
+ );
4432
+ }
4433
+ this.negativeCache.delete(url);
4434
+ }
4435
+ const flightKey = `${url}|${extraHeaders?.["If-None-Match"] ?? ""}`;
4436
+ const existing = this.inflightGets.get(flightKey);
4437
+ if (existing) {
4438
+ return existing.then((resp) => this.cloneResponse(resp));
4439
+ }
4440
+ const flight = this.doFetch(url, method, body, extraHeaders).then((resp) => this.recordOutcome(url, method, resp)).finally(() => this.inflightGets.delete(flightKey));
4441
+ this.inflightGets.set(flightKey, flight);
4442
+ return flight.then((resp) => this.cloneResponse(resp));
4443
+ }
4444
+ /** Bookkeeping on every real (non-synthesized) response — runs once per
4445
+ * physical fetch, never once per coalesced awaiter. */
4446
+ recordOutcome(url, method, response) {
4447
+ const status = response.status;
4448
+ if (status === 401 || status === 403 && method === "GET") {
4449
+ if (this.authFailedStatus === null) {
4450
+ this.authFailedStatus = status;
4451
+ console.warn(
4452
+ `[LionRapid] API key rejected (${status}) \u2014 network translations are disabled for this session. Fix the key and reload; retrying cannot heal an auth failure.`
4453
+ );
4454
+ }
4455
+ return response;
4456
+ }
4457
+ if ((status === 403 || status === 429) && method !== "GET") {
4458
+ if (this.writesFailedStatus === null) {
4459
+ this.writesFailedStatus = status;
4460
+ logger.warn(
4461
+ `Write path disabled after ${status} (scope/quota) \u2014 running read-only`
4462
+ );
4463
+ }
4464
+ return response;
4465
+ }
4466
+ if (method === "GET" && (status === 404 || status === 423)) {
4467
+ const retryAfterSec = status === 423 ? Number(response.headers?.get?.("Retry-After")) : NaN;
4468
+ const ttl = Number.isFinite(retryAfterSec) && retryAfterSec > 0 ? retryAfterSec * 1e3 : this.config.options?.negativeCacheTtlMs ?? _NetworkRepository.DEFAULT_NEGATIVE_TTL_MS;
4469
+ this.negativeCache.set(url, { status, until: Date.now() + ttl });
4470
+ } else if (method !== "GET" && response.ok) {
4471
+ this.negativeCache.clear();
4472
+ } else if (method === "GET" && response.ok) {
4473
+ this.negativeCache.delete(url);
4474
+ }
4475
+ return response;
4476
+ }
4477
+ /** Test doubles (and exotic runtimes) hand us plain objects — only real
4478
+ * Responses need cloning so each coalesced awaiter can read the body. */
4479
+ cloneResponse(response) {
4480
+ return typeof response.clone === "function" ? response.clone() : response;
4481
+ }
4482
+ async doFetch(url, method, body, extraHeaders) {
4343
4483
  const controller = new AbortController();
4344
4484
  const timeout = this.config.options?.timeout ?? 1e4;
4345
4485
  const timeoutId = setTimeout(
@@ -4372,6 +4512,7 @@ __publicField(_NetworkRepository, "SNAPSHOT_PREFIX", "lionrapid:ns:");
4372
4512
  /** Legacy bare-version prefix — a version WITHOUT its payload is exactly the
4373
4513
  * desync the snapshot store exists to prevent; entries are deleted on sight. */
4374
4514
  __publicField(_NetworkRepository, "LEGACY_VERSION_PREFIX", "lionrapid:v:");
4515
+ __publicField(_NetworkRepository, "DEFAULT_NEGATIVE_TTL_MS", 6e4);
4375
4516
  var NetworkRepository = _NetworkRepository;
4376
4517
 
4377
4518
  // src/content/validator/ContentValidator.ts
@@ -4970,9 +5111,11 @@ var LocalStorageRepository = class {
4970
5111
  this.config = {
4971
5112
  enabled: config.enabled ?? true,
4972
5113
  priority: config.priority ?? 0,
5114
+ // Use build sequence by default
4973
5115
  options: {
4974
5116
  prefix: config.options?.prefix ?? STORAGE_PREFIX,
4975
5117
  ttl: config.options?.ttl ?? 24 * 60 * 60 * 1e3,
5118
+ // 24 hours default
4976
5119
  maxSize: config.options?.maxSize ?? 500,
4977
5120
  ...config.options
4978
5121
  }
@@ -4985,6 +5128,10 @@ var LocalStorageRepository = class {
4985
5128
  this.migrateIfNeeded();
4986
5129
  }
4987
5130
  }
5131
+ /**
5132
+ * Check if localStorage is available
5133
+ * Returns false in SSR, private browsing, or when quota is 0
5134
+ */
4988
5135
  checkAvailability() {
4989
5136
  if (typeof window === "undefined") {
4990
5137
  return false;
@@ -4998,6 +5145,9 @@ var LocalStorageRepository = class {
4998
5145
  return false;
4999
5146
  }
5000
5147
  }
5148
+ /**
5149
+ * Handle version migrations - clear cache when version changes
5150
+ */
5001
5151
  migrateIfNeeded() {
5002
5152
  try {
5003
5153
  const storedVersion = window.localStorage.getItem(VERSION_KEY);
@@ -5008,6 +5158,10 @@ var LocalStorageRepository = class {
5008
5158
  } catch {
5009
5159
  }
5010
5160
  }
5161
+ /**
5162
+ * Get a translation value by key
5163
+ * Returns null if not found or expired
5164
+ */
5011
5165
  get(key) {
5012
5166
  if (!this.available) return null;
5013
5167
  try {
@@ -5024,6 +5178,10 @@ var LocalStorageRepository = class {
5024
5178
  return null;
5025
5179
  }
5026
5180
  }
5181
+ /**
5182
+ * Set a translation value
5183
+ * Automatically handles quota exceeded by evicting oldest entries
5184
+ */
5027
5185
  set(key, value) {
5028
5186
  if (!this.available) return;
5029
5187
  const fullKey = this.prefix + key;
@@ -5048,6 +5206,9 @@ var LocalStorageRepository = class {
5048
5206
  }
5049
5207
  }
5050
5208
  }
5209
+ /**
5210
+ * Remove a translation by key
5211
+ */
5051
5212
  remove(key) {
5052
5213
  if (!this.available) return;
5053
5214
  try {
@@ -5055,6 +5216,10 @@ var LocalStorageRepository = class {
5055
5216
  } catch {
5056
5217
  }
5057
5218
  }
5219
+ /**
5220
+ * Clear all LionRapid entries from localStorage
5221
+ * Preserves entries from other applications
5222
+ */
5058
5223
  clear() {
5059
5224
  if (!this.available) return;
5060
5225
  try {
@@ -5069,6 +5234,9 @@ var LocalStorageRepository = class {
5069
5234
  } catch {
5070
5235
  }
5071
5236
  }
5237
+ /**
5238
+ * Clear all entries for a specific namespace
5239
+ */
5072
5240
  clearNamespace(locale, namespace) {
5073
5241
  if (!this.available) return;
5074
5242
  const namespaceCacheKey = `@${locale}:${namespace}`;
@@ -5087,6 +5255,9 @@ var LocalStorageRepository = class {
5087
5255
  } catch {
5088
5256
  }
5089
5257
  }
5258
+ /**
5259
+ * Get namespace translations
5260
+ */
5090
5261
  getNamespace(locale, namespace) {
5091
5262
  const namespaceCacheKey = `@${locale}:${namespace}`;
5092
5263
  const value = this.get(namespaceCacheKey);
@@ -5098,10 +5269,16 @@ var LocalStorageRepository = class {
5098
5269
  return null;
5099
5270
  }
5100
5271
  }
5272
+ /**
5273
+ * Set namespace translations
5274
+ */
5101
5275
  setNamespace(locale, namespace, translations) {
5102
5276
  const namespaceCacheKey = `@${locale}:${namespace}`;
5103
5277
  this.set(namespaceCacheKey, JSON.stringify(translations));
5104
5278
  }
5279
+ /**
5280
+ * Get all cache entries for debugging
5281
+ */
5105
5282
  getEntries() {
5106
5283
  const entries = {};
5107
5284
  if (!this.available) return entries;
@@ -5121,12 +5298,21 @@ var LocalStorageRepository = class {
5121
5298
  }
5122
5299
  return entries;
5123
5300
  }
5301
+ /**
5302
+ * Get repository configuration
5303
+ */
5124
5304
  getConfig() {
5125
5305
  return this.config;
5126
5306
  }
5307
+ /**
5308
+ * Check if localStorage is available
5309
+ */
5127
5310
  isAvailable() {
5128
5311
  return this.available;
5129
5312
  }
5313
+ /**
5314
+ * Get the number of entries in the cache
5315
+ */
5130
5316
  getEntryCount() {
5131
5317
  if (!this.available) return 0;
5132
5318
  let count = 0;
@@ -5141,12 +5327,18 @@ var LocalStorageRepository = class {
5141
5327
  }
5142
5328
  return count;
5143
5329
  }
5330
+ /**
5331
+ * Check if error is a quota exceeded error
5332
+ */
5144
5333
  isQuotaError(error) {
5145
5334
  if (error instanceof DOMException) {
5146
5335
  return error.name === "QuotaExceededError" || error.name === "NS_ERROR_DOM_QUOTA_REACHED";
5147
5336
  }
5148
5337
  return false;
5149
5338
  }
5339
+ /**
5340
+ * Evict oldest 25% of entries when quota is exceeded
5341
+ */
5150
5342
  evictOldest() {
5151
5343
  if (!this.available) return;
5152
5344
  try {
@@ -5174,6 +5366,9 @@ var LocalStorageRepository = class {
5174
5366
  } catch {
5175
5367
  }
5176
5368
  }
5369
+ /**
5370
+ * Get cache statistics
5371
+ */
5177
5372
  getCacheStats() {
5178
5373
  const size = this.getEntryCount();
5179
5374
  const usage = this.maxSize > 0 ? `${Math.round(size / this.maxSize * 100)}%` : "0%";
@@ -5187,7 +5382,7 @@ var LocalStorageRepository = class {
5187
5382
 
5188
5383
  // src/browser/storage/cache-storage-repository.ts
5189
5384
  var CACHE_NAME = "lionrapid-v1";
5190
- var CACHE_PREFIX = "lionrapid:";
5385
+ var CACHE_PREFIX = "https://cache.lionrapid.internal/";
5191
5386
  var CacheStorageRepository = class {
5192
5387
  constructor(config = {}) {
5193
5388
  __publicField(this, "name", "CacheStorageRepository");
@@ -5202,6 +5397,7 @@ var CacheStorageRepository = class {
5202
5397
  options: {
5203
5398
  cacheName: config.options?.cacheName ?? CACHE_NAME,
5204
5399
  ttl: config.options?.ttl ?? 7 * 24 * 60 * 60 * 1e3,
5400
+ // 7 days default
5205
5401
  ...config.options
5206
5402
  }
5207
5403
  };
@@ -5209,9 +5405,17 @@ var CacheStorageRepository = class {
5209
5405
  this.defaultTTL = this.config.options?.ttl ?? 7 * 24 * 60 * 60 * 1e3;
5210
5406
  this.available = this.checkAvailability();
5211
5407
  }
5408
+ /**
5409
+ * Check if Cache API is available
5410
+ * Returns false in SSR, old browsers, or insecure contexts
5411
+ */
5212
5412
  checkAvailability() {
5213
5413
  return typeof caches !== "undefined";
5214
5414
  }
5415
+ /**
5416
+ * Get a translation value by key
5417
+ * @throws {TranslationNotFoundError} When translation doesn't exist or is expired
5418
+ */
5215
5419
  async get(key) {
5216
5420
  if (!this.available) {
5217
5421
  throw new TranslationNotFoundError(key);
@@ -5235,6 +5439,9 @@ var CacheStorageRepository = class {
5235
5439
  throw new TranslationNotFoundError(key);
5236
5440
  }
5237
5441
  }
5442
+ /**
5443
+ * Set a translation value
5444
+ */
5238
5445
  async set(key, value) {
5239
5446
  if (!this.available) return;
5240
5447
  try {
@@ -5253,6 +5460,9 @@ var CacheStorageRepository = class {
5253
5460
  } catch {
5254
5461
  }
5255
5462
  }
5463
+ /**
5464
+ * Remove a translation by key
5465
+ */
5256
5466
  async remove(key) {
5257
5467
  if (!this.available) return;
5258
5468
  try {
@@ -5261,6 +5471,9 @@ var CacheStorageRepository = class {
5261
5471
  } catch {
5262
5472
  }
5263
5473
  }
5474
+ /**
5475
+ * Clear all LionRapid entries from the cache
5476
+ */
5264
5477
  async clear() {
5265
5478
  if (!this.available) return;
5266
5479
  try {
@@ -5272,6 +5485,9 @@ var CacheStorageRepository = class {
5272
5485
  } catch {
5273
5486
  }
5274
5487
  }
5488
+ /**
5489
+ * Clear all entries for a specific namespace
5490
+ */
5275
5491
  async clearNamespace(locale, namespace) {
5276
5492
  if (!this.available) return;
5277
5493
  const namespaceCacheKey = `@${locale}:${namespace}`;
@@ -5289,6 +5505,10 @@ var CacheStorageRepository = class {
5289
5505
  } catch {
5290
5506
  }
5291
5507
  }
5508
+ /**
5509
+ * Get namespace translations
5510
+ * @throws {TranslationNotFoundError} When namespace doesn't exist
5511
+ */
5292
5512
  async getNamespace(locale, namespace) {
5293
5513
  const namespaceCacheKey = `@${locale}:${namespace}`;
5294
5514
  const value = await this.get(namespaceCacheKey);
@@ -5299,16 +5519,29 @@ var CacheStorageRepository = class {
5299
5519
  throw new TranslationNotFoundError(namespaceCacheKey);
5300
5520
  }
5301
5521
  }
5522
+ /**
5523
+ * Set namespace translations
5524
+ */
5302
5525
  async setNamespace(locale, namespace, translations) {
5303
5526
  const namespaceCacheKey = `@${locale}:${namespace}`;
5304
5527
  await this.set(namespaceCacheKey, JSON.stringify(translations));
5305
5528
  }
5529
+ /**
5530
+ * Get repository configuration
5531
+ */
5306
5532
  getConfig() {
5307
5533
  return this.config;
5308
5534
  }
5535
+ /**
5536
+ * Check if Cache API is available
5537
+ */
5309
5538
  isAvailable() {
5310
5539
  return this.available;
5311
5540
  }
5541
+ /**
5542
+ * Get all cached entries (for debugging)
5543
+ * Returns an object mapping keys to values
5544
+ */
5312
5545
  async getEntries() {
5313
5546
  const entries = {};
5314
5547
  if (!this.available) return entries;
@@ -5331,6 +5564,9 @@ var CacheStorageRepository = class {
5331
5564
  }
5332
5565
  return entries;
5333
5566
  }
5567
+ /**
5568
+ * Get cache statistics
5569
+ */
5334
5570
  async getCacheStats() {
5335
5571
  if (!this.available) {
5336
5572
  return { size: 0, cacheName: this.cacheName };