@gyeonghokim/authenticated-image 0.0.0-stage → 1.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 GyeongHo Kim <me@gyeongho.dev>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,311 @@
1
- # Temporary Holding Version
1
+ # authenticated-image
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ > A drop-in `<img>` for images behind `Authorization` headers.
4
+
5
+ [![npm](https://img.shields.io/npm/v/@gyeonghokim/authenticated-image)](https://www.npmjs.com/package/@gyeonghokim/authenticated-image)
6
+ [![bundle size](https://img.shields.io/bundlejs/size/@gyeonghokim/authenticated-image)](https://bundlejs.com/?q=@gyeonghokim/authenticated-image)
7
+ [![release](https://github.com/GyeongHoKim/authenticated-image/actions/workflows/release.yml/badge.svg)](https://github.com/GyeongHoKim/authenticated-image/actions/workflows/release.yml)
8
+ [![license](https://img.shields.io/npm/l/@gyeonghokim/authenticated-image)](LICENSE)
9
+
10
+ `<img src>` can't send an `Authorization` header. `<authenticated-image>` fetches the image with your token, displays it, and cleans up after itself. It is a Web Component, so it works in any framework, and it ships a React wrapper.
11
+
12
+ ## Before / After
13
+
14
+ **Before:** fetch the image yourself and manage the object URL.
15
+
16
+ ```js
17
+ const img = document.querySelector('img');
18
+
19
+ fetch('/api/avatar', { headers: { Authorization: `Bearer ${token}` } })
20
+ .then((res) => {
21
+ if (!res.ok) throw new Error(res.statusText);
22
+ return res.blob();
23
+ })
24
+ .then((blob) => {
25
+ img.src = URL.createObjectURL(blob);
26
+ });
27
+
28
+ // Still to do: abort when src changes, revoke the URL on unmount,
29
+ // lazy-load, dedupe repeated images, show errors...
30
+ ```
31
+
32
+ **After:** configure the token once and use the tag.
33
+
34
+ ```js
35
+ import { configure } from '@gyeonghokim/authenticated-image';
36
+
37
+ configure({
38
+ headers: () => ({ Authorization: `Bearer ${getToken()}` }),
39
+ });
40
+ ```
41
+
42
+ ```html
43
+ <authenticated-image src="/api/avatar" alt="Avatar"></authenticated-image>
44
+ ```
45
+
46
+ ## Install
47
+
48
+ ```sh
49
+ npm install @gyeonghokim/authenticated-image
50
+ # or
51
+ pnpm add @gyeonghokim/authenticated-image
52
+ ```
53
+
54
+ ## Quick start
55
+
56
+ ```js
57
+ // main.js: run once when your app starts
58
+ import { configure } from '@gyeonghokim/authenticated-image';
59
+
60
+ configure({
61
+ headers: async () => ({ Authorization: `Bearer ${await getToken()}` }),
62
+ });
63
+ ```
64
+
65
+ ```html
66
+ <authenticated-image src="/api/invoices/42/scan" alt="Invoice scan" loading="lazy" width="320" height="240">
67
+ <span slot="loading">Loading…</span>
68
+ <span slot="error">Could not load the image.</span>
69
+ </authenticated-image>
70
+ ```
71
+
72
+ Importing the package registers the `<authenticated-image>` tag.
73
+
74
+ ## Features
75
+
76
+ - Sends your `Authorization` header, or loads through your own HTTP client (axios, ky, …).
77
+ - Cancels stale requests when `src` changes and revokes object URLs when removed.
78
+ - `loading="lazy"`, starting 200px before the element enters the viewport.
79
+ - One request for an image shown many times on the page.
80
+ - `loading` and `error` slots, plus a `status` attribute for styling.
81
+ - Presigned URLs, refreshed once if they fail to load.
82
+ - Works in any framework, with a React wrapper included.
83
+
84
+ ## Usage
85
+
86
+ ### React
87
+
88
+ ```tsx
89
+ 'use client';
90
+
91
+ import { useRef } from 'react';
92
+ import {
93
+ AuthenticatedImage,
94
+ type AuthenticatedImageElement,
95
+ } from '@gyeonghokim/authenticated-image/react';
96
+
97
+ export function Avatar({ userId }: { userId: string }) {
98
+ const ref = useRef<AuthenticatedImageElement>(null);
99
+
100
+ return (
101
+ <AuthenticatedImage
102
+ ref={ref}
103
+ src={`/api/users/${userId}/avatar`}
104
+ alt="Avatar"
105
+ loading="lazy"
106
+ onError={(event) => console.warn(event.detail.error.status)}
107
+ >
108
+ <span slot="loading">Loading…</span>
109
+ <button slot="error" type="button" onClick={() => ref.current?.reload()}>
110
+ Retry
111
+ </button>
112
+ </AuthenticatedImage>
113
+ );
114
+ }
115
+ ```
116
+
117
+ `configure` and `clearCache` are also exported from `@gyeonghokim/authenticated-image/react`. Works with React 18 and 19.
118
+
119
+ With SSR, the server renders an empty `<authenticated-image>` tag, and the image loads in the browser after hydration.
120
+
121
+ ### Reuse your HTTP client
122
+
123
+ Pass a `fetcher` to keep your interceptors, token refresh and retries.
124
+
125
+ ```ts
126
+ import axios from 'axios';
127
+ import { configure } from '@gyeonghokim/authenticated-image';
128
+
129
+ const api = axios.create({ baseURL: '/api' }); // your client with auth interceptors
130
+
131
+ configure({
132
+ fetcher: async (src, { signal }) => {
133
+ const response = await api.get<Blob>(src, { responseType: 'blob', signal });
134
+ return response.data;
135
+ },
136
+ });
137
+ ```
138
+
139
+ With [ky](https://github.com/sindresorhus/ky): `fetcher: (src, { signal }) => api.get(src, { signal }).blob()`.
140
+
141
+ ### Presigned URLs
142
+
143
+ Return a string instead of a `Blob`, and it is used as `img.src` as-is.
144
+
145
+ ```ts
146
+ configure({
147
+ fetcher: async (src, { signal }) => {
148
+ const response = await api.get<{ url: string }>(`${src}/presign`, { signal });
149
+ return response.data.url;
150
+ },
151
+ });
152
+ ```
153
+
154
+ If a presigned URL fails to load, for example because it expired, the element asks your `fetcher` for a new one once.
155
+
156
+ ### Loading and error states
157
+
158
+ Use the `loading` and `error` slots, or style the `status` attribute (`idle`, `loading`, `loaded`, `error`).
159
+
160
+ ```css
161
+ authenticated-image[status="error"] {
162
+ outline: 2px solid crimson;
163
+ }
164
+ ```
165
+
166
+ ### Styling
167
+
168
+ The inner `<img>` sizes itself like a normal `<img>`. To fill a fixed-size box:
169
+
170
+ ```css
171
+ authenticated-image {
172
+ width: 64px;
173
+ height: 64px;
174
+ border-radius: 50%;
175
+ overflow: hidden;
176
+ --authenticated-image-object-fit: cover;
177
+ }
178
+
179
+ authenticated-image::part(img) {
180
+ width: 100%;
181
+ height: 100%;
182
+ }
183
+ ```
184
+
185
+ The host is an `inline-grid` that stacks the image and the slots in one cell. If you change its `display`, use `grid` or `inline-grid`.
186
+
187
+ ### Per-element settings
188
+
189
+ Set `fetcher` or `headers` on one element to override the global configuration. These elements don't use the shared cache.
190
+
191
+ ```js
192
+ document.querySelector('#public-preview').headers = {};
193
+ ```
194
+
195
+ Changing `fetcher` or `headers` doesn't refetch, so passing a new inline object on every React render is fine. The new value takes effect on the next load, when `src` changes. Call `reload()` to apply it immediately.
196
+
197
+ ### Logging out
198
+
199
+ ```js
200
+ import { clearCache } from '@gyeonghokim/authenticated-image';
201
+
202
+ await logout();
203
+ clearCache();
204
+ ```
205
+
206
+ Images already on screen stay visible. New elements fetch again. Calling `configure()` also clears the cache.
207
+
208
+ ## API
209
+
210
+ ### Attributes
211
+
212
+ | Name | Type | Default | Description |
213
+ | ----------------- | ----------------------------- | --------- | ----------------------------------------------------------------------- |
214
+ | `src` | `string` | `''` | Image URL. Changing it cancels the current request. |
215
+ | `alt` | `string` | `''` | Alternative text for the inner `<img>`. |
216
+ | `loading` | `'eager' \| 'lazy'` | `'eager'` | `lazy` starts the request 200px before the element enters the viewport. |
217
+ | `width`, `height` | `number` | | Forwarded to the inner `<img>` to reserve space. |
218
+ | `decoding` | `'async' \| 'sync' \| 'auto'` | `'async'` | Forwarded to the inner `<img>`. |
219
+
220
+ ### Properties
221
+
222
+ | Name | Type | Description |
223
+ | ---------- | -------------------------------------------- | ------------------------------------------------------- |
224
+ | `fetcher` | `Fetcher` | Loads this element's image. Bypasses the cache. |
225
+ | `headers` | `HeadersProvider` | Headers for this element's request. Bypasses the cache. |
226
+ | `status` | `'idle' \| 'loading' \| 'loaded' \| 'error'` | Read-only. Mirrored to the `status` attribute. |
227
+ | `error` | `AuthenticatedImageError \| undefined` | Read-only. Why the last load failed. |
228
+ | `reload()` | `() => void` | Loads the image again, skipping the cached copy. |
229
+
230
+ ### Events
231
+
232
+ | Name | Type | When |
233
+ | ------- | ------------------------------------------------- | ---------------------------------- |
234
+ | `load` | `Event` | The image is decoded and rendered. |
235
+ | `error` | `CustomEvent<{ error: AuthenticatedImageError }>` | The request or decoding failed. |
236
+
237
+ ### Slots, parts and custom properties
238
+
239
+ | Name | Kind | Description |
240
+ | ---------------------------------- | --------------- | -------------------------------------------------- |
241
+ | `loading` | slot | Shown while loading or waiting to lazy-load. |
242
+ | `error` | slot | Shown after a failure. |
243
+ | `img` | part | The inner `<img>`. |
244
+ | `--authenticated-image-object-fit` | custom property | `object-fit` of the inner `<img>`. Default `fill`. |
245
+
246
+ ### Functions
247
+
248
+ | Name | Description |
249
+ | ------------------- | ------------------------------------------------------- |
250
+ | `configure(config)` | Replaces the global configuration and clears the cache. |
251
+ | `clearCache()` | Clears the shared cache. |
252
+
253
+ ```ts
254
+ type Fetcher = (src: string, context: { signal: AbortSignal }) => Promise<Blob | string>;
255
+ type HeadersProvider = HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
256
+
257
+ interface AuthenticatedImageConfig {
258
+ fetcher?: Fetcher; // takes precedence over headers
259
+ headers?: HeadersProvider;
260
+ cache?: { retainMs?: number }; // default 30000
261
+ }
262
+
263
+ class AuthenticatedImageError extends Error {
264
+ readonly src: string;
265
+ readonly status?: number; // HTTP status from the default fetch
266
+ // `cause` holds the original error
267
+ }
268
+ ```
269
+
270
+ Also exported: `AuthenticatedImageElement`, `AuthenticatedImageStatus`, `AuthenticatedImageErrorEvent` and `AuthenticatedImageErrorOptions`.
271
+
272
+ Precedence: element `fetcher` › element `headers` › global `fetcher` › global `headers`.
273
+
274
+ ## Security
275
+
276
+ `configure({ headers })` sends your token to every `src`, including other origins. If `src` can come from untrusted input, use a `fetcher` that checks the origin:
277
+
278
+ ```ts
279
+ configure({
280
+ fetcher: async (src, { signal }) => {
281
+ const url = new URL(src, location.href);
282
+ if (url.origin !== location.origin) {
283
+ throw new Error(`Refusing to send the token to ${url.origin}`);
284
+ }
285
+ const response = await fetch(url, {
286
+ headers: { Authorization: `Bearer ${await getToken()}` },
287
+ signal,
288
+ });
289
+ if (!response.ok) throw new Error(`Request failed with status ${response.status}`);
290
+ return response.blob();
291
+ },
292
+ });
293
+ ```
294
+
295
+ A cross-origin API must allow the header through CORS: `Access-Control-Allow-Headers: Authorization`.
296
+
297
+ ## Caching
298
+
299
+ Elements that use the global configuration share an in-memory cache keyed by `src`. An image stays in memory for `retainMs` (30 seconds by default) after the last element using it disconnects, so scrolling back or switching routes is instant. Nothing is written to disk. For caching across page loads, send `Cache-Control: private, max-age=…` from your server, and the browser HTTP cache handles it.
300
+
301
+ ## Browser support
302
+
303
+ Chrome, Edge, Firefox and Safari released in the last two years. CI tests Chromium, Firefox and WebKit.
304
+
305
+ ## Contributing
306
+
307
+ See [CONTRIBUTING.md](CONTRIBUTING.md).
308
+
309
+ ## License
310
+
311
+ [MIT](LICENSE) © GyeongHo Kim
@@ -0,0 +1,117 @@
1
+ import { LitElement, PropertyValues, TemplateResult } from "lit";
2
+ /**
3
+ * Loads an image. Resolve to a `Blob` to display it through an object URL, or to a string
4
+ * (such as a presigned URL) to use it as `img.src` directly.
5
+ */
6
+ type Fetcher = (src: string, context: {
7
+ signal: AbortSignal;
8
+ }) => Promise<Blob | string>;
9
+ /** Headers for the default fetch, or a function that returns them. */
10
+ type HeadersProvider = HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
11
+ /** Global configuration for every `<authenticated-image>`. */
12
+ interface AuthenticatedImageConfig {
13
+ /** Loads images. Takes precedence over `headers`. */
14
+ fetcher?: Fetcher;
15
+ /** Headers sent with the default `fetch`. */
16
+ headers?: HeadersProvider;
17
+ cache?: {
18
+ /**
19
+ * How long an unused image stays in memory, in milliseconds. `0` releases it immediately,
20
+ * and `Infinity` keeps it until the cache is cleared.
21
+ * @defaultValue 30000
22
+ */
23
+ retainMs?: number;
24
+ };
25
+ }
26
+ /**
27
+ * Replaces the global configuration and clears the cache.
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * configure({
32
+ * headers: async () => ({ Authorization: `Bearer ${await getToken()}` }),
33
+ * });
34
+ * ```
35
+ */
36
+ declare function configure(config: AuthenticatedImageConfig): void;
37
+ /**
38
+ * Clears the shared cache, for example on logout. Images on screen stay visible; new
39
+ * elements fetch again.
40
+ */
41
+ declare function clearCache(): void;
42
+ /** Options for {@link AuthenticatedImageError}. */
43
+ interface AuthenticatedImageErrorOptions {
44
+ /** The `src` that failed to load. */
45
+ src: string;
46
+ /** HTTP status code, when the default fetch received a non-2xx response. */
47
+ status?: number;
48
+ /** The original error thrown by `fetch`, a fetcher or a headers function. */
49
+ cause?: unknown;
50
+ }
51
+ /** Describes why an `<authenticated-image>` failed to load. */
52
+ declare class AuthenticatedImageError extends Error {
53
+ readonly name = "AuthenticatedImageError";
54
+ /** The `src` that failed to load. */
55
+ readonly src: string;
56
+ /** HTTP status code, when the default fetch received a non-2xx response. */
57
+ readonly status?: number;
58
+ constructor(message: string, options: AuthenticatedImageErrorOptions);
59
+ }
60
+ /** Loading state of an `<authenticated-image>`. */
61
+ type AuthenticatedImageStatus = 'idle' | 'loading' | 'loaded' | 'error';
62
+ /** The `error` event of `<authenticated-image>`. */
63
+ type AuthenticatedImageErrorEvent = CustomEvent<{
64
+ error: AuthenticatedImageError;
65
+ }>;
66
+ /**
67
+ * An `<img>` that loads its image with `fetch`, so the request can carry an `Authorization`
68
+ * header.
69
+ *
70
+ * @fires load - The image has been decoded and rendered.
71
+ * @fires error - Loading or decoding failed. `event.detail.error` describes why.
72
+ * @slot loading - Shown while the image is loading or waiting to be lazily loaded.
73
+ * @slot error - Shown when the image failed to load.
74
+ * @csspart img - The inner `<img>`.
75
+ * @cssprop --authenticated-image-object-fit - `object-fit` of the inner image. Defaults to `fill`.
76
+ */
77
+ declare class AuthenticatedImageElement extends LitElement {
78
+ static styles: import("lit").CSSResult;
79
+ /** URL of the image. Changing it cancels the current request. */
80
+ src: string;
81
+ /** Alternative text, forwarded to the inner `<img>`. */
82
+ alt: string;
83
+ /** `lazy` waits until the element is within 200px of the viewport before requesting. */
84
+ loading: 'eager' | 'lazy';
85
+ /** Forwarded to the inner `<img>`. */
86
+ decoding: 'async' | 'sync' | 'auto';
87
+ /** Width in pixels, forwarded to the inner `<img>`. */
88
+ width?: number;
89
+ /** Height in pixels, forwarded to the inner `<img>`. */
90
+ height?: number;
91
+ /**
92
+ * Loads this element's image instead of the global configuration. Disables caching. A new
93
+ * value applies on the next load; call `reload()` to apply it right away.
94
+ */
95
+ fetcher?: Fetcher;
96
+ /**
97
+ * Headers for this element's request instead of the global configuration. Disables caching.
98
+ * A new value applies on the next load; call `reload()` to apply it right away.
99
+ */
100
+ headers?: HeadersProvider;
101
+ private readonly _loader;
102
+ /** Current loading state. Mirrored to the `status` attribute. */
103
+ get status(): AuthenticatedImageStatus;
104
+ /** Why the last load failed, or `undefined`. */
105
+ get error(): AuthenticatedImageError | undefined;
106
+ /** Loads the image again. Elements that use the shared cache skip the cached copy. */
107
+ reload(): void;
108
+ protected willUpdate(changed: PropertyValues<this>): void;
109
+ protected updated(): void;
110
+ protected render(): TemplateResult;
111
+ }
112
+ declare global {
113
+ interface HTMLElementTagNameMap {
114
+ 'authenticated-image': AuthenticatedImageElement;
115
+ }
116
+ }
117
+ export { AuthenticatedImageErrorOptions as a, HeadersProvider as c, AuthenticatedImageError as i, clearCache as l, AuthenticatedImageErrorEvent as n, AuthenticatedImageConfig as o, AuthenticatedImageStatus as r, Fetcher as s, AuthenticatedImageElement as t, configure as u };
@@ -0,0 +1,2 @@
1
+ import { a as AuthenticatedImageErrorOptions, c as HeadersProvider, i as AuthenticatedImageError, l as clearCache, n as AuthenticatedImageErrorEvent, o as AuthenticatedImageConfig, r as AuthenticatedImageStatus, s as Fetcher, t as AuthenticatedImageElement, u as configure } from "./index-Cc9WrVMP.mjs";
2
+ export { type AuthenticatedImageConfig, AuthenticatedImageElement, AuthenticatedImageError, type AuthenticatedImageErrorEvent, type AuthenticatedImageErrorOptions, type AuthenticatedImageStatus, type Fetcher, type HeadersProvider, clearCache, configure };
package/dist/index.mjs ADDED
@@ -0,0 +1 @@
1
+ export { t as AuthenticatedImageElement, i as AuthenticatedImageError, n as clearCache, r as configure } from "./src-BMw-CcQI.mjs";
@@ -0,0 +1,16 @@
1
+ import { a as AuthenticatedImageErrorOptions, c as HeadersProvider, i as AuthenticatedImageError, l as clearCache, n as AuthenticatedImageErrorEvent, o as AuthenticatedImageConfig, r as AuthenticatedImageStatus, s as Fetcher, t as AuthenticatedImageElement, u as configure } from "./index-Cc9WrVMP.mjs";
2
+ import { EventName } from "@lit/react";
3
+ /**
4
+ * React component for `<authenticated-image>`. Props map to the element's properties, and
5
+ * `onLoad` / `onError` map to its events. `ref` points to the {@link AuthenticatedImageElement}.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * <AuthenticatedImage src="/api/avatar" alt="Avatar" onError={(e) => console.warn(e.detail.error)} />
10
+ * ```
11
+ */
12
+ export declare const AuthenticatedImage: import("@lit/react").ReactWebComponent<AuthenticatedImageElement, {
13
+ onLoad: EventName<Event>;
14
+ onError: EventName<AuthenticatedImageErrorEvent>;
15
+ }>;
16
+ export { type AuthenticatedImageConfig, type AuthenticatedImageElement, AuthenticatedImageError, type AuthenticatedImageErrorEvent, type AuthenticatedImageErrorOptions, type AuthenticatedImageStatus, type Fetcher, type HeadersProvider, clearCache, configure };
package/dist/react.mjs ADDED
@@ -0,0 +1,25 @@
1
+ "use client";
2
+ import { i as AuthenticatedImageError, n as clearCache, r as configure, t as AuthenticatedImageElement } from "./src-BMw-CcQI.mjs";
3
+ import { createComponent } from "@lit/react";
4
+ import * as React from "react";
5
+ // src/react.ts
6
+ /**
7
+ * React component for `<authenticated-image>`. Props map to the element's properties, and
8
+ * `onLoad` / `onError` map to its events. `ref` points to the {@link AuthenticatedImageElement}.
9
+ *
10
+ * @example
11
+ * ```tsx
12
+ * <AuthenticatedImage src="/api/avatar" alt="Avatar" onError={(e) => console.warn(e.detail.error)} />
13
+ * ```
14
+ */
15
+ const AuthenticatedImage = createComponent({
16
+ tagName: "authenticated-image",
17
+ elementClass: AuthenticatedImageElement,
18
+ react: React,
19
+ events: {
20
+ onLoad: "load",
21
+ onError: "error"
22
+ },
23
+ displayName: "AuthenticatedImage"
24
+ });
25
+ export { AuthenticatedImage, AuthenticatedImageError, clearCache, configure };
@@ -0,0 +1,579 @@
1
+ import { LitElement, css, html, nothing } from "lit";
2
+ import { property } from "lit/decorators.js";
3
+ // src/authenticated-image.styles.ts
4
+ /** Styles of `<authenticated-image>`. */
5
+ const styles = css`
6
+ :host {
7
+ display: inline-grid;
8
+ /* Lets ::part(img) { height: 100% } resolve against a sized host. */
9
+ grid-template: minmax(0, 1fr) / minmax(0, 1fr);
10
+ }
11
+
12
+ img,
13
+ slot {
14
+ grid-area: 1 / 1;
15
+ }
16
+
17
+ img {
18
+ display: block;
19
+ max-width: 100%;
20
+ height: auto;
21
+ object-fit: var(--authenticated-image-object-fit, fill);
22
+ }
23
+
24
+ /* Keep the box so width and height reserve space before the image loads. */
25
+ img[hidden] {
26
+ visibility: hidden;
27
+ }
28
+
29
+ slot {
30
+ display: block;
31
+ }
32
+
33
+ slot[hidden] {
34
+ display: none;
35
+ }
36
+ `;
37
+ // src/errors.ts
38
+ /** Describes why an `<authenticated-image>` failed to load. */
39
+ var AuthenticatedImageError = class extends Error {
40
+ constructor(message, options) {
41
+ super(message, { cause: options.cause });
42
+ this.name = "AuthenticatedImageError";
43
+ this.src = options.src;
44
+ if (options.status !== void 0) this.status = options.status;
45
+ }
46
+ };
47
+ /** Wraps any thrown value in an {@link AuthenticatedImageError}. */
48
+ function toAuthenticatedImageError(src, error) {
49
+ if (error instanceof AuthenticatedImageError) return error;
50
+ return new AuthenticatedImageError(`Failed to load image: ${src}`, {
51
+ src,
52
+ cause: error
53
+ });
54
+ }
55
+ // src/image-cache.ts
56
+ /** Converts a loader result into a displayable URL. */
57
+ function toImageUrl(result) {
58
+ if (typeof result === "string") return {
59
+ url: result,
60
+ isObjectUrl: false
61
+ };
62
+ if (result instanceof Blob) return {
63
+ url: URL.createObjectURL(result),
64
+ isObjectUrl: true
65
+ };
66
+ throw new TypeError("Expected the image loader to resolve to a Blob or a string.");
67
+ }
68
+ /** Revokes the object URL of an image. Strings are left untouched. */
69
+ function revokeImageUrl(image) {
70
+ if (image.isObjectUrl) URL.revokeObjectURL(image.url);
71
+ }
72
+ /** The largest delay `setTimeout` accepts. */
73
+ const MAX_TIMEOUT_MS = 2147483647;
74
+ /** Maps `retainMs` to a timer delay. `Infinity` means the entry never expires. */
75
+ function normalizeRetainMs(retainMs) {
76
+ if (Number.isNaN(retainMs) || retainMs <= 0) return 0;
77
+ if (retainMs > MAX_TIMEOUT_MS) return Number.POSITIVE_INFINITY;
78
+ return retainMs;
79
+ }
80
+ /**
81
+ * In-memory image cache keyed by `src`. Concurrent requests for the same key share one load,
82
+ * and an entry is revoked `retainMs` after its last lease is released. A `retainMs` that is not
83
+ * finite or exceeds the `setTimeout` limit keeps entries until they are detached; `NaN` and
84
+ * negative values mean `0`.
85
+ */
86
+ var ImageCache = class {
87
+ #entries = /* @__PURE__ */ new Map();
88
+ #retainMs;
89
+ constructor(retainMs) {
90
+ this.#retainMs = normalizeRetainMs(retainMs);
91
+ }
92
+ /** Number of entries that new leases can still join. */
93
+ get size() {
94
+ return this.#entries.size;
95
+ }
96
+ setRetainMs(retainMs) {
97
+ this.#retainMs = normalizeRetainMs(retainMs);
98
+ }
99
+ acquire(key, loader) {
100
+ let entry = this.#entries.get(key);
101
+ if (!entry) {
102
+ entry = this.#create(key, loader);
103
+ this.#entries.set(key, entry);
104
+ }
105
+ if (entry.timer !== void 0) {
106
+ clearTimeout(entry.timer);
107
+ entry.timer = void 0;
108
+ }
109
+ entry.refCount++;
110
+ const acquired = entry;
111
+ let released = false;
112
+ return {
113
+ image: acquired.image,
114
+ release: () => {
115
+ if (released) return;
116
+ released = true;
117
+ this.#release(acquired);
118
+ },
119
+ detach: () => {
120
+ if (!acquired.detached) this.#detach(acquired);
121
+ }
122
+ };
123
+ }
124
+ /** Stops sharing every entry. */
125
+ detachAll() {
126
+ for (const entry of [...this.#entries.values()]) this.#detach(entry);
127
+ }
128
+ #create(key, loader) {
129
+ const controller = new AbortController();
130
+ let entry;
131
+ const image = (async () => {
132
+ try {
133
+ const loaded = toImageUrl(await Promise.resolve().then(() => loader(controller.signal)));
134
+ entry.settled = true;
135
+ if (controller.signal.aborted) {
136
+ revokeImageUrl(loaded);
137
+ throw new DOMException("The image request was aborted.", "AbortError");
138
+ }
139
+ entry.loaded = loaded;
140
+ return loaded;
141
+ } catch (error) {
142
+ entry.settled = true;
143
+ this.#detach(entry);
144
+ throw error;
145
+ }
146
+ })();
147
+ image.catch(() => {});
148
+ entry = {
149
+ key,
150
+ controller,
151
+ image,
152
+ refCount: 0,
153
+ settled: false,
154
+ detached: false
155
+ };
156
+ return entry;
157
+ }
158
+ #release(entry) {
159
+ entry.refCount--;
160
+ if (entry.refCount > 0) return;
161
+ if (!entry.settled) {
162
+ entry.controller.abort();
163
+ this.#detach(entry);
164
+ return;
165
+ }
166
+ if (entry.detached || this.#retainMs === 0) {
167
+ this.#dispose(entry);
168
+ return;
169
+ }
170
+ if (this.#retainMs === Number.POSITIVE_INFINITY) return;
171
+ entry.timer = setTimeout(() => {
172
+ entry.timer = void 0;
173
+ this.#dispose(entry);
174
+ }, this.#retainMs);
175
+ }
176
+ #detach(entry) {
177
+ entry.detached = true;
178
+ if (this.#entries.get(entry.key) === entry) this.#entries.delete(entry.key);
179
+ if (entry.refCount === 0) this.#dispose(entry);
180
+ }
181
+ #dispose(entry) {
182
+ if (entry.timer !== void 0) {
183
+ clearTimeout(entry.timer);
184
+ entry.timer = void 0;
185
+ }
186
+ if (this.#entries.get(entry.key) === entry) this.#entries.delete(entry.key);
187
+ if (entry.loaded) {
188
+ revokeImageUrl(entry.loaded);
189
+ entry.loaded = void 0;
190
+ }
191
+ }
192
+ };
193
+ /** The cache shared by elements that use the global configuration. */
194
+ const imageCache = new ImageCache(3e4);
195
+ let globalConfig = {};
196
+ /**
197
+ * Replaces the global configuration and clears the cache.
198
+ *
199
+ * @example
200
+ * ```ts
201
+ * configure({
202
+ * headers: async () => ({ Authorization: `Bearer ${await getToken()}` }),
203
+ * });
204
+ * ```
205
+ */
206
+ function configure(config) {
207
+ globalConfig = { ...config };
208
+ imageCache.setRetainMs(config.cache?.retainMs ?? 3e4);
209
+ imageCache.detachAll();
210
+ }
211
+ /**
212
+ * Clears the shared cache, for example on logout. Images on screen stay visible; new
213
+ * elements fetch again.
214
+ */
215
+ function clearCache() {
216
+ imageCache.detachAll();
217
+ }
218
+ /** Fetches `src` with the given headers and resolves to the response body. */
219
+ async function defaultFetch(src, headers, signal) {
220
+ const init = typeof headers === "function" ? await headers() : headers;
221
+ const response = await fetch(src, {
222
+ headers: init,
223
+ signal,
224
+ credentials: "same-origin"
225
+ });
226
+ if (!response.ok) throw new AuthenticatedImageError(`Request failed with status ${response.status}: ${src}`, {
227
+ src,
228
+ status: response.status
229
+ });
230
+ return response.blob();
231
+ }
232
+ function fromFetcher(fetcher) {
233
+ return (src, signal) => fetcher(src, { signal });
234
+ }
235
+ function fromHeaders(headers) {
236
+ return (src, signal) => defaultFetch(src, headers, signal);
237
+ }
238
+ /**
239
+ * Picks the loader for an element. Precedence: element `fetcher`, element `headers`, global
240
+ * `fetcher`, global `headers`, then a plain fetch.
241
+ */
242
+ function resolveLoader(overrides) {
243
+ if (overrides.fetcher) return {
244
+ load: fromFetcher(overrides.fetcher),
245
+ cacheable: false
246
+ };
247
+ if (overrides.headers !== void 0) return {
248
+ load: fromHeaders(overrides.headers),
249
+ cacheable: false
250
+ };
251
+ if (globalConfig.fetcher) return {
252
+ load: fromFetcher(globalConfig.fetcher),
253
+ cacheable: true
254
+ };
255
+ return {
256
+ load: fromHeaders(globalConfig.headers),
257
+ cacheable: true
258
+ };
259
+ }
260
+ // src/lazy.ts
261
+ /** How far outside the viewport a lazy image starts loading. */
262
+ const LAZY_ROOT_MARGIN = "200px";
263
+ const callbacks = /* @__PURE__ */ new WeakMap();
264
+ let observer;
265
+ function getObserver() {
266
+ observer ??= new IntersectionObserver((entries) => {
267
+ for (const entry of entries) {
268
+ if (!entry.isIntersecting) continue;
269
+ const onNear = callbacks.get(entry.target);
270
+ if (!onNear) continue;
271
+ unobserveLazy(entry.target);
272
+ onNear();
273
+ }
274
+ }, { rootMargin: LAZY_ROOT_MARGIN });
275
+ return observer;
276
+ }
277
+ /** Calls `onNear` once when `element` comes within {@link LAZY_ROOT_MARGIN} of the viewport. */
278
+ function observeLazy(element, onNear) {
279
+ callbacks.set(element, onNear);
280
+ getObserver().observe(element);
281
+ }
282
+ /** Cancels a pending {@link observeLazy} callback. */
283
+ function unobserveLazy(element) {
284
+ callbacks.delete(element);
285
+ observer?.unobserve(element);
286
+ }
287
+ // src/lazy-controller.ts
288
+ /**
289
+ * Runs a callback once the host comes near the viewport, and cancels it when the host
290
+ * disconnects.
291
+ */
292
+ var LazyController = class {
293
+ #host;
294
+ #waiting = false;
295
+ constructor(host) {
296
+ this.#host = host;
297
+ host.addController(this);
298
+ }
299
+ /** Whether a callback is waiting for the host to come near the viewport. */
300
+ get waiting() {
301
+ return this.#waiting;
302
+ }
303
+ /** Calls `callback` once the host comes near the viewport. Replaces a pending callback. */
304
+ whenVisible(callback) {
305
+ this.#waiting = true;
306
+ observeLazy(this.#host, () => {
307
+ this.#waiting = false;
308
+ this.#host.requestUpdate();
309
+ callback();
310
+ });
311
+ this.#host.requestUpdate();
312
+ }
313
+ /** Cancels a pending callback. */
314
+ cancel() {
315
+ if (!this.#waiting) return;
316
+ this.#waiting = false;
317
+ unobserveLazy(this.#host);
318
+ this.#host.requestUpdate();
319
+ }
320
+ hostDisconnected() {
321
+ this.cancel();
322
+ }
323
+ };
324
+ // src/image-loader-controller.ts
325
+ /**
326
+ * Loads an image for its host and owns everything the load holds: the request, the cache lease
327
+ * and the object URL. Starting a new load or disconnecting the host releases them, and results
328
+ * that arrive after that are dropped.
329
+ */
330
+ var ImageLoaderController = class {
331
+ #host;
332
+ #options;
333
+ #lazy;
334
+ #src;
335
+ #lazyLoad;
336
+ #status;
337
+ #url;
338
+ #error;
339
+ #generation;
340
+ #needsStart;
341
+ #retried;
342
+ #controller;
343
+ #lease;
344
+ #ownedImage;
345
+ #image;
346
+ constructor(host, options) {
347
+ this.#src = "";
348
+ this.#lazyLoad = false;
349
+ this.#status = "idle";
350
+ this.#generation = 0;
351
+ this.#needsStart = true;
352
+ this.#retried = false;
353
+ this.handleImageLoad = () => {
354
+ if (!this.#image) return;
355
+ this.#status = "loaded";
356
+ this.#host.requestUpdate();
357
+ this.#options.onLoad();
358
+ };
359
+ this.handleImageError = () => {
360
+ if (!this.#image) return;
361
+ const src = this.#src;
362
+ this.#lease?.detach();
363
+ if (!this.#image.isObjectUrl && !this.#retried) {
364
+ this.#retried = true;
365
+ const generation = this.#generation;
366
+ this.#releaseResources();
367
+ this.#host.requestUpdate();
368
+ this.#host.updateComplete.then(() => this.#load(generation));
369
+ return;
370
+ }
371
+ this.#fail(new AuthenticatedImageError(`Failed to decode image: ${src}`, { src }));
372
+ };
373
+ this.#host = host;
374
+ this.#options = options;
375
+ this.#lazy = new LazyController(host);
376
+ host.addController(this);
377
+ }
378
+ get status() {
379
+ return this.#status;
380
+ }
381
+ /** URL for the inner `<img>`, set once the loader resolves. */
382
+ get url() {
383
+ return this.#url;
384
+ }
385
+ /** Why the last load failed, or `undefined`. */
386
+ get error() {
387
+ return this.#error;
388
+ }
389
+ /** Whether the image is loading or waiting to be lazily loaded. */
390
+ get busy() {
391
+ return this.#status === "loading" || this.#lazy.waiting;
392
+ }
393
+ /**
394
+ * Loads `src` on the host's next update and cancels the current load. With `lazy`, the request
395
+ * waits until the host comes near the viewport.
396
+ */
397
+ load(src, { lazy = false } = {}) {
398
+ this.#src = src;
399
+ this.#lazyLoad = lazy;
400
+ this.#needsStart = true;
401
+ this.#host.requestUpdate();
402
+ }
403
+ /** Loads the image again. A shared cached copy is skipped. */
404
+ reload() {
405
+ this.#lease?.detach();
406
+ if (!this.#host.isConnected) {
407
+ this.#needsStart = true;
408
+ return;
409
+ }
410
+ this.#start();
411
+ }
412
+ hostConnected() {
413
+ this.#needsStart = true;
414
+ this.#host.requestUpdate();
415
+ }
416
+ hostUpdate() {
417
+ if (this.#needsStart && this.#host.isConnected) {
418
+ this.#needsStart = false;
419
+ this.#start();
420
+ }
421
+ }
422
+ hostDisconnected() {
423
+ this.#cleanup();
424
+ this.#status = "idle";
425
+ this.#host.requestUpdate();
426
+ }
427
+ #start() {
428
+ this.#cleanup();
429
+ this.#error = void 0;
430
+ this.#retried = false;
431
+ this.#status = "idle";
432
+ this.#host.requestUpdate();
433
+ if (!this.#src) return;
434
+ const generation = this.#generation;
435
+ if (this.#lazyLoad) {
436
+ this.#lazy.whenVisible(() => void this.#load(generation));
437
+ return;
438
+ }
439
+ this.#load(generation);
440
+ }
441
+ async #load(generation) {
442
+ if (generation !== this.#generation) return;
443
+ const src = this.#src;
444
+ const loader = resolveLoader(this.#options.overrides());
445
+ this.#status = "loading";
446
+ this.#host.requestUpdate();
447
+ let image;
448
+ try {
449
+ if (loader.cacheable) {
450
+ const lease = imageCache.acquire(src, (signal) => loader.load(src, signal));
451
+ this.#lease = lease;
452
+ image = await lease.image;
453
+ } else {
454
+ const controller = new AbortController();
455
+ this.#controller = controller;
456
+ image = toImageUrl(await loader.load(src, controller.signal));
457
+ if (generation !== this.#generation) {
458
+ revokeImageUrl(image);
459
+ return;
460
+ }
461
+ this.#ownedImage = image;
462
+ }
463
+ } catch (error) {
464
+ if (generation === this.#generation) this.#fail(toAuthenticatedImageError(src, error));
465
+ return;
466
+ }
467
+ if (generation !== this.#generation) return;
468
+ this.#image = image;
469
+ this.#url = image.url;
470
+ this.#host.requestUpdate();
471
+ }
472
+ #fail(error) {
473
+ this.#releaseResources();
474
+ this.#error = error;
475
+ this.#status = "error";
476
+ this.#host.requestUpdate();
477
+ this.#options.onError(error);
478
+ }
479
+ #releaseResources() {
480
+ this.#controller?.abort();
481
+ this.#controller = void 0;
482
+ this.#lease?.release();
483
+ this.#lease = void 0;
484
+ if (this.#ownedImage) {
485
+ revokeImageUrl(this.#ownedImage);
486
+ this.#ownedImage = void 0;
487
+ }
488
+ this.#image = void 0;
489
+ this.#url = void 0;
490
+ }
491
+ #cleanup() {
492
+ this.#generation++;
493
+ this.#lazy.cancel();
494
+ this.#releaseResources();
495
+ }
496
+ };
497
+ // \0@oxc-project+runtime@0.155.0/helpers/esm/decorate.js
498
+ function __decorate(decorators, target, key, desc) {
499
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
500
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
501
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
502
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
503
+ }
504
+ // src/authenticated-image.ts
505
+ const RELOAD_TRIGGERS = ["src", "loading"];
506
+ /**
507
+ * An `<img>` that loads its image with `fetch`, so the request can carry an `Authorization`
508
+ * header.
509
+ *
510
+ * @fires load - The image has been decoded and rendered.
511
+ * @fires error - Loading or decoding failed. `event.detail.error` describes why.
512
+ * @slot loading - Shown while the image is loading or waiting to be lazily loaded.
513
+ * @slot error - Shown when the image failed to load.
514
+ * @csspart img - The inner `<img>`.
515
+ * @cssprop --authenticated-image-object-fit - `object-fit` of the inner image. Defaults to `fill`.
516
+ */
517
+ var AuthenticatedImageElement = class extends LitElement {
518
+ constructor(..._args) {
519
+ super(..._args);
520
+ this.src = "";
521
+ this.alt = "";
522
+ this.loading = "eager";
523
+ this.decoding = "async";
524
+ this._loader = new ImageLoaderController(this, {
525
+ overrides: () => this,
526
+ onLoad: () => this.dispatchEvent(new Event("load")),
527
+ onError: (error) => this.dispatchEvent(new CustomEvent("error", { detail: { error } }))
528
+ });
529
+ }
530
+ static {
531
+ this.styles = styles;
532
+ }
533
+ /** Current loading state. Mirrored to the `status` attribute. */
534
+ get status() {
535
+ return this._loader.status;
536
+ }
537
+ /** Why the last load failed, or `undefined`. */
538
+ get error() {
539
+ return this._loader.error;
540
+ }
541
+ /** Loads the image again. Elements that use the shared cache skip the cached copy. */
542
+ reload() {
543
+ this._loader.reload();
544
+ }
545
+ willUpdate(changed) {
546
+ if (RELOAD_TRIGGERS.some((key) => changed.has(key))) this._loader.load(this.src, { lazy: this.loading === "lazy" });
547
+ }
548
+ updated() {
549
+ this.setAttribute("status", this._loader.status);
550
+ if (this._loader.busy) this.setAttribute("aria-busy", "true");
551
+ else this.removeAttribute("aria-busy");
552
+ }
553
+ render() {
554
+ const loaded = this._loader.status === "loaded";
555
+ return html`<img
556
+ part="img"
557
+ ?hidden=${!loaded}
558
+ src=${this._loader.url ?? nothing}
559
+ alt=${loaded ? this.alt : ""}
560
+ decoding=${this.decoding}
561
+ width=${this.width ?? nothing}
562
+ height=${this.height ?? nothing}
563
+ @load=${this._loader.handleImageLoad}
564
+ @error=${this._loader.handleImageError}
565
+ /><slot name="loading" ?hidden=${!this._loader.busy}></slot
566
+ ><slot name="error" ?hidden=${this._loader.status !== "error"}></slot>`;
567
+ }
568
+ };
569
+ __decorate([property()], AuthenticatedImageElement.prototype, "src", void 0);
570
+ __decorate([property()], AuthenticatedImageElement.prototype, "alt", void 0);
571
+ __decorate([property()], AuthenticatedImageElement.prototype, "loading", void 0);
572
+ __decorate([property()], AuthenticatedImageElement.prototype, "decoding", void 0);
573
+ __decorate([property({ type: Number })], AuthenticatedImageElement.prototype, "width", void 0);
574
+ __decorate([property({ type: Number })], AuthenticatedImageElement.prototype, "height", void 0);
575
+ __decorate([property({ attribute: false })], AuthenticatedImageElement.prototype, "fetcher", void 0);
576
+ __decorate([property({ attribute: false })], AuthenticatedImageElement.prototype, "headers", void 0);
577
+ // src/index.ts
578
+ if (!customElements.get("authenticated-image")) customElements.define("authenticated-image", AuthenticatedImageElement);
579
+ export { AuthenticatedImageError as i, clearCache as n, configure as r, AuthenticatedImageElement as t };
package/package.json CHANGED
@@ -1,6 +1,95 @@
1
1
  {
2
2
  "name": "@gyeonghokim/authenticated-image",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.1",
4
+ "description": "A drop-in <img> for images behind Authorization headers. Web Component + React.",
5
+ "keywords": [
6
+ "web-component",
7
+ "custom-element",
8
+ "lit",
9
+ "image",
10
+ "img",
11
+ "authorization",
12
+ "bearer",
13
+ "jwt",
14
+ "blob",
15
+ "object-url",
16
+ "react",
17
+ "presigned-url"
18
+ ],
19
+ "homepage": "https://github.com/GyeongHoKim/authenticated-image#readme",
20
+ "bugs": "https://github.com/GyeongHoKim/authenticated-image/issues",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/GyeongHoKim/authenticated-image.git"
24
+ },
25
+ "license": "MIT",
26
+ "author": "GyeongHo Kim <me@gyeongho.dev>",
27
+ "type": "module",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.mts",
31
+ "default": "./dist/index.mjs"
32
+ },
33
+ "./react": {
34
+ "types": "./dist/react.d.mts",
35
+ "default": "./dist/react.mjs"
36
+ },
37
+ "./package.json": "./package.json"
38
+ },
39
+ "files": [
40
+ "dist"
41
+ ],
42
+ "sideEffects": [
43
+ "./dist/*.mjs"
44
+ ],
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "packageManager": "pnpm@12.14.0",
49
+ "scripts": {
50
+ "dev": "vite",
51
+ "build": "tsdown",
52
+ "typecheck": "tsc --noEmit",
53
+ "typecheck:dist": "tsc -p tsconfig.dist-check.json",
54
+ "test": "vitest run",
55
+ "test:watch": "vitest",
56
+ "check": "biome check",
57
+ "format": "biome check --write",
58
+ "lint:package": "publint --strict",
59
+ "prepare": "husky"
60
+ },
61
+ "peerDependencies": {
62
+ "react": "^18 || ^19"
63
+ },
64
+ "peerDependenciesMeta": {
65
+ "react": {
66
+ "optional": true
67
+ }
68
+ },
69
+ "devDependencies": {
70
+ "@biomejs/biome": "2.6.4",
71
+ "@commitlint/cli": "^21.2.4",
72
+ "@commitlint/config-conventional": "^21.2.4",
73
+ "@semantic-release/changelog": "^7.0.0",
74
+ "@semantic-release/git": "^11.0.1",
75
+ "@types/node": "^26.6.6",
76
+ "@types/react": "^19.3.0",
77
+ "@types/react-dom": "^19.3.0",
78
+ "@vitest/browser-playwright": "^5.0.5",
79
+ "conventional-changelog-conventionalcommits": "^9.3.1",
80
+ "husky": "^9.1.7",
81
+ "playwright": "^1.64.1",
82
+ "publint": "^0.3.25",
83
+ "react": "^19.3.1",
84
+ "react-dom": "^19.3.1",
85
+ "semantic-release": "^25.0.9",
86
+ "tsdown": "~0.23.3",
87
+ "typescript": "^7.0.2",
88
+ "vite": "^8.3.5",
89
+ "vitest": "^5.0.5"
90
+ },
91
+ "dependencies": {
92
+ "@lit/react": "^1.0.8",
93
+ "lit": "^3.3.3"
94
+ }
95
+ }