@lionrapid/core 0.6.1 → 0.6.3

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