@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
@@ -3977,6 +3977,31 @@ var LionRapid = (() => {
3977
3977
  * and payload can never desync because they live and die together.
3978
3978
  */
3979
3979
  __publicField(this, "namespaceSnapshots", /* @__PURE__ */ new Map());
3980
+ /**
3981
+ * Miss-path flood protection (READINESS before-scale #1: one cold locale
3982
+ * switch measured ~380 requests in 10 s — every t() miss refired a key GET
3983
+ * and a namespace GET on every re-render, forever on animated pages).
3984
+ */
3985
+ /** GETs currently on the wire, keyed by `url|if-none-match` — identical
3986
+ * concurrent requests share one fetch; each awaiter gets a clone. The
3987
+ * If-None-Match value is part of the key so the 304-without-snapshot
3988
+ * unconditional refetch never coalesces onto a conditional flight. */
3989
+ __publicField(this, "inflightGets", /* @__PURE__ */ new Map());
3990
+ /** URL → 404/423 seen recently: repeats are answered locally (same typed
3991
+ * error, no network) until `until`. 423 honors the server's Retry-After.
3992
+ * Cleared entirely by any successful write — discovery sync keeps its
3993
+ * miss → batch → served-back round-trip. */
3994
+ __publicField(this, "negativeCache", /* @__PURE__ */ new Map());
3995
+ /** One-way latch: a 401 anywhere (or a 403 on a READ) means the key is
3996
+ * wrong or unauthorized for this project, and retrying cannot heal it —
3997
+ * after the first one nothing touches the network again for this
3998
+ * instance (a page reload starts fresh). Holds the status that tripped
3999
+ * it so short-circuits answer honestly. */
4000
+ __publicField(this, "authFailedStatus", null);
4001
+ /** Write-only latch (auth-byok Phase 18): a 403 (scope) or 429 (quota) on
4002
+ * a WRITE degrades this instance to read-only — writes stop, silently
4003
+ * and without retries, while reads keep serving the page. */
4004
+ __publicField(this, "writesFailedStatus", null);
3980
4005
  /**
3981
4006
  * Default endpoint templates with placeholder support
3982
4007
  * Placeholders: :key, :locale, :namespace
@@ -3984,6 +4009,7 @@ var LionRapid = (() => {
3984
4009
  __publicField(this, "DEFAULT_ENDPOINTS", {
3985
4010
  getTranslation: "/api/integrations/translations/:key",
3986
4011
  getNamespace: "/api/integrations/translations/:locale/:namespace",
4012
+ getProjectConfig: "/api/integrations/project-config",
3987
4013
  setTranslation: "/api/integrations/sync/:key",
3988
4014
  setNamespace: "/api/integrations/sync/:locale/:namespace",
3989
4015
  setBatch: "/api/integrations/sync/batch",
@@ -4095,6 +4121,40 @@ var LionRapid = (() => {
4095
4121
  this.handleCaughtError(error);
4096
4122
  }
4097
4123
  }
4124
+ /**
4125
+ * Project-level config for this key — the dashboard's language list,
4126
+ * served under the same trust model as translation reads (docs/20,
4127
+ * companion track). Returns null when the server doesn't expose the
4128
+ * endpoint (404 — older server, feature not deployed) or the payload is
4129
+ * unusable: absence means "keep your local fallback", never an error.
4130
+ */
4131
+ async getProjectConfig() {
4132
+ if (!this.config?.options?.baseUrl) {
4133
+ throw new NetworkError("API base URL is not set");
4134
+ }
4135
+ const url = this.buildUrl("getProjectConfig", {});
4136
+ logger.debug(`API GET ProjectConfig: ${url}`);
4137
+ try {
4138
+ const response = await this.makeRequest(url, "GET");
4139
+ if (response.status === 404) {
4140
+ return null;
4141
+ }
4142
+ if (!response.ok) {
4143
+ this.handleHttpError(response, "project-config");
4144
+ }
4145
+ const data = await response.json();
4146
+ if (!data || typeof data.defaultLocale !== "string" || !Array.isArray(data.languages)) {
4147
+ logger.warn("project-config payload unusable \u2014 keeping local fallback");
4148
+ return null;
4149
+ }
4150
+ return {
4151
+ defaultLocale: data.defaultLocale,
4152
+ languages: data.languages.map(String)
4153
+ };
4154
+ } catch (error) {
4155
+ this.handleCaughtError(error);
4156
+ }
4157
+ }
4098
4158
  /**
4099
4159
  * Shared namespace fetch with the canonical 304 contract:
4100
4160
  * - If-None-Match is sent ONLY when a snapshot (version + payload) is in hand.
@@ -4419,7 +4479,87 @@ var LionRapid = (() => {
4419
4479
  /**
4420
4480
  * Make HTTP request with timeout and headers
4421
4481
  */
4422
- async makeRequest(url, method, body, extraHeaders) {
4482
+ makeRequest(url, method, body, extraHeaders) {
4483
+ if (this.authFailedStatus !== null) {
4484
+ return Promise.resolve(
4485
+ new Response(null, {
4486
+ status: this.authFailedStatus,
4487
+ statusText: `auth latched off after ${this.authFailedStatus}`
4488
+ })
4489
+ );
4490
+ }
4491
+ if (method !== "GET") {
4492
+ if (this.writesFailedStatus !== null) {
4493
+ return Promise.resolve(
4494
+ new Response(null, {
4495
+ status: this.writesFailedStatus,
4496
+ statusText: `writes latched off after ${this.writesFailedStatus}`
4497
+ })
4498
+ );
4499
+ }
4500
+ return this.doFetch(url, method, body, extraHeaders).then(
4501
+ (resp) => this.recordOutcome(url, method, resp)
4502
+ );
4503
+ }
4504
+ const negative = this.negativeCache.get(url);
4505
+ if (negative) {
4506
+ if (Date.now() < negative.until) {
4507
+ logger.debug(`Negative-cache hit (${negative.status}): ${url}`);
4508
+ const headers = negative.status === 423 ? { "Retry-After": String(Math.ceil((negative.until - Date.now()) / 1e3)) } : void 0;
4509
+ return Promise.resolve(
4510
+ new Response(null, { status: negative.status, statusText: "negative cache", headers })
4511
+ );
4512
+ }
4513
+ this.negativeCache.delete(url);
4514
+ }
4515
+ const flightKey = `${url}|${extraHeaders?.["If-None-Match"] ?? ""}`;
4516
+ const existing = this.inflightGets.get(flightKey);
4517
+ if (existing) {
4518
+ return existing.then((resp) => this.cloneResponse(resp));
4519
+ }
4520
+ const flight = this.doFetch(url, method, body, extraHeaders).then((resp) => this.recordOutcome(url, method, resp)).finally(() => this.inflightGets.delete(flightKey));
4521
+ this.inflightGets.set(flightKey, flight);
4522
+ return flight.then((resp) => this.cloneResponse(resp));
4523
+ }
4524
+ /** Bookkeeping on every real (non-synthesized) response — runs once per
4525
+ * physical fetch, never once per coalesced awaiter. */
4526
+ recordOutcome(url, method, response) {
4527
+ const status = response.status;
4528
+ if (status === 401 || status === 403 && method === "GET") {
4529
+ if (this.authFailedStatus === null) {
4530
+ this.authFailedStatus = status;
4531
+ console.warn(
4532
+ `[LionRapid] API key rejected (${status}) \u2014 network translations are disabled for this session. Fix the key and reload; retrying cannot heal an auth failure.`
4533
+ );
4534
+ }
4535
+ return response;
4536
+ }
4537
+ if ((status === 403 || status === 429) && method !== "GET") {
4538
+ if (this.writesFailedStatus === null) {
4539
+ this.writesFailedStatus = status;
4540
+ logger.warn(
4541
+ `Write path disabled after ${status} (scope/quota) \u2014 running read-only`
4542
+ );
4543
+ }
4544
+ return response;
4545
+ }
4546
+ if (method === "GET" && (status === 404 || status === 423)) {
4547
+ const retryAfterSec = status === 423 ? Number(response.headers?.get?.("Retry-After")) : NaN;
4548
+ const ttl = Number.isFinite(retryAfterSec) && retryAfterSec > 0 ? retryAfterSec * 1e3 : this.config.options?.negativeCacheTtlMs ?? _NetworkRepository.DEFAULT_NEGATIVE_TTL_MS;
4549
+ this.negativeCache.set(url, { status, until: Date.now() + ttl });
4550
+ } else if (method !== "GET" && response.ok) {
4551
+ this.negativeCache.clear();
4552
+ } else if (method === "GET" && response.ok) {
4553
+ this.negativeCache.delete(url);
4554
+ }
4555
+ return response;
4556
+ }
4557
+ /** Test doubles (and exotic runtimes) hand us plain objects — only real
4558
+ * Responses need cloning so each coalesced awaiter can read the body. */
4559
+ cloneResponse(response) {
4560
+ return typeof response.clone === "function" ? response.clone() : response;
4561
+ }
4562
+ async doFetch(url, method, body, extraHeaders) {
4423
4563
  const controller = new AbortController();
4424
4564
  const timeout = this.config.options?.timeout ?? 1e4;
4425
4565
  const timeoutId = setTimeout(
@@ -4452,6 +4592,7 @@ var LionRapid = (() => {
4452
4592
  /** Legacy bare-version prefix — a version WITHOUT its payload is exactly the
4453
4593
  * desync the snapshot store exists to prevent; entries are deleted on sight. */
4454
4594
  __publicField(_NetworkRepository, "LEGACY_VERSION_PREFIX", "lionrapid:v:");
4595
+ __publicField(_NetworkRepository, "DEFAULT_NEGATIVE_TTL_MS", 6e4);
4455
4596
  var NetworkRepository = _NetworkRepository;
4456
4597
 
4457
4598
  // src/content/validator/ContentValidator.ts
@@ -5050,9 +5191,11 @@ var LionRapid = (() => {
5050
5191
  this.config = {
5051
5192
  enabled: config.enabled ?? true,
5052
5193
  priority: config.priority ?? 0,
5194
+ // Use build sequence by default
5053
5195
  options: {
5054
5196
  prefix: config.options?.prefix ?? STORAGE_PREFIX,
5055
5197
  ttl: config.options?.ttl ?? 24 * 60 * 60 * 1e3,
5198
+ // 24 hours default
5056
5199
  maxSize: config.options?.maxSize ?? 500,
5057
5200
  ...config.options
5058
5201
  }
@@ -5065,6 +5208,10 @@ var LionRapid = (() => {
5065
5208
  this.migrateIfNeeded();
5066
5209
  }
5067
5210
  }
5211
+ /**
5212
+ * Check if localStorage is available
5213
+ * Returns false in SSR, private browsing, or when quota is 0
5214
+ */
5068
5215
  checkAvailability() {
5069
5216
  if (typeof window === "undefined") {
5070
5217
  return false;
@@ -5078,6 +5225,9 @@ var LionRapid = (() => {
5078
5225
  return false;
5079
5226
  }
5080
5227
  }
5228
+ /**
5229
+ * Handle version migrations - clear cache when version changes
5230
+ */
5081
5231
  migrateIfNeeded() {
5082
5232
  try {
5083
5233
  const storedVersion = window.localStorage.getItem(VERSION_KEY);
@@ -5088,6 +5238,10 @@ var LionRapid = (() => {
5088
5238
  } catch {
5089
5239
  }
5090
5240
  }
5241
+ /**
5242
+ * Get a translation value by key
5243
+ * Returns null if not found or expired
5244
+ */
5091
5245
  get(key) {
5092
5246
  if (!this.available) return null;
5093
5247
  try {
@@ -5104,6 +5258,10 @@ var LionRapid = (() => {
5104
5258
  return null;
5105
5259
  }
5106
5260
  }
5261
+ /**
5262
+ * Set a translation value
5263
+ * Automatically handles quota exceeded by evicting oldest entries
5264
+ */
5107
5265
  set(key, value) {
5108
5266
  if (!this.available) return;
5109
5267
  const fullKey = this.prefix + key;
@@ -5128,6 +5286,9 @@ var LionRapid = (() => {
5128
5286
  }
5129
5287
  }
5130
5288
  }
5289
+ /**
5290
+ * Remove a translation by key
5291
+ */
5131
5292
  remove(key) {
5132
5293
  if (!this.available) return;
5133
5294
  try {
@@ -5135,6 +5296,10 @@ var LionRapid = (() => {
5135
5296
  } catch {
5136
5297
  }
5137
5298
  }
5299
+ /**
5300
+ * Clear all LionRapid entries from localStorage
5301
+ * Preserves entries from other applications
5302
+ */
5138
5303
  clear() {
5139
5304
  if (!this.available) return;
5140
5305
  try {
@@ -5149,6 +5314,9 @@ var LionRapid = (() => {
5149
5314
  } catch {
5150
5315
  }
5151
5316
  }
5317
+ /**
5318
+ * Clear all entries for a specific namespace
5319
+ */
5152
5320
  clearNamespace(locale, namespace) {
5153
5321
  if (!this.available) return;
5154
5322
  const namespaceCacheKey = `@${locale}:${namespace}`;
@@ -5167,6 +5335,9 @@ var LionRapid = (() => {
5167
5335
  } catch {
5168
5336
  }
5169
5337
  }
5338
+ /**
5339
+ * Get namespace translations
5340
+ */
5170
5341
  getNamespace(locale, namespace) {
5171
5342
  const namespaceCacheKey = `@${locale}:${namespace}`;
5172
5343
  const value = this.get(namespaceCacheKey);
@@ -5178,10 +5349,16 @@ var LionRapid = (() => {
5178
5349
  return null;
5179
5350
  }
5180
5351
  }
5352
+ /**
5353
+ * Set namespace translations
5354
+ */
5181
5355
  setNamespace(locale, namespace, translations) {
5182
5356
  const namespaceCacheKey = `@${locale}:${namespace}`;
5183
5357
  this.set(namespaceCacheKey, JSON.stringify(translations));
5184
5358
  }
5359
+ /**
5360
+ * Get all cache entries for debugging
5361
+ */
5185
5362
  getEntries() {
5186
5363
  const entries = {};
5187
5364
  if (!this.available) return entries;
@@ -5201,12 +5378,21 @@ var LionRapid = (() => {
5201
5378
  }
5202
5379
  return entries;
5203
5380
  }
5381
+ /**
5382
+ * Get repository configuration
5383
+ */
5204
5384
  getConfig() {
5205
5385
  return this.config;
5206
5386
  }
5387
+ /**
5388
+ * Check if localStorage is available
5389
+ */
5207
5390
  isAvailable() {
5208
5391
  return this.available;
5209
5392
  }
5393
+ /**
5394
+ * Get the number of entries in the cache
5395
+ */
5210
5396
  getEntryCount() {
5211
5397
  if (!this.available) return 0;
5212
5398
  let count = 0;
@@ -5221,12 +5407,18 @@ var LionRapid = (() => {
5221
5407
  }
5222
5408
  return count;
5223
5409
  }
5410
+ /**
5411
+ * Check if error is a quota exceeded error
5412
+ */
5224
5413
  isQuotaError(error) {
5225
5414
  if (error instanceof DOMException) {
5226
5415
  return error.name === "QuotaExceededError" || error.name === "NS_ERROR_DOM_QUOTA_REACHED";
5227
5416
  }
5228
5417
  return false;
5229
5418
  }
5419
+ /**
5420
+ * Evict oldest 25% of entries when quota is exceeded
5421
+ */
5230
5422
  evictOldest() {
5231
5423
  if (!this.available) return;
5232
5424
  try {
@@ -5254,6 +5446,9 @@ var LionRapid = (() => {
5254
5446
  } catch {
5255
5447
  }
5256
5448
  }
5449
+ /**
5450
+ * Get cache statistics
5451
+ */
5257
5452
  getCacheStats() {
5258
5453
  const size = this.getEntryCount();
5259
5454
  const usage = this.maxSize > 0 ? `${Math.round(size / this.maxSize * 100)}%` : "0%";
@@ -5267,7 +5462,7 @@ var LionRapid = (() => {
5267
5462
 
5268
5463
  // src/browser/storage/cache-storage-repository.ts
5269
5464
  var CACHE_NAME = "lionrapid-v1";
5270
- var CACHE_PREFIX = "lionrapid:";
5465
+ var CACHE_PREFIX = "https://cache.lionrapid.internal/";
5271
5466
  var CacheStorageRepository = class {
5272
5467
  constructor(config = {}) {
5273
5468
  __publicField(this, "name", "CacheStorageRepository");
@@ -5282,6 +5477,7 @@ var LionRapid = (() => {
5282
5477
  options: {
5283
5478
  cacheName: config.options?.cacheName ?? CACHE_NAME,
5284
5479
  ttl: config.options?.ttl ?? 7 * 24 * 60 * 60 * 1e3,
5480
+ // 7 days default
5285
5481
  ...config.options
5286
5482
  }
5287
5483
  };
@@ -5289,9 +5485,17 @@ var LionRapid = (() => {
5289
5485
  this.defaultTTL = this.config.options?.ttl ?? 7 * 24 * 60 * 60 * 1e3;
5290
5486
  this.available = this.checkAvailability();
5291
5487
  }
5488
+ /**
5489
+ * Check if Cache API is available
5490
+ * Returns false in SSR, old browsers, or insecure contexts
5491
+ */
5292
5492
  checkAvailability() {
5293
5493
  return typeof caches !== "undefined";
5294
5494
  }
5495
+ /**
5496
+ * Get a translation value by key
5497
+ * @throws {TranslationNotFoundError} When translation doesn't exist or is expired
5498
+ */
5295
5499
  async get(key) {
5296
5500
  if (!this.available) {
5297
5501
  throw new TranslationNotFoundError(key);
@@ -5315,6 +5519,9 @@ var LionRapid = (() => {
5315
5519
  throw new TranslationNotFoundError(key);
5316
5520
  }
5317
5521
  }
5522
+ /**
5523
+ * Set a translation value
5524
+ */
5318
5525
  async set(key, value) {
5319
5526
  if (!this.available) return;
5320
5527
  try {
@@ -5333,6 +5540,9 @@ var LionRapid = (() => {
5333
5540
  } catch {
5334
5541
  }
5335
5542
  }
5543
+ /**
5544
+ * Remove a translation by key
5545
+ */
5336
5546
  async remove(key) {
5337
5547
  if (!this.available) return;
5338
5548
  try {
@@ -5341,6 +5551,9 @@ var LionRapid = (() => {
5341
5551
  } catch {
5342
5552
  }
5343
5553
  }
5554
+ /**
5555
+ * Clear all LionRapid entries from the cache
5556
+ */
5344
5557
  async clear() {
5345
5558
  if (!this.available) return;
5346
5559
  try {
@@ -5352,6 +5565,9 @@ var LionRapid = (() => {
5352
5565
  } catch {
5353
5566
  }
5354
5567
  }
5568
+ /**
5569
+ * Clear all entries for a specific namespace
5570
+ */
5355
5571
  async clearNamespace(locale, namespace) {
5356
5572
  if (!this.available) return;
5357
5573
  const namespaceCacheKey = `@${locale}:${namespace}`;
@@ -5369,6 +5585,10 @@ var LionRapid = (() => {
5369
5585
  } catch {
5370
5586
  }
5371
5587
  }
5588
+ /**
5589
+ * Get namespace translations
5590
+ * @throws {TranslationNotFoundError} When namespace doesn't exist
5591
+ */
5372
5592
  async getNamespace(locale, namespace) {
5373
5593
  const namespaceCacheKey = `@${locale}:${namespace}`;
5374
5594
  const value = await this.get(namespaceCacheKey);
@@ -5379,16 +5599,29 @@ var LionRapid = (() => {
5379
5599
  throw new TranslationNotFoundError(namespaceCacheKey);
5380
5600
  }
5381
5601
  }
5602
+ /**
5603
+ * Set namespace translations
5604
+ */
5382
5605
  async setNamespace(locale, namespace, translations) {
5383
5606
  const namespaceCacheKey = `@${locale}:${namespace}`;
5384
5607
  await this.set(namespaceCacheKey, JSON.stringify(translations));
5385
5608
  }
5609
+ /**
5610
+ * Get repository configuration
5611
+ */
5386
5612
  getConfig() {
5387
5613
  return this.config;
5388
5614
  }
5615
+ /**
5616
+ * Check if Cache API is available
5617
+ */
5389
5618
  isAvailable() {
5390
5619
  return this.available;
5391
5620
  }
5621
+ /**
5622
+ * Get all cached entries (for debugging)
5623
+ * Returns an object mapping keys to values
5624
+ */
5392
5625
  async getEntries() {
5393
5626
  const entries = {};
5394
5627
  if (!this.available) return entries;
@@ -5411,6 +5644,9 @@ var LionRapid = (() => {
5411
5644
  }
5412
5645
  return entries;
5413
5646
  }
5647
+ /**
5648
+ * Get cache statistics
5649
+ */
5414
5650
  async getCacheStats() {
5415
5651
  if (!this.available) {
5416
5652
  return { size: 0, cacheName: this.cacheName };