bunnyquery 1.8.8 → 1.8.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bunnyquery",
3
- "version": "1.8.8",
3
+ "version": "1.8.9",
4
4
  "description": "Embeddable BunnyQuery AI chat widget + its framework-agnostic chat engine",
5
5
  "main": "bunnyquery.js",
6
6
  "exports": {
Binary file
@@ -21,6 +21,24 @@ export var LINK_LABEL_MAX_DISPLAY_CHARS = 32;
21
21
  */
22
22
  export var EXPIRED_LINK_REFRESH_EXPIRES_SECONDS = 20 * 60;
23
23
 
24
+ /**
25
+ * Lifetime of the url minted for an inline image PREVIEW.
26
+ *
27
+ * Longer than the click url above, and for a different reason. A click hands the
28
+ * user a url they may keep, so it stays short. A preview url is consumed by the
29
+ * page itself and never leaves it, and it is the ONE lever on how long the
30
+ * downloaded picture stays reusable: get_signed_url will not cache a mint for
31
+ * longer than the credential inside it survives, so `browser_cache` cannot buy
32
+ * local availability that `expires` has not paid for. Twenty minutes meant every
33
+ * image re-downloaded three times an hour of ordinary reading.
34
+ *
35
+ * An hour, giving 55 minutes of cache once the server's five minute headroom is
36
+ * taken off. Short enough that a leaked preview url is not a standing grant, long
37
+ * enough that a conversation does not re-fetch its own pictures while the user is
38
+ * still reading it.
39
+ */
40
+ export var PREVIEW_URL_EXPIRES_SECONDS = 60 * 60;
41
+
24
42
  /**
25
43
  * Seconds the browser may reuse a minted preview url (`browser_cache`).
26
44
  *
@@ -30,12 +48,17 @@ export var EXPIRED_LINK_REFRESH_EXPIRES_SECONDS = 20 * 60;
30
48
  * url comes back out of the browser cache, so the body already on disk stays
31
49
  * addressable.
32
50
  *
33
- * Deliberately far longer than EXPIRED_LINK_REFRESH_EXPIRES_SECONDS above, and
34
- * that is the whole trick: the url is short-lived while the file stays available
35
- * locally for a WEEK. What keeps an image painting is the cached BODY, not a live
36
- * url. Once the browser evicts that body it refetches with a url that has since
37
- * expired, gets a 403, and the error path re-mints with `refresh`. That path is
38
- * therefore load-bearing, not a rare fallback.
51
+ * A CEILING, not a promise. get_signed_url caps what it grants at the lifetime of
52
+ * the url inside the response (expires minus headroom, so 15 minutes for the
53
+ * platform's 20 minute url), because a mint cached for longer than its own
54
+ * credential is a guaranteed 403 that the browser keeps serving from its own
55
+ * store. Asking for the week is still right: it says what this client would
56
+ * reuse if the url were stable by construction, and the server decides.
57
+ *
58
+ * What keeps an image painting is the cached BODY, not a live url. Once the
59
+ * browser evicts that body it refetches with a url that has since expired, gets a
60
+ * 403, and the error path re-mints with `refresh` and mintCacheBustStamp. That
61
+ * path is load-bearing, not a rare fallback.
39
62
  *
40
63
  * A week is the platform default for reading a private file, not a number chosen
41
64
  * here: skapi-js reads every private record file with
@@ -60,6 +83,102 @@ export var PREVIEW_BROWSER_CACHE_SECONDS = 7 * 24 * 60 * 60;
60
83
  */
61
84
  export var LINK_REFRESH_WINDOW_MS = (EXPIRED_LINK_REFRESH_EXPIRES_SECONDS - 5 * 60) * 1000;
62
85
 
86
+ /**
87
+ * Cache generation for the mint request url. BUMP THIS to abandon every mint
88
+ * response browsers are currently holding.
89
+ *
90
+ * Generation 2 retires the entries written before 2026-08-11. Those were stored
91
+ * with `max-age=604800` around a presign that dies in twenty minutes, so from
92
+ * minute 21 each one is a guaranteed 403 that the browser keeps serving from its
93
+ * own store for the rest of the week. The server no longer grants a lifetime a
94
+ * url cannot back (get_signed_url resolve_browser_cache), but that fixes what is
95
+ * written from now on and cannot reach what is already stored on a user's
96
+ * device. Changing the url is the only thing that can: an entry nobody requests
97
+ * again is an entry that cannot answer again.
98
+ */
99
+ export var MINT_CACHE_GENERATION = 2;
100
+
101
+ /**
102
+ * Window stamp for a REFRESH mint.
103
+ *
104
+ * WINDOWED, not Date.now(): a per-call stamp is a new cache key per image per
105
+ * retry, which is what made the original `nocache` parameter worse than the
106
+ * disease. One stamp per refresh window means every repair inside those minutes
107
+ * shares a single entry, and it rotates before the url it carries can die.
108
+ */
109
+ export function mintCacheBustStamp(now?: number): number {
110
+ return Math.floor((now == null ? Date.now() : now) / LINK_REFRESH_WINDOW_MS);
111
+ }
112
+
113
+ /**
114
+ * The `nocache` value for a preview mint: the generation, plus a window stamp
115
+ * when this mint is a repair.
116
+ *
117
+ * A repair MUST reach the origin, and the request header the clients used to
118
+ * rely on cannot do it. `Cache-Control: no-cache` is not a CORS-safelisted
119
+ * request header, and the record gateway's preflight answers
120
+ * `Access-Control-Allow-Headers` WITHOUT it (verified against the live api on
121
+ * 2026-08-11), so a mint carrying that header is rejected by the browser before
122
+ * it is ever sent. Every repair therefore failed, in every browser, and the chip
123
+ * went straight to "(unavailable)". Only a phone noticed, because only a phone
124
+ * drops image bodies often enough to need the repair at all.
125
+ *
126
+ * A query parameter has no such problem: it is part of the url, so it needs no
127
+ * preflight and no cooperation from the cache.
128
+ */
129
+ export function previewMintCacheToken(refresh?: boolean): string {
130
+ if (!refresh) return String(MINT_CACHE_GENERATION);
131
+ return MINT_CACHE_GENERATION + '.' + mintCacheBustStamp();
132
+ }
133
+
134
+ /**
135
+ * How long before a presign dies we stop handing it out.
136
+ *
137
+ * A url served with one second left is a 403 with extra steps: the request still
138
+ * has to reach S3, and an image body still has to start arriving.
139
+ */
140
+ export var PRESIGN_SAFETY_MARGIN_MS = 60 * 1000;
141
+
142
+ /**
143
+ * When the url in hand actually dies, read out of the url itself, or null if it
144
+ * carries no expiry we recognise.
145
+ *
146
+ * Every client-side cache here ages a url from the moment it ARRIVED, which is
147
+ * only the same thing as its lifetime when the mint went to the network. Once
148
+ * mint responses are cacheable that assumption breaks: a mint answered from the
149
+ * browser's store can be nearly as old as its own max-age, and the client then
150
+ * adds its own reuse window on top, so a 20 minute credential can be handed to an
151
+ * <img> half an hour after it was signed. Asking the url when it dies removes the
152
+ * stacking instead of trying to budget for it.
153
+ *
154
+ * Both signature versions, because the platform mints SigV2 through the host
155
+ * bucket and SigV4 elsewhere.
156
+ */
157
+ export function presignExpiryEpochMs(url: string): number | null {
158
+ if (!url) return null;
159
+ var q = url.indexOf('?');
160
+ if (q < 0) return null;
161
+ var params: URLSearchParams;
162
+ try { params = new URLSearchParams(url.slice(q + 1)); }
163
+ catch (e) { return null; }
164
+
165
+ // SigV2: Expires is an absolute epoch in seconds.
166
+ var v2 = params.get('Expires');
167
+ if (v2 && /^\d+$/.test(v2)) return parseInt(v2, 10) * 1000;
168
+
169
+ // SigV4: signing time plus a duration.
170
+ var signed = params.get('X-Amz-Date');
171
+ var lifetime = params.get('X-Amz-Expires');
172
+ if (signed && lifetime && /^\d+$/.test(lifetime)) {
173
+ var m = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z$/.exec(signed);
174
+ if (m) {
175
+ var at = Date.UTC(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], +m[6]);
176
+ return at + parseInt(lifetime, 10) * 1000;
177
+ }
178
+ }
179
+ return null;
180
+ }
181
+
63
182
  // The two "balanced parens" groups match ONE CHARACTER per step, never a `+`
64
183
  // run, so each position has exactly one way to be matched: `[^()\n]` cannot
65
184
  // start with `(`, and the nested-paren alternative always does. That disjointness
@@ -545,6 +664,24 @@ export function linkUnavailableKeyForHref(href: string): string {
545
664
  return 'href:' + (href || '');
546
665
  }
547
666
 
667
+ /**
668
+ * Every key a stored file can be marked under, given only its path.
669
+ *
670
+ * Marking writes ONE key (whichever identifier the failing call had) and the
671
+ * lookup ORs all of them, which is fine in one direction and wrong in the other:
672
+ * a view that later learns the file is reachable knows only the path, and
673
+ * clearing `path:` alone leaves a chip greyed by a failed CLICK (which marks
674
+ * `href:` too) exactly as dead as before. The placeholder href is derived from
675
+ * the path, so both keys can be rebuilt from it.
676
+ */
677
+ export function linkUnavailableKeysForPath(remotePath: string): string[] {
678
+ if (!remotePath) return [];
679
+ return [
680
+ linkUnavailableKeyForPath(remotePath),
681
+ linkUnavailableKeyForHref(buildDisplayExpiredAttachmentHref(remotePath)),
682
+ ];
683
+ }
684
+
548
685
  export function isLinkUnavailable(
549
686
  link: { href?: string; expiredHref?: string; remotePath?: string } | null | undefined,
550
687
  map: Record<string, boolean | undefined> | null | undefined,