tempest-react-sdk 0.80.0 → 0.82.0
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/README.md +40 -10
- package/bin/create-tempest-app.e2e.test.mjs +31 -0
- package/bin/create-tempest-app.mjs +11 -1
- package/bin/lib/component/generate.test.mjs +13 -1
- package/bin/lib/doctor/doctor.e2e.test.mjs +29 -0
- package/bin/lib/doctor/vitest.mjs +120 -0
- package/bin/lib/doctor/vitest.test.mjs +139 -0
- package/bin/lib/fs/contain.mjs +7 -0
- package/bin/lib/openapi/escape.mjs +68 -4
- package/bin/lib/openapi/generate.hostile.test.mjs +150 -1
- package/bin/lib/openapi/generate.mjs +78 -13
- package/bin/lib/openapi/generate.test.mjs +13 -1
- package/bin/lib/openapi/pattern-risk.mjs +134 -0
- package/bin/lib/openapi/pattern-risk.test.mjs +45 -0
- package/bin/tempest-gen.test.mjs +100 -0
- package/bin/tempest.mjs +24 -8
- package/dist/access/use-can.cjs +1 -1
- package/dist/access/use-can.cjs.map +1 -1
- package/dist/access/use-can.js +25 -28
- package/dist/access/use-can.js.map +1 -1
- package/dist/auth/create-tempest-auth.cjs +1 -1
- package/dist/auth/create-tempest-auth.cjs.map +1 -1
- package/dist/auth/create-tempest-auth.js +234 -122
- package/dist/auth/create-tempest-auth.js.map +1 -1
- package/dist/auth/session-identity.cjs +2 -0
- package/dist/auth/session-identity.cjs.map +1 -0
- package/dist/auth/session-identity.js +12 -0
- package/dist/auth/session-identity.js.map +1 -0
- package/dist/br/boleto.cjs +1 -1
- package/dist/br/boleto.cjs.map +1 -1
- package/dist/br/boleto.js +2 -1
- package/dist/br/boleto.js.map +1 -1
- package/dist/br.d.ts +22 -3
- package/dist/components/AuthForm/AuthFieldControl.cjs +2 -0
- package/dist/components/AuthForm/AuthFieldControl.cjs.map +1 -0
- package/dist/components/AuthForm/AuthFieldControl.js +85 -0
- package/dist/components/AuthForm/AuthFieldControl.js.map +1 -0
- package/dist/components/AuthForm/AuthForm.cjs +2 -0
- package/dist/components/AuthForm/AuthForm.cjs.map +1 -0
- package/dist/components/AuthForm/AuthForm.js +83 -0
- package/dist/components/AuthForm/AuthForm.js.map +1 -0
- package/dist/components/AuthForm/AuthForm.module.cjs +3 -0
- package/dist/components/AuthForm/AuthForm.module.cjs.map +1 -0
- package/dist/components/AuthForm/AuthForm.module.js +14 -0
- package/dist/components/AuthForm/AuthForm.module.js.map +1 -0
- package/dist/components/AuthForm/AuthLinks.cjs +2 -0
- package/dist/components/AuthForm/AuthLinks.cjs.map +1 -0
- package/dist/components/AuthForm/AuthLinks.js +32 -0
- package/dist/components/AuthForm/AuthLinks.js.map +1 -0
- package/dist/components/AuthForm/ForgotPasswordForm.cjs +2 -0
- package/dist/components/AuthForm/ForgotPasswordForm.cjs.map +1 -0
- package/dist/components/AuthForm/ForgotPasswordForm.js +32 -0
- package/dist/components/AuthForm/ForgotPasswordForm.js.map +1 -0
- package/dist/components/AuthForm/LoginForm.cjs +2 -0
- package/dist/components/AuthForm/LoginForm.cjs.map +1 -0
- package/dist/components/AuthForm/LoginForm.js +39 -0
- package/dist/components/AuthForm/LoginForm.js.map +1 -0
- package/dist/components/AuthForm/ResetPasswordForm.cjs +2 -0
- package/dist/components/AuthForm/ResetPasswordForm.cjs.map +1 -0
- package/dist/components/AuthForm/ResetPasswordForm.js +39 -0
- package/dist/components/AuthForm/ResetPasswordForm.js.map +1 -0
- package/dist/components/AuthForm/SignUpForm.cjs +2 -0
- package/dist/components/AuthForm/SignUpForm.cjs.map +1 -0
- package/dist/components/AuthForm/SignUpForm.js +32 -0
- package/dist/components/AuthForm/SignUpForm.js.map +1 -0
- package/dist/components/AuthForm/auth-defaults.cjs +2 -0
- package/dist/components/AuthForm/auth-defaults.cjs.map +1 -0
- package/dist/components/AuthForm/auth-defaults.js +54 -0
- package/dist/components/AuthForm/auth-defaults.js.map +1 -0
- package/dist/components/AuthForm/auth-schemas.cjs +2 -0
- package/dist/components/AuthForm/auth-schemas.cjs.map +1 -0
- package/dist/components/AuthForm/auth-schemas.js +26 -0
- package/dist/components/AuthForm/auth-schemas.js.map +1 -0
- package/dist/components/AuthPage/AuthPage.cjs +2 -0
- package/dist/components/AuthPage/AuthPage.cjs.map +1 -0
- package/dist/components/AuthPage/AuthPage.js +97 -0
- package/dist/components/AuthPage/AuthPage.js.map +1 -0
- package/dist/components/AuthPage/AuthPage.module.cjs +3 -0
- package/dist/components/AuthPage/AuthPage.module.cjs.map +1 -0
- package/dist/components/AuthPage/AuthPage.module.js +24 -0
- package/dist/components/AuthPage/AuthPage.module.js.map +1 -0
- package/dist/components/Breadcrumbs/Breadcrumbs.cjs +1 -1
- package/dist/components/Breadcrumbs/Breadcrumbs.cjs.map +1 -1
- package/dist/components/Breadcrumbs/Breadcrumbs.js +22 -21
- package/dist/components/Breadcrumbs/Breadcrumbs.js.map +1 -1
- package/dist/components/Button/Button.cjs +1 -1
- package/dist/components/Button/Button.cjs.map +1 -1
- package/dist/components/Button/Button.js +41 -40
- package/dist/components/Button/Button.js.map +1 -1
- package/dist/components/Chat/ChatAttachments.cjs +1 -1
- package/dist/components/Chat/ChatAttachments.cjs.map +1 -1
- package/dist/components/Chat/ChatAttachments.js +38 -38
- package/dist/components/FilterBar/filter-apply.cjs +1 -1
- package/dist/components/FilterBar/filter-apply.cjs.map +1 -1
- package/dist/components/FilterBar/filter-apply.js +7 -7
- package/dist/components/Form/Form.cjs +1 -1
- package/dist/components/Form/Form.cjs.map +1 -1
- package/dist/components/Form/Form.js +30 -32
- package/dist/components/Form/Form.js.map +1 -1
- package/dist/components/Layout/Layout.cjs +1 -1
- package/dist/components/Layout/Layout.cjs.map +1 -1
- package/dist/components/Layout/Layout.js +25 -24
- package/dist/components/Layout/Layout.js.map +1 -1
- package/dist/components/Layout/gap.cjs +2 -0
- package/dist/components/Layout/gap.cjs.map +1 -0
- package/dist/components/Layout/gap.js +15 -0
- package/dist/components/Layout/gap.js.map +1 -0
- package/dist/components/NavigationMenu/NavigationMenu.cjs +1 -1
- package/dist/components/NavigationMenu/NavigationMenu.cjs.map +1 -1
- package/dist/components/NavigationMenu/NavigationMenu.js +71 -70
- package/dist/components/NavigationMenu/NavigationMenu.js.map +1 -1
- package/dist/components/Sidebar/Sidebar.cjs.map +1 -1
- package/dist/components/Sidebar/Sidebar.js.map +1 -1
- package/dist/components/Sidebar/SidebarEntry.cjs +1 -1
- package/dist/components/Sidebar/SidebarEntry.cjs.map +1 -1
- package/dist/components/Sidebar/SidebarEntry.js +30 -29
- package/dist/components/Sidebar/SidebarEntry.js.map +1 -1
- package/dist/data/create-data-provider.cjs +1 -1
- package/dist/data/create-data-provider.cjs.map +1 -1
- package/dist/data/create-data-provider.js +1 -2
- package/dist/data/create-data-provider.js.map +1 -1
- package/dist/forms/index.js +7 -7
- package/dist/forms/masked-inputs.cjs +1 -1
- package/dist/forms/masked-inputs.cjs.map +1 -1
- package/dist/forms/masked-inputs.js +15 -15
- package/dist/forms/validate-form.cjs +1 -1
- package/dist/forms/validate-form.cjs.map +1 -1
- package/dist/forms/validate-form.js +6 -1
- package/dist/forms/validate-form.js.map +1 -1
- package/dist/forms/zod-resolver.cjs +1 -1
- package/dist/forms/zod-resolver.cjs.map +1 -1
- package/dist/forms/zod-resolver.js +8 -3
- package/dist/forms/zod-resolver.js.map +1 -1
- package/dist/http/api-client.cjs +1 -1
- package/dist/http/api-client.cjs.map +1 -1
- package/dist/http/api-client.js +137 -116
- package/dist/http/api-client.js.map +1 -1
- package/dist/http/credential-scope.cjs +1 -1
- package/dist/http/credential-scope.cjs.map +1 -1
- package/dist/http/credential-scope.js +33 -9
- package/dist/http/credential-scope.js.map +1 -1
- package/dist/http/presigned-post.cjs.map +1 -1
- package/dist/http/presigned-post.js.map +1 -1
- package/dist/http/resumable-upload.cjs +1 -1
- package/dist/http/resumable-upload.cjs.map +1 -1
- package/dist/http/resumable-upload.js +140 -134
- package/dist/http/resumable-upload.js.map +1 -1
- package/dist/http/upload-with-progress.cjs +1 -1
- package/dist/http/upload-with-progress.cjs.map +1 -1
- package/dist/http/upload-with-progress.js +17 -17
- package/dist/http/upload-with-progress.js.map +1 -1
- package/dist/http/use-describe-api-error.cjs +1 -1
- package/dist/http/use-describe-api-error.cjs.map +1 -1
- package/dist/http/use-describe-api-error.js +9 -9
- package/dist/oauth/GoogleSignIn.cjs +1 -1
- package/dist/oauth/GoogleSignIn.cjs.map +1 -1
- package/dist/oauth/GoogleSignIn.js +5 -4
- package/dist/oauth/GoogleSignIn.js.map +1 -1
- package/dist/offline/create-offline-sync.cjs +1 -1
- package/dist/offline/create-offline-sync.cjs.map +1 -1
- package/dist/offline/create-offline-sync.js +108 -68
- package/dist/offline/create-offline-sync.js.map +1 -1
- package/dist/query/offline-persistence.cjs +1 -1
- package/dist/query/offline-persistence.cjs.map +1 -1
- package/dist/query/offline-persistence.js +54 -44
- package/dist/query/offline-persistence.js.map +1 -1
- package/dist/sse/create-event-stream.cjs +1 -1
- package/dist/sse/create-event-stream.cjs.map +1 -1
- package/dist/sse/create-event-stream.js +48 -41
- package/dist/sse/create-event-stream.js.map +1 -1
- package/dist/sse/ticket-event-stream.cjs +1 -1
- package/dist/sse/ticket-event-stream.cjs.map +1 -1
- package/dist/sse/ticket-event-stream.js +48 -19
- package/dist/sse/ticket-event-stream.js.map +1 -1
- package/dist/sse/use-event-stream.cjs +1 -1
- package/dist/sse/use-event-stream.cjs.map +1 -1
- package/dist/sse/use-event-stream.js +19 -14
- package/dist/sse/use-event-stream.js.map +1 -1
- package/dist/styles/AuthForm.css +2 -0
- package/dist/styles/AuthPage.css +2 -0
- package/dist/styles/component/AuthForm.css +9 -0
- package/dist/styles/component/AuthPage.css +29 -0
- package/dist/styles/forms.css +36 -0
- package/dist/styles/manifest.json +63 -0
- package/dist/styles.css +1 -1
- package/dist/sw/background-sync.cjs +1 -1
- package/dist/sw/background-sync.cjs.map +1 -1
- package/dist/sw/background-sync.js +132 -112
- package/dist/sw/background-sync.js.map +1 -1
- package/dist/sw/cache.cjs +1 -1
- package/dist/sw/cache.cjs.map +1 -1
- package/dist/sw/cache.js +36 -30
- package/dist/sw/cache.js.map +1 -1
- package/dist/sw/create-push-handler.cjs +1 -1
- package/dist/sw/create-push-handler.cjs.map +1 -1
- package/dist/sw/create-push-handler.js +20 -10
- package/dist/sw/create-push-handler.js.map +1 -1
- package/dist/sw.d.ts +93 -28
- package/dist/tabular/compact.cjs +1 -1
- package/dist/tabular/compact.cjs.map +1 -1
- package/dist/tabular/compact.js +35 -16
- package/dist/tabular/compact.js.map +1 -1
- package/dist/tabular/manifest.cjs +1 -1
- package/dist/tabular/manifest.cjs.map +1 -1
- package/dist/tabular/manifest.js +33 -15
- package/dist/tabular/manifest.js.map +1 -1
- package/dist/tabular.d.ts +11 -1
- package/dist/tempest-react-sdk.cjs +1 -1
- package/dist/tempest-react-sdk.d.ts +1057 -83
- package/dist/tempest-react-sdk.js +373 -365
- package/dist/theme/initial-theme.cjs +4 -4
- package/dist/theme/initial-theme.cjs.map +1 -1
- package/dist/theme/initial-theme.js +8 -5
- package/dist/theme/initial-theme.js.map +1 -1
- package/dist/utils/compressed-storage.cjs +1 -1
- package/dist/utils/compressed-storage.cjs.map +1 -1
- package/dist/utils/compressed-storage.js +13 -7
- package/dist/utils/compressed-storage.js.map +1 -1
- package/dist/utils/format.cjs +1 -1
- package/dist/utils/format.cjs.map +1 -1
- package/dist/utils/format.js +25 -25
- package/dist/utils/numbers.cjs +1 -1
- package/dist/utils/numbers.cjs.map +1 -1
- package/dist/utils/numbers.js +7 -7
- package/dist/utils/objects.cjs +1 -1
- package/dist/utils/objects.cjs.map +1 -1
- package/dist/utils/objects.js +13 -7
- package/dist/utils/objects.js.map +1 -1
- package/dist/utils/safe-url.cjs +1 -1
- package/dist/utils/safe-url.cjs.map +1 -1
- package/dist/utils/safe-url.js +4 -1
- package/dist/utils/safe-url.js.map +1 -1
- package/dist/webrtc/peer-mesh.cjs +1 -1
- package/dist/webrtc/peer-mesh.cjs.map +1 -1
- package/dist/webrtc/peer-mesh.js +7 -1
- package/dist/webrtc/peer-mesh.js.map +1 -1
- package/dist/ws/create-web-socket.cjs +1 -1
- package/dist/ws/create-web-socket.cjs.map +1 -1
- package/dist/ws/create-web-socket.js +77 -61
- package/dist/ws/create-web-socket.js.map +1 -1
- package/dist/ws/use-web-socket.cjs +1 -1
- package/dist/ws/use-web-socket.cjs.map +1 -1
- package/dist/ws/use-web-socket.js +4 -3
- package/dist/ws/use-web-socket.js.map +1 -1
- package/package.json +1 -1
- package/template-pwa/src/pwa.tsx +42 -0
- package/template-pwa/src/sw.ts +15 -10
package/dist/sw/cache.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache.js","names":["getSwScope","precacheName","precachedPaths","CACHED_AT_HEADER","isExpired","maxAgeSeconds","stamp","response","dateHeader","storedAt","cacheControlDirectives","directive","isStorable","directives","route","credentialsOn","credentialHeaderSet","request","name","store","headers","vary","varies","missing","copy","cache","trimCache","maxEntries","cacheName","keys","key","cacheFirst","cached","networkFirst","network","timeoutMs","timeout","extend","reject","staleWhileRevalidate","fresh","runRoute","routeMatches","url","createPartialResponse","rangeHeader","match","buffer","total","start","end","suffix","slice","installRuntimeCache","event","routes","candidate","promise","full","stripRange","out","installPrecache","sw","options","manifest","manifestUrl","skipWaiting","navigationPreload","navigateFallbackDenylist","re","shell","navigateFallback"],"sources":["../../src/sw/cache.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines — precache, runtime strategies (cache-first, network-\n * first, stale-while-revalidate), expiry and the inspect/clear helpers all address\n * the same Cache Storage namespace, and the naming scheme that keeps them from\n * evicting each other is the file's whole contract.\n */\n/**\n * Service-worker caching helpers — a small, dependency-free subset of what\n * Workbox provides: precaching of the build's app shell (so the app launches\n * offline) plus runtime caching strategies for fonts, APIs and images.\n *\n * Import these inside your own `sw.ts`. They run in the service-worker global\n * scope, not the main thread. Pair `installPrecache` with the\n * `tempestPwaManifest()` Vite plugin (from `tempest-react-sdk/vite`), which\n * emits the `precache-manifest.json` this reads at install time.\n *\n * @example\n * /// <reference lib=\"webworker\" />\n * import { installRuntimeCache, installPrecache } from \"tempest-react-sdk/sw\";\n *\n * // Register specific routes FIRST so they win over the precache catch-all.\n * installRuntimeCache([\n * { match: /\\/api\\//, strategy: \"network-first\", cacheName: \"tempest-api\", maxAgeSeconds: 300 },\n * ]);\n * installPrecache();\n */\n\nimport { credentialHeaderSet } from \"./credential-headers\";\n\n/** Minimal shape of the events we touch, to avoid pulling in `lib.webworker`. */\ninterface ExtendableEventLike {\n waitUntil(promise: Promise<unknown>): void;\n}\n\ninterface FetchEventLike extends ExtendableEventLike {\n request: Request;\n respondWith(response: Response | Promise<Response>): void;\n /** Resolves to the navigation-preload response when it is enabled, else `undefined`. */\n preloadResponse?: Promise<Response | undefined>;\n}\n\n/** Minimal shape of `registration.navigationPreload` (Navigation Preload API). */\ninterface NavigationPreloadManagerLike {\n enable(): Promise<void>;\n disable(): Promise<void>;\n}\n\ninterface CacheSwGlobal {\n addEventListener(type: \"install\", listener: (event: ExtendableEventLike) => void): void;\n addEventListener(type: \"activate\", listener: (event: ExtendableEventLike) => void): void;\n addEventListener(type: \"fetch\", listener: (event: FetchEventLike) => void): void;\n clients: { claim(): Promise<void> };\n skipWaiting(): Promise<void>;\n location: { origin: string };\n registration?: { navigationPreload?: NavigationPreloadManagerLike };\n}\n\nfunction getSwScope(): CacheSwGlobal {\n return globalThis as unknown as CacheSwGlobal;\n}\n\n/** Caching strategy for a runtime route. Mirrors the common Workbox trio. */\nexport type RuntimeStrategy = \"cache-first\" | \"network-first\" | \"stale-while-revalidate\";\n\n/** A single runtime-caching rule, matched against each `GET` request. */\nexport interface RuntimeRoute {\n /** A `RegExp` tested against the full URL, or a predicate over the parsed URL. */\n match: RegExp | ((url: URL, request: Request) => boolean);\n /** How to resolve a match. */\n strategy: RuntimeStrategy;\n /** Cache bucket name for this route. */\n cacheName: string;\n /** Trim the cache to at most this many entries (FIFO) after each write. */\n maxEntries?: number;\n /** Treat a cached response older than this (seconds) as a miss. */\n maxAgeSeconds?: number;\n /** For `network-first`: fall back to cache after this timeout (seconds). */\n networkTimeoutSeconds?: number;\n /**\n * Serve HTTP `Range` requests (206 Partial Content) by slicing the cached\n * full response. Enable for audio/video so seeking works offline. The full\n * resource is cached once (the `Range` header is stripped before caching).\n */\n rangeRequests?: boolean;\n /**\n * Opt in to caching requests that carry a credential header (see\n * `credentialHeaders`), and responses marked `Cache-Control: private`.\n * Default `false`.\n *\n * Cache Storage is keyed by URL and outlives the session: without this\n * gate, a `GET /api/me` stored for one account was served to the next\n * account on the same browser whenever the network failed or exceeded\n * `networkTimeoutSeconds` (reproduced in Chromium 153, 11/10/2026). With\n * the default, credentialed requests bypass the route entirely and reach\n * the network as if no service worker were installed.\n *\n * When enabled, every stored entry gets `Vary` on each credential header the\n * request carried, so the Cache API only matches it for the same credential —\n * another account (or no account) misses instead of reading it. A token\n * rotated while online simply repopulates the entry. Pair it with\n * `clearSessionData()` on logout, which removes the entries themselves.\n * `Cache-Control: no-store` is never cached, opt-in or not.\n *\n * A cookie session is invisible here: the worker cannot see `Cookie`, and\n * gating on `request.credentials` would switch off caching for every\n * same-origin image and font. A cookie-authenticated API stays private only\n * through `Cache-Control: private`/`no-store` on its responses and\n * `clearSessionData()` on logout.\n */\n cacheAuthenticated?: boolean;\n /**\n * Header names that mark a request as credentialed. Default\n * `DEFAULT_CREDENTIAL_HEADERS` (`authorization`, `x-api-key`,\n * `x-auth-token`, …) — the same list the background-sync queue strips.\n */\n credentialHeaders?: readonly string[];\n}\n\n/** Options for {@link installPrecache}. */\nexport interface InstallPrecacheOptions {\n /** URL of the manifest emitted by `tempestPwaManifest()`. Default `/precache-manifest.json`. */\n manifestUrl?: string;\n /** Cache name prefix; the manifest `version` is appended. Default `tempest-precache`. */\n cacheName?: string;\n /** App-shell document served for navigation requests offline. Default `/index.html`. */\n navigateFallback?: string;\n /** Navigation paths that should NOT use the fallback (e.g. `[/^\\/api\\//]`). */\n navigateFallbackDenylist?: RegExp[];\n /** Activate the new worker immediately after precaching. Default `true`. */\n skipWaiting?: boolean;\n /**\n * Enable the Navigation Preload API on `activate`, so the browser fetches\n * the navigation request in parallel with the worker boot and the handler\n * serves `event.preloadResponse` — cutting first-navigation latency after\n * the worker starts. Silently ignored where unsupported. Default `true`.\n */\n navigationPreload?: boolean;\n}\n\ninterface PrecacheManifest {\n version: string;\n urls: string[];\n}\n\nlet precacheName = \"\";\nconst precachedPaths = new Set<string>();\n\n/**\n * Header stamped on every runtime-cache entry with the epoch milliseconds of the\n * write. `Date` alone is not enough: it is not a CORS-safelisted response\n * header, so for a cross-origin API the worker reads `null` and `maxAgeSeconds`\n * would never expire anything.\n */\nconst CACHED_AT_HEADER = \"x-tempest-cached-at\";\n\n/**\n * Whether a cached response has aged past `maxAgeSeconds`. Reads the\n * {@link CACHED_AT_HEADER} stamp written by the runtime cache, and falls back to\n * `Date` for entries written before the stamp existed.\n */\nfunction isExpired(response: Response, maxAgeSeconds?: number): boolean {\n if (!maxAgeSeconds) return false;\n const stamp = Number(response.headers.get(CACHED_AT_HEADER));\n const dateHeader = response.headers.get(\"date\");\n const storedAt = stamp > 0 ? stamp : dateHeader ? new Date(dateHeader).getTime() : NaN;\n if (Number.isNaN(storedAt)) return false;\n return (Date.now() - storedAt) / 1000 > maxAgeSeconds;\n}\n\n/** The `Cache-Control` directives of a response, lower-cased. */\nfunction cacheControlDirectives(response: Response): string[] {\n return (response.headers.get(\"cache-control\") ?? \"\")\n .toLowerCase()\n .split(\",\")\n .map((directive) => directive.trim().split(\"=\")[0]);\n}\n\n/**\n * Whether a network response may be written to the route's cache: it must be\n * `ok`, never `no-store`, and `private` only under `cacheAuthenticated`.\n */\nfunction isStorable(response: Response, route: RuntimeRoute): boolean {\n if (!response.ok) return false;\n const directives = cacheControlDirectives(response);\n if (directives.includes(\"no-store\")) return false;\n return route.cacheAuthenticated === true || !directives.includes(\"private\");\n}\n\n/** The credential headers, lower-cased, that a request carries for its route. */\nfunction credentialsOn(request: Request, route: RuntimeRoute): string[] {\n return [...credentialHeaderSet(route.credentialHeaders)].filter((name) =>\n request.headers.has(name),\n );\n}\n\n/**\n * Write a response to the route's cache with the {@link CACHED_AT_HEADER} stamp\n * and, under `cacheAuthenticated`, `Vary` on each credential header the request\n * carried, so the entry only matches the credential that produced it. Skips\n * responses that {@link isStorable} rejects, then trims the cache to\n * `maxEntries`.\n */\nasync function store(\n cache: Cache,\n request: Request,\n response: Response,\n route: RuntimeRoute,\n): Promise<void> {\n if (!isStorable(response, route)) return;\n const headers = new Headers(response.headers);\n headers.set(CACHED_AT_HEADER, String(Date.now()));\n if (route.cacheAuthenticated) {\n const vary = headers.get(\"vary\");\n const varies = (vary ?? \"\")\n .toLowerCase()\n .split(\",\")\n .map((name) => name.trim());\n const missing = credentialsOn(request, route).filter((name) => !varies.includes(name));\n if (missing.length > 0 && !varies.includes(\"*\")) {\n headers.set(\"vary\", [vary, ...missing].filter(Boolean).join(\", \"));\n }\n }\n const copy = response.clone();\n await cache.put(\n request,\n new Response(copy.body, { status: copy.status, statusText: copy.statusText, headers }),\n );\n await trimCache(route.cacheName, route.maxEntries);\n}\n\n/** Drop oldest entries until the cache holds at most `maxEntries`. */\nasync function trimCache(cacheName: string, maxEntries?: number): Promise<void> {\n if (!maxEntries) return;\n const cache = await caches.open(cacheName);\n const keys = await cache.keys();\n if (keys.length <= maxEntries) return;\n for (const key of keys.slice(0, keys.length - maxEntries)) {\n await cache.delete(key);\n }\n}\n\nasync function cacheFirst(request: Request, route: RuntimeRoute): Promise<Response> {\n const cache = await caches.open(route.cacheName);\n const cached = await cache.match(request);\n if (cached && !isExpired(cached, route.maxAgeSeconds)) return cached;\n\n const response = await fetch(request);\n await store(cache, request, response, route);\n return response;\n}\n\n/**\n * Network-first. When the timeout wins, the network request keeps running to\n * refresh the cache, and is handed to `extend` (`event.waitUntil`) so the\n * worker is not stopped mid-write.\n */\nasync function networkFirst(\n request: Request,\n route: RuntimeRoute,\n extend: (promise: Promise<unknown>) => void,\n): Promise<Response> {\n const cache = await caches.open(route.cacheName);\n\n const network = (async (): Promise<Response> => {\n const response = await fetch(request);\n await store(cache, request, response, route);\n return response;\n })();\n\n const timeoutMs = (route.networkTimeoutSeconds ?? 0) * 1000;\n try {\n if (timeoutMs > 0) {\n const timeout = new Promise<never>((_, reject) =>\n setTimeout(() => {\n extend(network.catch(() => undefined));\n reject(new Error(\"network-timeout\"));\n }, timeoutMs),\n );\n return await Promise.race([network, timeout]);\n }\n return await network;\n } catch {\n const cached = await cache.match(request);\n if (cached && !isExpired(cached, route.maxAgeSeconds)) return cached;\n throw new Error(\"network-first: no network and no cache\");\n }\n}\n\n/**\n * Stale-while-revalidate. A fresh cached response is served at once, and the\n * revalidation is handed to `extend` (`event.waitUntil`): without it the\n * browser may stop the worker before the refreshed response is written.\n */\nasync function staleWhileRevalidate(\n request: Request,\n route: RuntimeRoute,\n extend: (promise: Promise<unknown>) => void,\n): Promise<Response> {\n const cache = await caches.open(route.cacheName);\n const cached = await cache.match(request);\n\n const network = fetch(request)\n .then(async (response) => {\n await store(cache, request, response, route);\n return response;\n })\n .catch(() => undefined);\n\n const fresh = cached && !isExpired(cached, route.maxAgeSeconds) ? cached : undefined;\n if (fresh) {\n extend(network);\n return fresh;\n }\n const response = await network;\n if (response) return response;\n throw new Error(\"stale-while-revalidate: no network and no cache\");\n}\n\nfunction runRoute(\n request: Request,\n route: RuntimeRoute,\n extend: (promise: Promise<unknown>) => void,\n): Promise<Response> {\n switch (route.strategy) {\n case \"cache-first\":\n return cacheFirst(request, route);\n case \"network-first\":\n return networkFirst(request, route, extend);\n case \"stale-while-revalidate\":\n return staleWhileRevalidate(request, route, extend);\n }\n}\n\nfunction routeMatches(route: RuntimeRoute, url: URL, request: Request): boolean {\n return typeof route.match === \"function\"\n ? route.match(url, request)\n : route.match.test(url.href);\n}\n\n/**\n * Build a `206 Partial Content` response from a full one for an HTTP `Range`\n * request. Supports `bytes=start-end`, open-ended `bytes=start-` and suffix\n * `bytes=-suffixLength` (the last N bytes). Returns the original response when there is no usable\n * `Range` header, or a `416` when the range is unsatisfiable.\n *\n * @param request The incoming request (its `Range` header drives the slice).\n * @param response The full (200) response to slice.\n */\nexport async function createPartialResponse(\n request: Request,\n response: Response,\n): Promise<Response> {\n const rangeHeader = request.headers.get(\"range\");\n if (!rangeHeader) return response;\n\n const match = /^bytes=(\\d*)-(\\d*)$/.exec(rangeHeader.trim());\n if (!match || (match[1] === \"\" && match[2] === \"\")) {\n return new Response(null, { status: 416, statusText: \"Range Not Satisfiable\" });\n }\n\n const buffer = await response.clone().arrayBuffer();\n const total = buffer.byteLength;\n\n let start: number;\n let end: number;\n if (match[1] === \"\") {\n const suffix = Number(match[2]);\n start = Math.max(0, total - suffix);\n end = total - 1;\n } else {\n start = Number(match[1]);\n end = match[2] === \"\" ? total - 1 : Math.min(Number(match[2]), total - 1);\n }\n\n if (start > end || start >= total) {\n return new Response(null, {\n status: 416,\n statusText: \"Range Not Satisfiable\",\n headers: { \"Content-Range\": `bytes */${total}` },\n });\n }\n\n const slice = buffer.slice(start, end + 1);\n const headers = new Headers(response.headers);\n headers.set(\"Content-Range\", `bytes ${start}-${end}/${total}`);\n headers.set(\"Content-Length\", String(slice.byteLength));\n headers.set(\"Accept-Ranges\", \"bytes\");\n\n return new Response(slice, {\n status: 206,\n statusText: \"Partial Content\",\n headers,\n });\n}\n\n/**\n * Install a `fetch` handler that resolves matching `GET` requests with the\n * given runtime strategies. Non-matching requests are left untouched (no\n * `respondWith`), so a later {@link installPrecache} can handle them.\n *\n * A request carrying a credential header (`Authorization`, `X-Api-Key`, … — the\n * route's `credentialHeaders`) is also left untouched unless its route sets\n * `cacheAuthenticated`, and a response with `Cache-Control: no-store`\n * (or `private`, without that opt-in) is served but never stored. `maxAgeSeconds`\n * applies to every read, including the offline fallback: an expired entry is a\n * miss, never a last resort. Under `rangeRequests`, the full resource is cached\n * once (the `Range` header stripped) and each range is sliced from it.\n *\n * Register this BEFORE `installPrecache` so specific routes win over the\n * precache catch-all.\n *\n * @param routes Ordered rules; the first whose `match` passes handles the request.\n */\nexport function installRuntimeCache(routes: RuntimeRoute[]): void {\n const sw = getSwScope();\n sw.addEventListener(\"fetch\", (event) => {\n const request = event.request;\n if (request.method !== \"GET\") return;\n\n const url = new URL(request.url);\n const route = routes.find((candidate) => routeMatches(candidate, url, request));\n if (!route) return;\n if (!route.cacheAuthenticated && credentialsOn(request, route).length > 0) return;\n const extend = (promise: Promise<unknown>): void => event.waitUntil(promise);\n\n if (route.rangeRequests && request.headers.has(\"range\")) {\n const full = new Request(request.url, {\n headers: stripRange(request.headers),\n credentials: request.credentials,\n mode: request.mode === \"navigate\" ? \"same-origin\" : request.mode,\n });\n event.respondWith(\n runRoute(full, route, extend).then((response) =>\n createPartialResponse(request, response),\n ),\n );\n return;\n }\n\n event.respondWith(runRoute(request, route, extend));\n });\n}\n\n/** Copy headers without the `Range` header (so the cached entry is the full file). */\nfunction stripRange(headers: Headers): Headers {\n const out = new Headers(headers);\n out.delete(\"range\");\n return out;\n}\n\n/**\n * Precache the app shell at `install` and serve it offline:\n * - reads `precache-manifest.json` (emitted by `tempestPwaManifest()`),\n * - caches every listed URL under a versioned cache,\n * - on `activate`, deletes stale precache versions and claims open clients,\n * - on `fetch`, serves precached assets cache-first and falls back to the\n * `navigateFallback` document for offline navigations (SPA routing).\n *\n * Same-origin only. Register this LAST, after any {@link installRuntimeCache}.\n */\nexport function installPrecache(options: InstallPrecacheOptions = {}): void {\n const sw = getSwScope();\n const {\n manifestUrl = \"/precache-manifest.json\",\n cacheName = \"tempest-precache\",\n navigateFallback = \"/index.html\",\n navigateFallbackDenylist = [],\n skipWaiting = true,\n navigationPreload = true,\n } = options;\n\n sw.addEventListener(\"install\", (event) => {\n event.waitUntil(\n (async () => {\n const response = await fetch(manifestUrl, { cache: \"no-cache\" });\n const manifest = (await response.json()) as PrecacheManifest;\n precacheName = `${cacheName}-${manifest.version}`;\n\n const cache = await caches.open(precacheName);\n await cache.addAll(manifest.urls);\n for (const url of manifest.urls) {\n precachedPaths.add(new URL(url, sw.location.origin).pathname);\n }\n if (skipWaiting) await sw.skipWaiting();\n })(),\n );\n });\n\n sw.addEventListener(\"activate\", (event) => {\n event.waitUntil(\n (async () => {\n const keys = await caches.keys();\n await Promise.all(\n keys\n .filter((key) => key.startsWith(`${cacheName}-`) && key !== precacheName)\n .map((key) => caches.delete(key)),\n );\n if (navigationPreload && sw.registration?.navigationPreload) {\n await sw.registration.navigationPreload.enable();\n }\n await sw.clients.claim();\n })(),\n );\n });\n\n sw.addEventListener(\"fetch\", (event) => {\n const request = event.request;\n if (request.method !== \"GET\") return;\n\n const url = new URL(request.url);\n if (url.origin !== sw.location.origin) return;\n\n // SPA navigations: serve the cached app shell when offline.\n if (request.mode === \"navigate\") {\n if (navigateFallbackDenylist.some((re) => re.test(url.pathname))) return;\n event.respondWith(\n (async () => {\n try {\n const preloaded = await event.preloadResponse;\n if (preloaded) return preloaded;\n return await fetch(request);\n } catch {\n const cache = await caches.open(precacheName);\n const shell = await cache.match(navigateFallback);\n if (shell) return shell;\n throw new Error(\"offline and no cached app shell\");\n }\n })(),\n );\n return;\n }\n\n // Precached static assets: cache-first.\n if (precachedPaths.has(url.pathname)) {\n event.respondWith(\n (async () => {\n const cache = await caches.open(precacheName);\n const cached = await cache.match(request);\n return cached ?? fetch(request);\n })(),\n );\n }\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cache.js","names":["getSwScope","precacheName","precachedPaths","CACHED_AT_HEADER","isExpired","maxAgeSeconds","stamp","response","dateHeader","storedAt","cacheControlDirectives","directive","varyNames","vary","name","isStorable","directives","route","isCredentialed","request","credentialsOn","credentialHeaderSet","store","headers","varies","missing","copy","cache","trimCache","maxEntries","cacheName","keys","key","cacheFirst","cached","networkFirst","network","timeoutMs","timeout","extend","reject","staleWhileRevalidate","fresh","runRoute","routeMatches","url","createPartialResponse","rangeHeader","match","buffer","total","start","end","suffix","slice","installRuntimeCache","event","routes","candidate","promise","full","stripRange","out","installPrecache","sw","options","manifest","manifestUrl","skipWaiting","navigationPreload","navigateFallbackDenylist","re","shell","navigateFallback"],"sources":["../../src/sw/cache.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines — precache, runtime strategies (cache-first, network-\n * first, stale-while-revalidate), expiry and the inspect/clear helpers all address\n * the same Cache Storage namespace, and the naming scheme that keeps them from\n * evicting each other is the file's whole contract.\n */\n/**\n * Service-worker caching helpers — a small, dependency-free subset of what\n * Workbox provides: precaching of the build's app shell (so the app launches\n * offline) plus runtime caching strategies for fonts, APIs and images.\n *\n * Import these inside your own `sw.ts`. They run in the service-worker global\n * scope, not the main thread. Pair `installPrecache` with the\n * `tempestPwaManifest()` Vite plugin (from `tempest-react-sdk/vite`), which\n * emits the `precache-manifest.json` this reads at install time.\n *\n * @example\n * /// <reference lib=\"webworker\" />\n * import { installRuntimeCache, installPrecache } from \"tempest-react-sdk/sw\";\n *\n * // Register specific routes FIRST so they win over the precache catch-all.\n * installRuntimeCache([\n * { match: /\\/api\\//, strategy: \"network-first\", cacheName: \"tempest-api\", maxAgeSeconds: 300 },\n * ]);\n * installPrecache();\n */\n\nimport { credentialHeaderSet } from \"./credential-headers\";\n\n/** Minimal shape of the events we touch, to avoid pulling in `lib.webworker`. */\ninterface ExtendableEventLike {\n waitUntil(promise: Promise<unknown>): void;\n}\n\ninterface FetchEventLike extends ExtendableEventLike {\n request: Request;\n respondWith(response: Response | Promise<Response>): void;\n /** Resolves to the navigation-preload response when it is enabled, else `undefined`. */\n preloadResponse?: Promise<Response | undefined>;\n}\n\n/** Minimal shape of `registration.navigationPreload` (Navigation Preload API). */\ninterface NavigationPreloadManagerLike {\n enable(): Promise<void>;\n disable(): Promise<void>;\n}\n\ninterface CacheSwGlobal {\n addEventListener(type: \"install\", listener: (event: ExtendableEventLike) => void): void;\n addEventListener(type: \"activate\", listener: (event: ExtendableEventLike) => void): void;\n addEventListener(type: \"fetch\", listener: (event: FetchEventLike) => void): void;\n clients: { claim(): Promise<void> };\n skipWaiting(): Promise<void>;\n location: { origin: string };\n registration?: { navigationPreload?: NavigationPreloadManagerLike };\n}\n\nfunction getSwScope(): CacheSwGlobal {\n return globalThis as unknown as CacheSwGlobal;\n}\n\n/** Caching strategy for a runtime route. Mirrors the common Workbox trio. */\nexport type RuntimeStrategy = \"cache-first\" | \"network-first\" | \"stale-while-revalidate\";\n\n/** A single runtime-caching rule, matched against each `GET` request. */\nexport interface RuntimeRoute {\n /** A `RegExp` tested against the full URL, or a predicate over the parsed URL. */\n match: RegExp | ((url: URL, request: Request) => boolean);\n /** How to resolve a match. */\n strategy: RuntimeStrategy;\n /** Cache bucket name for this route. */\n cacheName: string;\n /** Trim the cache to at most this many entries (FIFO) after each write. */\n maxEntries?: number;\n /** Treat a cached response older than this (seconds) as a miss. */\n maxAgeSeconds?: number;\n /** For `network-first`: fall back to cache after this timeout (seconds). */\n networkTimeoutSeconds?: number;\n /**\n * Serve HTTP `Range` requests (206 Partial Content) by slicing the cached\n * full response. Enable for audio/video so seeking works offline. The full\n * resource is cached once (the `Range` header is stripped before caching).\n */\n rangeRequests?: boolean;\n /**\n * Opt in to caching credentialed requests — those that carry a credential\n * header (see `credentialHeaders`) or are a CORS-mode `fetch` sent with\n * `credentials: \"include\"` — and responses marked `Cache-Control: private`.\n * Default `false`.\n *\n * Cache Storage is keyed by URL and outlives the session: without this\n * gate, a `GET /api/me` stored for one account was served to the next\n * account on the same browser whenever the network failed or exceeded\n * `networkTimeoutSeconds` (reproduced in Chromium 153, 11/10/2026). With\n * the default, credentialed requests bypass the route entirely and reach\n * the network as if no service worker were installed.\n *\n * When enabled, every stored entry gets `Vary` on each credential header the\n * request carried, so the Cache API only matches it for the same credential —\n * another account (or no account) misses instead of reading it. A token\n * rotated while online simply repopulates the entry. Pair it with\n * `clearSessionData()` on logout, which removes the entries themselves.\n * `Cache-Control: no-store` is never cached, opt-in or not.\n *\n * A cookie session is only partly visible here: the worker cannot see\n * `Cookie`. What it can see is a `fetch` that asked for cookies explicitly —\n * `mode: \"cors\"` with `credentials: \"include\"`, which is what `ApiClient`\n * sends under `withCredentials` — and since 0.82.0 that request is treated\n * as credentialed too (#531). `<img>`, `<audio>` and `<video>` also send\n * `credentials: \"include\"`, but in `no-cors` mode, so they keep caching. A\n * plain same-origin `fetch` (`credentials: \"same-origin\"`, the default)\n * carries the cookie without saying so; such an API stays private only\n * through `Cache-Control: private`/`no-store` on its responses and\n * `clearSessionData()` on logout. Opting a cookie request in caches it by\n * URL — there is no credential header to vary on — so `clearSessionData()`\n * is then its only isolation.\n */\n cacheAuthenticated?: boolean;\n /**\n * Header names that mark a request as credentialed. Default\n * `DEFAULT_CREDENTIAL_HEADERS` (`authorization`, `x-api-key`,\n * `x-auth-token`, …) — the same list the background-sync queue strips.\n */\n credentialHeaders?: readonly string[];\n}\n\n/** Options for {@link installPrecache}. */\nexport interface InstallPrecacheOptions {\n /** URL of the manifest emitted by `tempestPwaManifest()`. Default `/precache-manifest.json`. */\n manifestUrl?: string;\n /** Cache name prefix; the manifest `version` is appended. Default `tempest-precache`. */\n cacheName?: string;\n /** App-shell document served for navigation requests offline. Default `/index.html`. */\n navigateFallback?: string;\n /** Navigation paths that should NOT use the fallback (e.g. `[/^\\/api\\//]`). */\n navigateFallbackDenylist?: RegExp[];\n /** Activate the new worker immediately after precaching. Default `true`. */\n skipWaiting?: boolean;\n /**\n * Enable the Navigation Preload API on `activate`, so the browser fetches\n * the navigation request in parallel with the worker boot and the handler\n * serves `event.preloadResponse` — cutting first-navigation latency after\n * the worker starts. Silently ignored where unsupported. Default `true`.\n */\n navigationPreload?: boolean;\n}\n\ninterface PrecacheManifest {\n version: string;\n urls: string[];\n}\n\nlet precacheName = \"\";\nconst precachedPaths = new Set<string>();\n\n/**\n * Header stamped on every runtime-cache entry with the epoch milliseconds of the\n * write. `Date` alone is not enough: it is not a CORS-safelisted response\n * header, so for a cross-origin API the worker reads `null` and `maxAgeSeconds`\n * would never expire anything.\n */\nconst CACHED_AT_HEADER = \"x-tempest-cached-at\";\n\n/**\n * Whether a cached response has aged past `maxAgeSeconds`. Reads the\n * {@link CACHED_AT_HEADER} stamp written by the runtime cache, and falls back to\n * `Date` for entries written before the stamp existed.\n */\nfunction isExpired(response: Response, maxAgeSeconds?: number): boolean {\n if (!maxAgeSeconds) return false;\n const stamp = Number(response.headers.get(CACHED_AT_HEADER));\n const dateHeader = response.headers.get(\"date\");\n const storedAt = stamp > 0 ? stamp : dateHeader ? new Date(dateHeader).getTime() : NaN;\n if (Number.isNaN(storedAt)) return false;\n return (Date.now() - storedAt) / 1000 > maxAgeSeconds;\n}\n\n/** The `Cache-Control` directives of a response, lower-cased. */\nfunction cacheControlDirectives(response: Response): string[] {\n return (response.headers.get(\"cache-control\") ?? \"\")\n .toLowerCase()\n .split(\",\")\n .map((directive) => directive.trim().split(\"=\")[0]);\n}\n\n/** The comma-separated names of a response's `Vary` header, trimmed and lower-cased. */\nfunction varyNames(vary: string | null): string[] {\n return (vary ?? \"\")\n .toLowerCase()\n .split(\",\")\n .map((name) => name.trim());\n}\n\n/**\n * Whether a network response may be written to the route's cache: it must be\n * `ok`, never `no-store`, never `Vary: *`, and `private` only under\n * `cacheAuthenticated`.\n *\n * `Vary: *` says no later request can be proven to match, and `Cache.put`\n * rejects it with a `TypeError`. Before 0.82.0 that rejection escaped the\n * strategy, so a response the network had delivered became the offline\n * fallback path (network-first), an error (cache-first) or nothing at all\n * (stale-while-revalidate) (#534).\n */\nfunction isStorable(response: Response, route: RuntimeRoute): boolean {\n if (!response.ok) return false;\n if (varyNames(response.headers.get(\"vary\")).includes(\"*\")) return false;\n const directives = cacheControlDirectives(response);\n if (directives.includes(\"no-store\")) return false;\n return route.cacheAuthenticated === true || !directives.includes(\"private\");\n}\n\n/**\n * Whether a request is session-bound for its route: it carries a credential\n * header, or it is a CORS `fetch` that asked for cookies with\n * `credentials: \"include\"` (`ApiClient` under `withCredentials`). A `no-cors`\n * request — `<img>`, `<audio>`, `<video>` — also sends `include` by default and\n * is not counted, or every image would bypass the cache.\n */\nfunction isCredentialed(request: Request, route: RuntimeRoute): boolean {\n if (request.mode === \"cors\" && request.credentials === \"include\") return true;\n return credentialsOn(request, route).length > 0;\n}\n\n/** The credential headers, lower-cased, that a request carries for its route. */\nfunction credentialsOn(request: Request, route: RuntimeRoute): string[] {\n return [...credentialHeaderSet(route.credentialHeaders)].filter((name) =>\n request.headers.has(name),\n );\n}\n\n/**\n * Write a response to the route's cache with the {@link CACHED_AT_HEADER} stamp\n * and, under `cacheAuthenticated`, `Vary` on each credential header the request\n * carried, so the entry only matches the credential that produced it. Skips\n * responses that {@link isStorable} rejects, then trims the cache to\n * `maxEntries`.\n */\nasync function store(\n cache: Cache,\n request: Request,\n response: Response,\n route: RuntimeRoute,\n): Promise<void> {\n if (!isStorable(response, route)) return;\n const headers = new Headers(response.headers);\n headers.set(CACHED_AT_HEADER, String(Date.now()));\n if (route.cacheAuthenticated) {\n const vary = headers.get(\"vary\");\n const varies = varyNames(vary);\n const missing = credentialsOn(request, route).filter((name) => !varies.includes(name));\n if (missing.length > 0) {\n headers.set(\"vary\", [vary, ...missing].filter(Boolean).join(\", \"));\n }\n }\n const copy = response.clone();\n await cache.put(\n request,\n new Response(copy.body, { status: copy.status, statusText: copy.statusText, headers }),\n );\n await trimCache(route.cacheName, route.maxEntries);\n}\n\n/** Drop oldest entries until the cache holds at most `maxEntries`. */\nasync function trimCache(cacheName: string, maxEntries?: number): Promise<void> {\n if (!maxEntries) return;\n const cache = await caches.open(cacheName);\n const keys = await cache.keys();\n if (keys.length <= maxEntries) return;\n for (const key of keys.slice(0, keys.length - maxEntries)) {\n await cache.delete(key);\n }\n}\n\nasync function cacheFirst(request: Request, route: RuntimeRoute): Promise<Response> {\n const cache = await caches.open(route.cacheName);\n const cached = await cache.match(request);\n if (cached && !isExpired(cached, route.maxAgeSeconds)) return cached;\n\n const response = await fetch(request);\n await store(cache, request, response, route);\n return response;\n}\n\n/**\n * Network-first. When the timeout wins, the network request keeps running to\n * refresh the cache, and is handed to `extend` (`event.waitUntil`) so the\n * worker is not stopped mid-write.\n */\nasync function networkFirst(\n request: Request,\n route: RuntimeRoute,\n extend: (promise: Promise<unknown>) => void,\n): Promise<Response> {\n const cache = await caches.open(route.cacheName);\n\n const network = (async (): Promise<Response> => {\n const response = await fetch(request);\n await store(cache, request, response, route);\n return response;\n })();\n\n const timeoutMs = (route.networkTimeoutSeconds ?? 0) * 1000;\n try {\n if (timeoutMs > 0) {\n const timeout = new Promise<never>((_, reject) =>\n setTimeout(() => {\n extend(network.catch(() => undefined));\n reject(new Error(\"network-timeout\"));\n }, timeoutMs),\n );\n return await Promise.race([network, timeout]);\n }\n return await network;\n } catch {\n const cached = await cache.match(request);\n if (cached && !isExpired(cached, route.maxAgeSeconds)) return cached;\n throw new Error(\"network-first: no network and no cache\");\n }\n}\n\n/**\n * Stale-while-revalidate. A fresh cached response is served at once, and the\n * revalidation is handed to `extend` (`event.waitUntil`): without it the\n * browser may stop the worker before the refreshed response is written.\n */\nasync function staleWhileRevalidate(\n request: Request,\n route: RuntimeRoute,\n extend: (promise: Promise<unknown>) => void,\n): Promise<Response> {\n const cache = await caches.open(route.cacheName);\n const cached = await cache.match(request);\n\n const network = fetch(request)\n .then(async (response) => {\n await store(cache, request, response, route);\n return response;\n })\n .catch(() => undefined);\n\n const fresh = cached && !isExpired(cached, route.maxAgeSeconds) ? cached : undefined;\n if (fresh) {\n extend(network);\n return fresh;\n }\n const response = await network;\n if (response) return response;\n throw new Error(\"stale-while-revalidate: no network and no cache\");\n}\n\nfunction runRoute(\n request: Request,\n route: RuntimeRoute,\n extend: (promise: Promise<unknown>) => void,\n): Promise<Response> {\n switch (route.strategy) {\n case \"cache-first\":\n return cacheFirst(request, route);\n case \"network-first\":\n return networkFirst(request, route, extend);\n case \"stale-while-revalidate\":\n return staleWhileRevalidate(request, route, extend);\n }\n}\n\nfunction routeMatches(route: RuntimeRoute, url: URL, request: Request): boolean {\n return typeof route.match === \"function\"\n ? route.match(url, request)\n : route.match.test(url.href);\n}\n\n/**\n * Build a `206 Partial Content` response from a full one for an HTTP `Range`\n * request. Supports `bytes=start-end`, open-ended `bytes=start-` and suffix\n * `bytes=-suffixLength` (the last N bytes). Returns the original response when there is no usable\n * `Range` header, or a `416` when the range is unsatisfiable.\n *\n * @param request The incoming request (its `Range` header drives the slice).\n * @param response The full (200) response to slice.\n */\nexport async function createPartialResponse(\n request: Request,\n response: Response,\n): Promise<Response> {\n const rangeHeader = request.headers.get(\"range\");\n if (!rangeHeader) return response;\n\n const match = /^bytes=(\\d*)-(\\d*)$/.exec(rangeHeader.trim());\n if (!match || (match[1] === \"\" && match[2] === \"\")) {\n return new Response(null, { status: 416, statusText: \"Range Not Satisfiable\" });\n }\n\n const buffer = await response.clone().arrayBuffer();\n const total = buffer.byteLength;\n\n let start: number;\n let end: number;\n if (match[1] === \"\") {\n const suffix = Number(match[2]);\n start = Math.max(0, total - suffix);\n end = total - 1;\n } else {\n start = Number(match[1]);\n end = match[2] === \"\" ? total - 1 : Math.min(Number(match[2]), total - 1);\n }\n\n if (start > end || start >= total) {\n return new Response(null, {\n status: 416,\n statusText: \"Range Not Satisfiable\",\n headers: { \"Content-Range\": `bytes */${total}` },\n });\n }\n\n const slice = buffer.slice(start, end + 1);\n const headers = new Headers(response.headers);\n headers.set(\"Content-Range\", `bytes ${start}-${end}/${total}`);\n headers.set(\"Content-Length\", String(slice.byteLength));\n headers.set(\"Accept-Ranges\", \"bytes\");\n\n return new Response(slice, {\n status: 206,\n statusText: \"Partial Content\",\n headers,\n });\n}\n\n/**\n * Install a `fetch` handler that resolves matching `GET` requests with the\n * given runtime strategies. Non-matching requests are left untouched (no\n * `respondWith`), so a later {@link installPrecache} can handle them.\n *\n * A request carrying a credential header (`Authorization`, `X-Api-Key`, … — the\n * route's `credentialHeaders`), or a CORS `fetch` sent with\n * `credentials: \"include\"`, is also left untouched unless its route sets\n * `cacheAuthenticated`, and a response with `Cache-Control: no-store`\n * (or `private`, without that opt-in) or `Vary: *` is served but never stored. `maxAgeSeconds`\n * applies to every read, including the offline fallback: an expired entry is a\n * miss, never a last resort. Under `rangeRequests`, the full resource is cached\n * once (the `Range` header stripped) and each range is sliced from it.\n *\n * Register this BEFORE `installPrecache` so specific routes win over the\n * precache catch-all.\n *\n * @param routes Ordered rules; the first whose `match` passes handles the request.\n */\nexport function installRuntimeCache(routes: RuntimeRoute[]): void {\n const sw = getSwScope();\n sw.addEventListener(\"fetch\", (event) => {\n const request = event.request;\n if (request.method !== \"GET\") return;\n\n const url = new URL(request.url);\n const route = routes.find((candidate) => routeMatches(candidate, url, request));\n if (!route) return;\n if (!route.cacheAuthenticated && isCredentialed(request, route)) return;\n const extend = (promise: Promise<unknown>): void => event.waitUntil(promise);\n\n if (route.rangeRequests && request.headers.has(\"range\")) {\n const full = new Request(request.url, {\n headers: stripRange(request.headers),\n credentials: request.credentials,\n mode: request.mode === \"navigate\" ? \"same-origin\" : request.mode,\n });\n event.respondWith(\n runRoute(full, route, extend).then((response) =>\n createPartialResponse(request, response),\n ),\n );\n return;\n }\n\n event.respondWith(runRoute(request, route, extend));\n });\n}\n\n/** Copy headers without the `Range` header (so the cached entry is the full file). */\nfunction stripRange(headers: Headers): Headers {\n const out = new Headers(headers);\n out.delete(\"range\");\n return out;\n}\n\n/**\n * Precache the app shell at `install` and serve it offline:\n * - reads `precache-manifest.json` (emitted by `tempestPwaManifest()`),\n * - caches every listed URL under a versioned cache,\n * - on `activate`, deletes stale precache versions and claims open clients,\n * - on `fetch`, serves precached assets cache-first and falls back to the\n * `navigateFallback` document for offline navigations (SPA routing).\n *\n * Same-origin only. Register this LAST, after any {@link installRuntimeCache}.\n */\nexport function installPrecache(options: InstallPrecacheOptions = {}): void {\n const sw = getSwScope();\n const {\n manifestUrl = \"/precache-manifest.json\",\n cacheName = \"tempest-precache\",\n navigateFallback = \"/index.html\",\n navigateFallbackDenylist = [],\n skipWaiting = true,\n navigationPreload = true,\n } = options;\n\n sw.addEventListener(\"install\", (event) => {\n event.waitUntil(\n (async () => {\n const response = await fetch(manifestUrl, { cache: \"no-cache\" });\n const manifest = (await response.json()) as PrecacheManifest;\n precacheName = `${cacheName}-${manifest.version}`;\n\n const cache = await caches.open(precacheName);\n await cache.addAll(manifest.urls);\n for (const url of manifest.urls) {\n precachedPaths.add(new URL(url, sw.location.origin).pathname);\n }\n if (skipWaiting) await sw.skipWaiting();\n })(),\n );\n });\n\n sw.addEventListener(\"activate\", (event) => {\n event.waitUntil(\n (async () => {\n const keys = await caches.keys();\n await Promise.all(\n keys\n .filter((key) => key.startsWith(`${cacheName}-`) && key !== precacheName)\n .map((key) => caches.delete(key)),\n );\n if (navigationPreload && sw.registration?.navigationPreload) {\n await sw.registration.navigationPreload.enable();\n }\n await sw.clients.claim();\n })(),\n );\n });\n\n sw.addEventListener(\"fetch\", (event) => {\n const request = event.request;\n if (request.method !== \"GET\") return;\n\n const url = new URL(request.url);\n if (url.origin !== sw.location.origin) return;\n\n // SPA navigations: serve the cached app shell when offline.\n if (request.mode === \"navigate\") {\n if (navigateFallbackDenylist.some((re) => re.test(url.pathname))) return;\n event.respondWith(\n (async () => {\n try {\n const preloaded = await event.preloadResponse;\n if (preloaded) return preloaded;\n return await fetch(request);\n } catch {\n const cache = await caches.open(precacheName);\n const shell = await cache.match(navigateFallback);\n if (shell) return shell;\n throw new Error(\"offline and no cached app shell\");\n }\n })(),\n );\n return;\n }\n\n // Precached static assets: cache-first.\n if (precachedPaths.has(url.pathname)) {\n event.respondWith(\n (async () => {\n const cache = await caches.open(precacheName);\n const cached = await cache.match(request);\n return cached ?? fetch(request);\n })(),\n );\n }\n });\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
function e(){return globalThis}var t=[`body`,`image`,`tag`,`requireInteraction`,`vibrate`,`silent`,`renotify`,`timestamp`,`dir`,`lang`];function n(e,n,r){let i={icon:e.icon??n,badge:e.badge??r};for(let n of t)e[n]!==void 0&&(i[n]=e[n]);let a={
|
|
1
|
+
function e(){return globalThis}var t=[`body`,`image`,`tag`,`requireInteraction`,`vibrate`,`silent`,`renotify`,`timestamp`,`dir`,`lang`];function n(e,n,r){let i={icon:e.icon??n,badge:e.badge??r};for(let n of t)e[n]!==void 0&&(i[n]=e[n]);let a={...e.data??{},url:e.url??`/`};if(delete a.actionUrls,e.actions){let t={};i.actions=e.actions.map(({url:e,...n})=>(e!==void 0&&(t[n.action]=e),n)),Object.keys(t).length>0&&(a.actionUrls=t)}return i.data=a,i}function r(t={}){let r=e(),{defaultTitle:i=`Notificação`,defaultIcon:a,defaultBadge:o,transform:s}=t;r.addEventListener(`push`,e=>{if(!e.data)return;let t;try{t=e.data.json()}catch{t={title:i,body:e.data.text()}}let c=s?s(t):t;if(!c)return;let l=c.title??i,u=n(c,a,o);e.waitUntil(r.registration.showNotification(l,u))})}function i(e,t){if(typeof e==`string`)return e;if(!e||typeof e!=`object`)return`/`;let n=e;if(t&&n.actionUrls&&typeof n.actionUrls==`object`){let e=n.actionUrls[t];if(typeof e==`string`)return e}return typeof n.url==`string`?n.url:`/`}function a(e,t,n){if(t===void 0)return`/`;let r;try{r=new URL(e,t)}catch{return`/`}return r.protocol!==`http:`&&r.protocol!==`https:`||n&&r.origin!==t?`/`:e}function o(e,t){try{return new URL(t,e).href===new URL(e).href}catch{return!1}}function s(t={}){let n=e(),r=t.resolveUrl??i,s=t.resolveUrl===void 0;n.addEventListener(`notificationclick`,e=>{e.notification.close();let t=a(r(e.notification.data,e.action),n.location?.origin,s);e.waitUntil((async()=>{let e=await n.clients.matchAll({type:`window`,includeUncontrolled:!0});for(let n of e)if(o(n.url,t))return n.focus();return n.clients.openWindow(t)})())})}function c(){let t=e();t.addEventListener(`message`,e=>{e.data?.type===`SKIP_WAITING`&&t.skipWaiting()})}exports.installNotificationClickHandler=s,exports.installPushHandler=r,exports.installSkipWaitingListener=c;
|
|
2
2
|
//# sourceMappingURL=create-push-handler.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-push-handler.cjs","names":["getSwScope","FORWARDED_OPTIONS","buildNotificationOptions","options","payload","defaultIcon","defaultBadge","key","data","actionUrls","action","url","installPushHandler","sw","event","raw","defaultTitle","transform","title","notification","defaultResolveUrl","record","actionUrl","clientShowsTarget","target","clientUrl","installNotificationClickHandler","resolveUrl","clients","client","installSkipWaitingListener"],"sources":["../../src/sw/create-push-handler.ts"],"sourcesContent":["/**\n * Service-worker context helpers for handling `push` and `notificationclick`\n * events. Import these inside your own `sw.ts` — they expect to run in the\n * service-worker global scope, not in the main thread.\n *\n * @example\n * /// <reference lib=\"webworker\" />\n * import { installPushHandler, installNotificationClickHandler } from \"tempest-react-sdk\";\n *\n * installPushHandler({ defaultIcon: \"/icons/Logo.png\" });\n * installNotificationClickHandler();\n */\n\ninterface SwGlobal {\n registration: {\n showNotification(title: string, options?: NotificationOptions): Promise<void>;\n };\n clients: {\n matchAll(options: { type: \"window\"; includeUncontrolled?: boolean }): Promise<\n {\n url: string;\n focused: boolean;\n focus(): Promise<unknown>;\n navigate(url: string): Promise<unknown>;\n }[]\n >;\n openWindow(url: string): Promise<unknown>;\n };\n addEventListener(\n type: \"push\",\n listener: (event: {\n data: { json(): unknown; text(): string } | null;\n waitUntil(promise: Promise<unknown>): void;\n }) => void,\n ): void;\n addEventListener(\n type: \"notificationclick\",\n listener: (event: {\n notification: { close(): void; data?: unknown };\n action?: string;\n waitUntil(promise: Promise<unknown>): void;\n }) => void,\n ): void;\n skipWaiting(): Promise<void>;\n}\n\nfunction getSwScope(): SwGlobal {\n return globalThis as unknown as SwGlobal;\n}\n\n/**\n * A notification button as the push payload describes it.\n *\n * `action`, `title` and `icon` are forwarded to the browser as a\n * `NotificationAction`. `url` is not part of that type — the browser would drop\n * it — so {@link installPushHandler} moves it into `data.actionUrls`, where\n * {@link installNotificationClickHandler} reads it back when that button is\n * clicked.\n */\nexport interface PushNotificationAction {\n /** Identifier delivered as `event.action` when the button is clicked. */\n action: string;\n /** Button label. */\n title: string;\n /** Button icon URL. */\n icon?: string;\n /** URL opened when this button is clicked. Falls back to the top-level `url`. */\n url?: string;\n}\n\nexport interface PushPayload {\n title?: string;\n body?: string;\n icon?: string;\n badge?: string;\n image?: string;\n tag?: string;\n /** URL opened when the notification body is clicked. Default: `\"/\"`. */\n url?: string;\n /** Buttons rendered on the notification (browsers show up to `Notification.maxActions`). */\n actions?: PushNotificationAction[];\n /** Keep the notification on screen until the user interacts with it. */\n requireInteraction?: boolean;\n /** Vibration pattern in milliseconds (mobile). */\n vibrate?: number | number[];\n /** Show the notification without sound or vibration. */\n silent?: boolean;\n /** Alert again when a notification with the same `tag` replaces an older one. */\n renotify?: boolean;\n /** Event time in epoch milliseconds, shown by some platforms. */\n timestamp?: number;\n /** Text direction of the title and body. */\n dir?: NotificationDirection;\n /** BCP 47 language tag of the title and body. */\n lang?: string;\n /** Arbitrary extra data forwarded to `event.notification.data`. */\n data?: Record<string, unknown>;\n}\n\n/**\n * `NotificationOptions` plus the members TypeScript's DOM lib leaves out\n * because not every engine implements them. Chromium honours all of them;\n * engines without support ignore the unknown keys.\n */\ntype ExtendedNotificationOptions = NotificationOptions & {\n image?: string;\n actions?: Omit<PushNotificationAction, \"url\">[];\n vibrate?: number | number[];\n renotify?: boolean;\n timestamp?: number;\n};\n\n/**\n * Payload keys copied verbatim onto the notification options when present.\n */\nconst FORWARDED_OPTIONS = [\n \"body\",\n \"image\",\n \"tag\",\n \"requireInteraction\",\n \"vibrate\",\n \"silent\",\n \"renotify\",\n \"timestamp\",\n \"dir\",\n \"lang\",\n] as const satisfies readonly (keyof PushPayload & keyof ExtendedNotificationOptions)[];\n\n/**\n * Build the `showNotification` options from a push payload.\n *\n * Keys the payload omits stay absent instead of being set to `undefined`, so\n * the browser applies its own defaults. Per-action `url`s are collected into\n * `data.actionUrls` (only when at least one action carries one) and stripped\n * from the actions handed to the browser.\n *\n * @param payload - The parsed (and transformed) push payload.\n * @param defaultIcon - Icon used when the payload omits one.\n * @param defaultBadge - Badge used when the payload omits one.\n * @returns The options object for `registration.showNotification`.\n */\nfunction buildNotificationOptions(\n payload: PushPayload,\n defaultIcon: string | undefined,\n defaultBadge: string | undefined,\n): ExtendedNotificationOptions {\n const options: Record<string, unknown> = {\n icon: payload.icon ?? defaultIcon,\n badge: payload.badge ?? defaultBadge,\n };\n for (const key of FORWARDED_OPTIONS) {\n if (payload[key] !== undefined) options[key] = payload[key];\n }\n\n const data: Record<string, unknown> = { url: payload.url ?? \"/\", ...(payload.data ?? {}) };\n if (payload.actions) {\n const actionUrls: Record<string, string> = {};\n options.actions = payload.actions.map(({ url, ...action }) => {\n if (url !== undefined) actionUrls[action.action] = url;\n return action;\n });\n if (Object.keys(actionUrls).length > 0) data.actionUrls = actionUrls;\n }\n options.data = data;\n return options as ExtendedNotificationOptions;\n}\n\nexport interface InstallPushHandlerOptions {\n /** Title used when the payload omits one. */\n defaultTitle?: string;\n /** Icon used when the payload omits one. */\n defaultIcon?: string;\n /** Badge image (mobile). */\n defaultBadge?: string;\n /**\n * Transform the raw payload before showing the notification. Return `null`\n * to suppress the notification entirely (e.g. silent pings).\n */\n transform?: (payload: PushPayload) => PushPayload | null;\n}\n\n/**\n * Install a `push` event listener that parses the payload as JSON (with a\n * plain-text fallback) and shows a notification.\n *\n * Every `NotificationOptions` member the payload carries is forwarded —\n * `actions`, `requireInteraction`, `vibrate`, `silent`, `renotify`,\n * `timestamp`, `dir` and `lang` alongside `body`, `icon`, `badge`, `image` and\n * `tag`. `data` receives the top-level `url` (default `\"/\"`), the payload's\n * own `data`, and `actionUrls` mapping each action id to its `url`.\n */\nexport function installPushHandler(options: InstallPushHandlerOptions = {}): void {\n const sw = getSwScope();\n const { defaultTitle = \"Notificação\", defaultIcon, defaultBadge, transform } = options;\n\n sw.addEventListener(\"push\", (event) => {\n if (!event.data) return;\n\n let raw: PushPayload;\n try {\n raw = event.data.json() as PushPayload;\n } catch {\n raw = { title: defaultTitle, body: event.data.text() };\n }\n\n const payload = transform ? transform(raw) : raw;\n if (!payload) return;\n\n const title = payload.title ?? defaultTitle;\n const notification = buildNotificationOptions(payload, defaultIcon, defaultBadge);\n\n event.waitUntil(sw.registration.showNotification(title, notification));\n });\n}\n\nexport interface InstallNotificationClickHandlerOptions {\n /**\n * Resolve the destination URL from the notification data and the clicked\n * action id (`\"\"` or `undefined` for a click on the body). Default: the\n * action's entry in `data.actionUrls`, else `data.url`, else `\"/\"`.\n */\n resolveUrl?: (data: unknown, action: string | undefined) => string;\n}\n\n/**\n * Default URL resolution for a notification click.\n *\n * @param data - `event.notification.data`, as {@link installPushHandler} wrote it.\n * @param action - `event.action`; empty or `undefined` for a click on the body.\n * @returns The action's URL when it has one, else `data.url`, else `\"/\"`.\n */\nfunction defaultResolveUrl(data: unknown, action: string | undefined): string {\n if (typeof data === \"string\") return data;\n if (!data || typeof data !== \"object\") return \"/\";\n const record = data as Record<string, unknown>;\n if (action && record.actionUrls && typeof record.actionUrls === \"object\") {\n const actionUrl = (record.actionUrls as Record<string, unknown>)[action];\n if (typeof actionUrl === \"string\") return actionUrl;\n }\n return typeof record.url === \"string\" ? record.url : \"/\";\n}\n\n/**\n * Whether an open window client is already showing `target`.\n *\n * `target` is resolved against the client's own URL, so a relative path\n * compares on the client's origin, and the comparison is on the full href.\n * A substring test would let `/events/1` match a client on `/events/10`, and\n * `/` match every client.\n *\n * @param clientUrl - The window client's absolute URL.\n * @param target - The URL the click resolved to, absolute or relative.\n * @returns `true` when both point at the same document.\n */\nfunction clientShowsTarget(clientUrl: string, target: string): boolean {\n try {\n return new URL(target, clientUrl).href === new URL(clientUrl).href;\n } catch {\n return false;\n }\n}\n\n/**\n * Install a `notificationclick` handler that focuses an existing client when\n * possible and falls back to opening a new window.\n *\n * A click on an action button opens that action's URL (see\n * {@link PushNotificationAction.url}); a click on the body, or on an action\n * without one, opens the top-level `url`.\n */\nexport function installNotificationClickHandler(\n options: InstallNotificationClickHandlerOptions = {},\n): void {\n const sw = getSwScope();\n const resolveUrl = options.resolveUrl ?? defaultResolveUrl;\n\n sw.addEventListener(\"notificationclick\", (event) => {\n event.notification.close();\n const target = resolveUrl(event.notification.data, event.action);\n\n event.waitUntil(\n (async () => {\n const clients = await sw.clients.matchAll({\n type: \"window\",\n includeUncontrolled: true,\n });\n for (const client of clients) {\n if (clientShowsTarget(client.url, target)) {\n return client.focus();\n }\n }\n return sw.clients.openWindow(target);\n })(),\n );\n });\n}\n\n/**\n * Install a `message` listener that activates a waiting worker when the host\n * app sends `{ type: \"SKIP_WAITING\" }`.\n */\nexport function installSkipWaitingListener(): void {\n const sw = getSwScope() as SwGlobal & {\n addEventListener(\n type: \"message\",\n listener: (event: { data?: { type?: string } }) => void,\n ): void;\n };\n sw.addEventListener(\"message\", (event) => {\n if (event.data?.type === \"SKIP_WAITING\") {\n void sw.skipWaiting();\n }\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"create-push-handler.cjs","names":["getSwScope","FORWARDED_OPTIONS","buildNotificationOptions","options","payload","defaultIcon","defaultBadge","key","data","actionUrls","action","url","installPushHandler","sw","event","raw","defaultTitle","transform","title","notification","defaultResolveUrl","record","actionUrl","allowedTarget","origin","target","sameOriginOnly","clientShowsTarget","clientUrl","installNotificationClickHandler","resolveUrl","clients","client","installSkipWaitingListener"],"sources":["../../src/sw/create-push-handler.ts"],"sourcesContent":["/**\n * Service-worker context helpers for handling `push` and `notificationclick`\n * events. Import these inside your own `sw.ts` — they expect to run in the\n * service-worker global scope, not in the main thread.\n *\n * @example\n * /// <reference lib=\"webworker\" />\n * import { installPushHandler, installNotificationClickHandler } from \"tempest-react-sdk\";\n *\n * installPushHandler({ defaultIcon: \"/icons/Logo.png\" });\n * installNotificationClickHandler();\n */\n\ninterface SwGlobal {\n registration: {\n showNotification(title: string, options?: NotificationOptions): Promise<void>;\n };\n clients: {\n matchAll(options: { type: \"window\"; includeUncontrolled?: boolean }): Promise<\n {\n url: string;\n focused: boolean;\n focus(): Promise<unknown>;\n navigate(url: string): Promise<unknown>;\n }[]\n >;\n openWindow(url: string): Promise<unknown>;\n };\n addEventListener(\n type: \"push\",\n listener: (event: {\n data: { json(): unknown; text(): string } | null;\n waitUntil(promise: Promise<unknown>): void;\n }) => void,\n ): void;\n addEventListener(\n type: \"notificationclick\",\n listener: (event: {\n notification: { close(): void; data?: unknown };\n action?: string;\n waitUntil(promise: Promise<unknown>): void;\n }) => void,\n ): void;\n skipWaiting(): Promise<void>;\n location?: { origin: string };\n}\n\nfunction getSwScope(): SwGlobal {\n return globalThis as unknown as SwGlobal;\n}\n\n/**\n * A notification button as the push payload describes it.\n *\n * `action`, `title` and `icon` are forwarded to the browser as a\n * `NotificationAction`. `url` is not part of that type — the browser would drop\n * it — so {@link installPushHandler} moves it into `data.actionUrls`, where\n * {@link installNotificationClickHandler} reads it back when that button is\n * clicked.\n */\nexport interface PushNotificationAction {\n /** Identifier delivered as `event.action` when the button is clicked. */\n action: string;\n /** Button label. */\n title: string;\n /** Button icon URL. */\n icon?: string;\n /** URL opened when this button is clicked. Falls back to the top-level `url`. */\n url?: string;\n}\n\nexport interface PushPayload {\n title?: string;\n body?: string;\n icon?: string;\n badge?: string;\n image?: string;\n tag?: string;\n /** URL opened when the notification body is clicked. Default: `\"/\"`. */\n url?: string;\n /** Buttons rendered on the notification (browsers show up to `Notification.maxActions`). */\n actions?: PushNotificationAction[];\n /** Keep the notification on screen until the user interacts with it. */\n requireInteraction?: boolean;\n /** Vibration pattern in milliseconds (mobile). */\n vibrate?: number | number[];\n /** Show the notification without sound or vibration. */\n silent?: boolean;\n /** Alert again when a notification with the same `tag` replaces an older one. */\n renotify?: boolean;\n /** Event time in epoch milliseconds, shown by some platforms. */\n timestamp?: number;\n /** Text direction of the title and body. */\n dir?: NotificationDirection;\n /** BCP 47 language tag of the title and body. */\n lang?: string;\n /**\n * Arbitrary extra data forwarded to `event.notification.data`.\n *\n * Its `url` and `actionUrls` keys are overwritten by the top-level `url` and\n * the actions' `url`s: those two keys decide where a click goes, and a\n * free-form bag — often filled from the record that triggered the push — must\n * not be able to redirect it.\n */\n data?: Record<string, unknown>;\n}\n\n/**\n * `NotificationOptions` plus the members TypeScript's DOM lib leaves out\n * because not every engine implements them. Chromium honours all of them;\n * engines without support ignore the unknown keys.\n */\ntype ExtendedNotificationOptions = NotificationOptions & {\n image?: string;\n actions?: Omit<PushNotificationAction, \"url\">[];\n vibrate?: number | number[];\n renotify?: boolean;\n timestamp?: number;\n};\n\n/**\n * Payload keys copied verbatim onto the notification options when present.\n */\nconst FORWARDED_OPTIONS = [\n \"body\",\n \"image\",\n \"tag\",\n \"requireInteraction\",\n \"vibrate\",\n \"silent\",\n \"renotify\",\n \"timestamp\",\n \"dir\",\n \"lang\",\n] as const satisfies readonly (keyof PushPayload & keyof ExtendedNotificationOptions)[];\n\n/**\n * Build the `showNotification` options from a push payload.\n *\n * Keys the payload omits stay absent instead of being set to `undefined`, so\n * the browser applies its own defaults. Per-action `url`s are collected into\n * `data.actionUrls` (only when at least one action carries one) and stripped\n * from the actions handed to the browser.\n *\n * `payload.data` is spread **first**, so its own `url` or `actionUrls` can never\n * replace the destinations the payload declares: until 0.81.0 it was spread last,\n * and `{ url: \"/orders/9\", data: { url: \"https://elsewhere\" } }` opened\n * `https://elsewhere` on click.\n *\n * @param payload - The parsed (and transformed) push payload.\n * @param defaultIcon - Icon used when the payload omits one.\n * @param defaultBadge - Badge used when the payload omits one.\n * @returns The options object for `registration.showNotification`.\n */\nfunction buildNotificationOptions(\n payload: PushPayload,\n defaultIcon: string | undefined,\n defaultBadge: string | undefined,\n): ExtendedNotificationOptions {\n const options: Record<string, unknown> = {\n icon: payload.icon ?? defaultIcon,\n badge: payload.badge ?? defaultBadge,\n };\n for (const key of FORWARDED_OPTIONS) {\n if (payload[key] !== undefined) options[key] = payload[key];\n }\n\n const data: Record<string, unknown> = { ...(payload.data ?? {}), url: payload.url ?? \"/\" };\n delete data.actionUrls;\n if (payload.actions) {\n const actionUrls: Record<string, string> = {};\n options.actions = payload.actions.map(({ url, ...action }) => {\n if (url !== undefined) actionUrls[action.action] = url;\n return action;\n });\n if (Object.keys(actionUrls).length > 0) data.actionUrls = actionUrls;\n }\n options.data = data;\n return options as ExtendedNotificationOptions;\n}\n\nexport interface InstallPushHandlerOptions {\n /** Title used when the payload omits one. */\n defaultTitle?: string;\n /** Icon used when the payload omits one. */\n defaultIcon?: string;\n /** Badge image (mobile). */\n defaultBadge?: string;\n /**\n * Transform the raw payload before showing the notification. Return `null`\n * to suppress the notification entirely (e.g. silent pings).\n */\n transform?: (payload: PushPayload) => PushPayload | null;\n}\n\n/**\n * Install a `push` event listener that parses the payload as JSON (with a\n * plain-text fallback) and shows a notification.\n *\n * Every `NotificationOptions` member the payload carries is forwarded —\n * `actions`, `requireInteraction`, `vibrate`, `silent`, `renotify`,\n * `timestamp`, `dir` and `lang` alongside `body`, `icon`, `badge`, `image` and\n * `tag`. `data` receives the payload's own `data`, then the top-level `url`\n * (default `\"/\"`) and `actionUrls` mapping each action id to its `url` — in that\n * order, so `payload.data` cannot override where a click goes.\n */\nexport function installPushHandler(options: InstallPushHandlerOptions = {}): void {\n const sw = getSwScope();\n const { defaultTitle = \"Notificação\", defaultIcon, defaultBadge, transform } = options;\n\n sw.addEventListener(\"push\", (event) => {\n if (!event.data) return;\n\n let raw: PushPayload;\n try {\n raw = event.data.json() as PushPayload;\n } catch {\n raw = { title: defaultTitle, body: event.data.text() };\n }\n\n const payload = transform ? transform(raw) : raw;\n if (!payload) return;\n\n const title = payload.title ?? defaultTitle;\n const notification = buildNotificationOptions(payload, defaultIcon, defaultBadge);\n\n event.waitUntil(sw.registration.showNotification(title, notification));\n });\n}\n\nexport interface InstallNotificationClickHandlerOptions {\n /**\n * Resolve the destination URL from the notification data and the clicked\n * action id (`\"\"` or `undefined` for a click on the body). Default: the\n * action's entry in `data.actionUrls`, else `data.url`, else `\"/\"`.\n *\n * The default only ever opens a page of the worker's **own origin**: a URL on\n * another origin, or with a scheme other than `http:`/`https:`, becomes `\"/\"`.\n * A resolver of your own is how an app opts into another origin — what it\n * returns may point anywhere `http:`/`https:`, and any other scheme still\n * becomes `\"/\"`.\n */\n resolveUrl?: (data: unknown, action: string | undefined) => string;\n}\n\n/**\n * Default URL resolution for a notification click.\n *\n * @param data - `event.notification.data`, as {@link installPushHandler} wrote it.\n * @param action - `event.action`; empty or `undefined` for a click on the body.\n * @returns The action's URL when it has one, else `data.url`, else `\"/\"`.\n */\nfunction defaultResolveUrl(data: unknown, action: string | undefined): string {\n if (typeof data === \"string\") return data;\n if (!data || typeof data !== \"object\") return \"/\";\n const record = data as Record<string, unknown>;\n if (action && record.actionUrls && typeof record.actionUrls === \"object\") {\n const actionUrl = (record.actionUrls as Record<string, unknown>)[action];\n if (typeof actionUrl === \"string\") return actionUrl;\n }\n return typeof record.url === \"string\" ? record.url : \"/\";\n}\n\n/**\n * The URL a click may open, given the worker's origin.\n *\n * The push payload is written by the app's server, but its `url` is often\n * copied from the record that triggered the push — a chat message, a ticket —\n * and `openWindow` opens it with the app's notification as the only context the\n * user sees. So the default resolver keeps to the worker's own origin, the same\n * line `safeLinkUrl` draws for rendered links, and a custom resolver is the\n * explicit opt-in to leave it. Neither may open a non-`http(s)` scheme.\n *\n * The URL comes back as given, not resolved, so a relative path stays relative —\n * `openWindow` resolves it against the worker's location exactly like this check\n * did.\n *\n * @param target - What the resolver returned.\n * @param origin - The worker's origin; `undefined` outside a worker, where\n * nothing can be judged and only `\"/\"` is opened.\n * @param sameOriginOnly - Whether another origin is refused too.\n * @returns `target` when it may be opened, `\"/\"` otherwise.\n */\nfunction allowedTarget(\n target: string,\n origin: string | undefined,\n sameOriginOnly: boolean,\n): string {\n if (origin === undefined) return \"/\";\n let url: URL;\n try {\n url = new URL(target, origin);\n } catch {\n return \"/\";\n }\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") return \"/\";\n if (sameOriginOnly && url.origin !== origin) return \"/\";\n return target;\n}\n\n/**\n * Whether an open window client is already showing `target`.\n *\n * `target` is resolved against the client's own URL, so a relative path\n * compares on the client's origin, and the comparison is on the full href.\n * A substring test would let `/events/1` match a client on `/events/10`, and\n * `/` match every client.\n *\n * @param clientUrl - The window client's absolute URL.\n * @param target - The URL the click resolved to, absolute or relative.\n * @returns `true` when both point at the same document.\n */\nfunction clientShowsTarget(clientUrl: string, target: string): boolean {\n try {\n return new URL(target, clientUrl).href === new URL(clientUrl).href;\n } catch {\n return false;\n }\n}\n\n/**\n * Install a `notificationclick` handler that focuses an existing client when\n * possible and falls back to opening a new window.\n *\n * A click on an action button opens that action's URL (see\n * {@link PushNotificationAction.url}); a click on the body, or on an action\n * without one, opens the top-level `url`. Only a same-origin `http(s)` URL is\n * opened by default; see {@link InstallNotificationClickHandlerOptions.resolveUrl}\n * for opting into another origin.\n */\nexport function installNotificationClickHandler(\n options: InstallNotificationClickHandlerOptions = {},\n): void {\n const sw = getSwScope();\n const resolveUrl = options.resolveUrl ?? defaultResolveUrl;\n const sameOriginOnly = options.resolveUrl === undefined;\n\n sw.addEventListener(\"notificationclick\", (event) => {\n event.notification.close();\n const target = allowedTarget(\n resolveUrl(event.notification.data, event.action),\n sw.location?.origin,\n sameOriginOnly,\n );\n\n event.waitUntil(\n (async () => {\n const clients = await sw.clients.matchAll({\n type: \"window\",\n includeUncontrolled: true,\n });\n for (const client of clients) {\n if (clientShowsTarget(client.url, target)) {\n return client.focus();\n }\n }\n return sw.clients.openWindow(target);\n })(),\n );\n });\n}\n\n/**\n * Install a `message` listener that activates a waiting worker when the host\n * app sends `{ type: \"SKIP_WAITING\" }`.\n */\nexport function installSkipWaitingListener(): void {\n const sw = getSwScope() as SwGlobal & {\n addEventListener(\n type: \"message\",\n listener: (event: { data?: { type?: string } }) => void,\n ): void;\n };\n sw.addEventListener(\"message\", (event) => {\n if (event.data?.type === \"SKIP_WAITING\") {\n void sw.skipWaiting();\n }\n });\n}\n"],"mappings":"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"}
|
|
@@ -21,10 +21,10 @@ function n(e, n, r) {
|
|
|
21
21
|
};
|
|
22
22
|
for (let n of t) e[n] !== void 0 && (i[n] = e[n]);
|
|
23
23
|
let a = {
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
...e.data ?? {},
|
|
25
|
+
url: e.url ?? "/"
|
|
26
26
|
};
|
|
27
|
-
if (e.actions) {
|
|
27
|
+
if (delete a.actionUrls, e.actions) {
|
|
28
28
|
let t = {};
|
|
29
29
|
i.actions = e.actions.map(({ url: e, ...n }) => (e !== void 0 && (t[n.action] = e), n)), Object.keys(t).length > 0 && (a.actionUrls = t);
|
|
30
30
|
}
|
|
@@ -59,35 +59,45 @@ function i(e, t) {
|
|
|
59
59
|
}
|
|
60
60
|
return typeof n.url == "string" ? n.url : "/";
|
|
61
61
|
}
|
|
62
|
-
function a(e, t) {
|
|
62
|
+
function a(e, t, n) {
|
|
63
|
+
if (t === void 0) return "/";
|
|
64
|
+
let r;
|
|
65
|
+
try {
|
|
66
|
+
r = new URL(e, t);
|
|
67
|
+
} catch {
|
|
68
|
+
return "/";
|
|
69
|
+
}
|
|
70
|
+
return r.protocol !== "http:" && r.protocol !== "https:" || n && r.origin !== t ? "/" : e;
|
|
71
|
+
}
|
|
72
|
+
function o(e, t) {
|
|
63
73
|
try {
|
|
64
74
|
return new URL(t, e).href === new URL(e).href;
|
|
65
75
|
} catch {
|
|
66
76
|
return !1;
|
|
67
77
|
}
|
|
68
78
|
}
|
|
69
|
-
function
|
|
70
|
-
let n = e(), r = t.resolveUrl ?? i;
|
|
79
|
+
function s(t = {}) {
|
|
80
|
+
let n = e(), r = t.resolveUrl ?? i, s = t.resolveUrl === void 0;
|
|
71
81
|
n.addEventListener("notificationclick", (e) => {
|
|
72
82
|
e.notification.close();
|
|
73
|
-
let t = r(e.notification.data, e.action);
|
|
83
|
+
let t = a(r(e.notification.data, e.action), n.location?.origin, s);
|
|
74
84
|
e.waitUntil((async () => {
|
|
75
85
|
let e = await n.clients.matchAll({
|
|
76
86
|
type: "window",
|
|
77
87
|
includeUncontrolled: !0
|
|
78
88
|
});
|
|
79
|
-
for (let n of e) if (
|
|
89
|
+
for (let n of e) if (o(n.url, t)) return n.focus();
|
|
80
90
|
return n.clients.openWindow(t);
|
|
81
91
|
})());
|
|
82
92
|
});
|
|
83
93
|
}
|
|
84
|
-
function
|
|
94
|
+
function c() {
|
|
85
95
|
let t = e();
|
|
86
96
|
t.addEventListener("message", (e) => {
|
|
87
97
|
e.data?.type === "SKIP_WAITING" && t.skipWaiting();
|
|
88
98
|
});
|
|
89
99
|
}
|
|
90
100
|
//#endregion
|
|
91
|
-
export {
|
|
101
|
+
export { s as installNotificationClickHandler, r as installPushHandler, c as installSkipWaitingListener };
|
|
92
102
|
|
|
93
103
|
//# sourceMappingURL=create-push-handler.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-push-handler.js","names":["getSwScope","FORWARDED_OPTIONS","buildNotificationOptions","options","payload","defaultIcon","defaultBadge","key","data","actionUrls","action","url","installPushHandler","sw","event","raw","defaultTitle","transform","title","notification","defaultResolveUrl","record","actionUrl","clientShowsTarget","target","clientUrl","installNotificationClickHandler","resolveUrl","clients","client","installSkipWaitingListener"],"sources":["../../src/sw/create-push-handler.ts"],"sourcesContent":["/**\n * Service-worker context helpers for handling `push` and `notificationclick`\n * events. Import these inside your own `sw.ts` — they expect to run in the\n * service-worker global scope, not in the main thread.\n *\n * @example\n * /// <reference lib=\"webworker\" />\n * import { installPushHandler, installNotificationClickHandler } from \"tempest-react-sdk\";\n *\n * installPushHandler({ defaultIcon: \"/icons/Logo.png\" });\n * installNotificationClickHandler();\n */\n\ninterface SwGlobal {\n registration: {\n showNotification(title: string, options?: NotificationOptions): Promise<void>;\n };\n clients: {\n matchAll(options: { type: \"window\"; includeUncontrolled?: boolean }): Promise<\n {\n url: string;\n focused: boolean;\n focus(): Promise<unknown>;\n navigate(url: string): Promise<unknown>;\n }[]\n >;\n openWindow(url: string): Promise<unknown>;\n };\n addEventListener(\n type: \"push\",\n listener: (event: {\n data: { json(): unknown; text(): string } | null;\n waitUntil(promise: Promise<unknown>): void;\n }) => void,\n ): void;\n addEventListener(\n type: \"notificationclick\",\n listener: (event: {\n notification: { close(): void; data?: unknown };\n action?: string;\n waitUntil(promise: Promise<unknown>): void;\n }) => void,\n ): void;\n skipWaiting(): Promise<void>;\n}\n\nfunction getSwScope(): SwGlobal {\n return globalThis as unknown as SwGlobal;\n}\n\n/**\n * A notification button as the push payload describes it.\n *\n * `action`, `title` and `icon` are forwarded to the browser as a\n * `NotificationAction`. `url` is not part of that type — the browser would drop\n * it — so {@link installPushHandler} moves it into `data.actionUrls`, where\n * {@link installNotificationClickHandler} reads it back when that button is\n * clicked.\n */\nexport interface PushNotificationAction {\n /** Identifier delivered as `event.action` when the button is clicked. */\n action: string;\n /** Button label. */\n title: string;\n /** Button icon URL. */\n icon?: string;\n /** URL opened when this button is clicked. Falls back to the top-level `url`. */\n url?: string;\n}\n\nexport interface PushPayload {\n title?: string;\n body?: string;\n icon?: string;\n badge?: string;\n image?: string;\n tag?: string;\n /** URL opened when the notification body is clicked. Default: `\"/\"`. */\n url?: string;\n /** Buttons rendered on the notification (browsers show up to `Notification.maxActions`). */\n actions?: PushNotificationAction[];\n /** Keep the notification on screen until the user interacts with it. */\n requireInteraction?: boolean;\n /** Vibration pattern in milliseconds (mobile). */\n vibrate?: number | number[];\n /** Show the notification without sound or vibration. */\n silent?: boolean;\n /** Alert again when a notification with the same `tag` replaces an older one. */\n renotify?: boolean;\n /** Event time in epoch milliseconds, shown by some platforms. */\n timestamp?: number;\n /** Text direction of the title and body. */\n dir?: NotificationDirection;\n /** BCP 47 language tag of the title and body. */\n lang?: string;\n /** Arbitrary extra data forwarded to `event.notification.data`. */\n data?: Record<string, unknown>;\n}\n\n/**\n * `NotificationOptions` plus the members TypeScript's DOM lib leaves out\n * because not every engine implements them. Chromium honours all of them;\n * engines without support ignore the unknown keys.\n */\ntype ExtendedNotificationOptions = NotificationOptions & {\n image?: string;\n actions?: Omit<PushNotificationAction, \"url\">[];\n vibrate?: number | number[];\n renotify?: boolean;\n timestamp?: number;\n};\n\n/**\n * Payload keys copied verbatim onto the notification options when present.\n */\nconst FORWARDED_OPTIONS = [\n \"body\",\n \"image\",\n \"tag\",\n \"requireInteraction\",\n \"vibrate\",\n \"silent\",\n \"renotify\",\n \"timestamp\",\n \"dir\",\n \"lang\",\n] as const satisfies readonly (keyof PushPayload & keyof ExtendedNotificationOptions)[];\n\n/**\n * Build the `showNotification` options from a push payload.\n *\n * Keys the payload omits stay absent instead of being set to `undefined`, so\n * the browser applies its own defaults. Per-action `url`s are collected into\n * `data.actionUrls` (only when at least one action carries one) and stripped\n * from the actions handed to the browser.\n *\n * @param payload - The parsed (and transformed) push payload.\n * @param defaultIcon - Icon used when the payload omits one.\n * @param defaultBadge - Badge used when the payload omits one.\n * @returns The options object for `registration.showNotification`.\n */\nfunction buildNotificationOptions(\n payload: PushPayload,\n defaultIcon: string | undefined,\n defaultBadge: string | undefined,\n): ExtendedNotificationOptions {\n const options: Record<string, unknown> = {\n icon: payload.icon ?? defaultIcon,\n badge: payload.badge ?? defaultBadge,\n };\n for (const key of FORWARDED_OPTIONS) {\n if (payload[key] !== undefined) options[key] = payload[key];\n }\n\n const data: Record<string, unknown> = { url: payload.url ?? \"/\", ...(payload.data ?? {}) };\n if (payload.actions) {\n const actionUrls: Record<string, string> = {};\n options.actions = payload.actions.map(({ url, ...action }) => {\n if (url !== undefined) actionUrls[action.action] = url;\n return action;\n });\n if (Object.keys(actionUrls).length > 0) data.actionUrls = actionUrls;\n }\n options.data = data;\n return options as ExtendedNotificationOptions;\n}\n\nexport interface InstallPushHandlerOptions {\n /** Title used when the payload omits one. */\n defaultTitle?: string;\n /** Icon used when the payload omits one. */\n defaultIcon?: string;\n /** Badge image (mobile). */\n defaultBadge?: string;\n /**\n * Transform the raw payload before showing the notification. Return `null`\n * to suppress the notification entirely (e.g. silent pings).\n */\n transform?: (payload: PushPayload) => PushPayload | null;\n}\n\n/**\n * Install a `push` event listener that parses the payload as JSON (with a\n * plain-text fallback) and shows a notification.\n *\n * Every `NotificationOptions` member the payload carries is forwarded —\n * `actions`, `requireInteraction`, `vibrate`, `silent`, `renotify`,\n * `timestamp`, `dir` and `lang` alongside `body`, `icon`, `badge`, `image` and\n * `tag`. `data` receives the top-level `url` (default `\"/\"`), the payload's\n * own `data`, and `actionUrls` mapping each action id to its `url`.\n */\nexport function installPushHandler(options: InstallPushHandlerOptions = {}): void {\n const sw = getSwScope();\n const { defaultTitle = \"Notificação\", defaultIcon, defaultBadge, transform } = options;\n\n sw.addEventListener(\"push\", (event) => {\n if (!event.data) return;\n\n let raw: PushPayload;\n try {\n raw = event.data.json() as PushPayload;\n } catch {\n raw = { title: defaultTitle, body: event.data.text() };\n }\n\n const payload = transform ? transform(raw) : raw;\n if (!payload) return;\n\n const title = payload.title ?? defaultTitle;\n const notification = buildNotificationOptions(payload, defaultIcon, defaultBadge);\n\n event.waitUntil(sw.registration.showNotification(title, notification));\n });\n}\n\nexport interface InstallNotificationClickHandlerOptions {\n /**\n * Resolve the destination URL from the notification data and the clicked\n * action id (`\"\"` or `undefined` for a click on the body). Default: the\n * action's entry in `data.actionUrls`, else `data.url`, else `\"/\"`.\n */\n resolveUrl?: (data: unknown, action: string | undefined) => string;\n}\n\n/**\n * Default URL resolution for a notification click.\n *\n * @param data - `event.notification.data`, as {@link installPushHandler} wrote it.\n * @param action - `event.action`; empty or `undefined` for a click on the body.\n * @returns The action's URL when it has one, else `data.url`, else `\"/\"`.\n */\nfunction defaultResolveUrl(data: unknown, action: string | undefined): string {\n if (typeof data === \"string\") return data;\n if (!data || typeof data !== \"object\") return \"/\";\n const record = data as Record<string, unknown>;\n if (action && record.actionUrls && typeof record.actionUrls === \"object\") {\n const actionUrl = (record.actionUrls as Record<string, unknown>)[action];\n if (typeof actionUrl === \"string\") return actionUrl;\n }\n return typeof record.url === \"string\" ? record.url : \"/\";\n}\n\n/**\n * Whether an open window client is already showing `target`.\n *\n * `target` is resolved against the client's own URL, so a relative path\n * compares on the client's origin, and the comparison is on the full href.\n * A substring test would let `/events/1` match a client on `/events/10`, and\n * `/` match every client.\n *\n * @param clientUrl - The window client's absolute URL.\n * @param target - The URL the click resolved to, absolute or relative.\n * @returns `true` when both point at the same document.\n */\nfunction clientShowsTarget(clientUrl: string, target: string): boolean {\n try {\n return new URL(target, clientUrl).href === new URL(clientUrl).href;\n } catch {\n return false;\n }\n}\n\n/**\n * Install a `notificationclick` handler that focuses an existing client when\n * possible and falls back to opening a new window.\n *\n * A click on an action button opens that action's URL (see\n * {@link PushNotificationAction.url}); a click on the body, or on an action\n * without one, opens the top-level `url`.\n */\nexport function installNotificationClickHandler(\n options: InstallNotificationClickHandlerOptions = {},\n): void {\n const sw = getSwScope();\n const resolveUrl = options.resolveUrl ?? defaultResolveUrl;\n\n sw.addEventListener(\"notificationclick\", (event) => {\n event.notification.close();\n const target = resolveUrl(event.notification.data, event.action);\n\n event.waitUntil(\n (async () => {\n const clients = await sw.clients.matchAll({\n type: \"window\",\n includeUncontrolled: true,\n });\n for (const client of clients) {\n if (clientShowsTarget(client.url, target)) {\n return client.focus();\n }\n }\n return sw.clients.openWindow(target);\n })(),\n );\n });\n}\n\n/**\n * Install a `message` listener that activates a waiting worker when the host\n * app sends `{ type: \"SKIP_WAITING\" }`.\n */\nexport function installSkipWaitingListener(): void {\n const sw = getSwScope() as SwGlobal & {\n addEventListener(\n type: \"message\",\n listener: (event: { data?: { type?: string } }) => void,\n ): void;\n };\n sw.addEventListener(\"message\", (event) => {\n if (event.data?.type === \"SKIP_WAITING\") {\n void sw.skipWaiting();\n }\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"create-push-handler.js","names":["getSwScope","FORWARDED_OPTIONS","buildNotificationOptions","options","payload","defaultIcon","defaultBadge","key","data","actionUrls","action","url","installPushHandler","sw","event","raw","defaultTitle","transform","title","notification","defaultResolveUrl","record","actionUrl","allowedTarget","origin","target","sameOriginOnly","clientShowsTarget","clientUrl","installNotificationClickHandler","resolveUrl","clients","client","installSkipWaitingListener"],"sources":["../../src/sw/create-push-handler.ts"],"sourcesContent":["/**\n * Service-worker context helpers for handling `push` and `notificationclick`\n * events. Import these inside your own `sw.ts` — they expect to run in the\n * service-worker global scope, not in the main thread.\n *\n * @example\n * /// <reference lib=\"webworker\" />\n * import { installPushHandler, installNotificationClickHandler } from \"tempest-react-sdk\";\n *\n * installPushHandler({ defaultIcon: \"/icons/Logo.png\" });\n * installNotificationClickHandler();\n */\n\ninterface SwGlobal {\n registration: {\n showNotification(title: string, options?: NotificationOptions): Promise<void>;\n };\n clients: {\n matchAll(options: { type: \"window\"; includeUncontrolled?: boolean }): Promise<\n {\n url: string;\n focused: boolean;\n focus(): Promise<unknown>;\n navigate(url: string): Promise<unknown>;\n }[]\n >;\n openWindow(url: string): Promise<unknown>;\n };\n addEventListener(\n type: \"push\",\n listener: (event: {\n data: { json(): unknown; text(): string } | null;\n waitUntil(promise: Promise<unknown>): void;\n }) => void,\n ): void;\n addEventListener(\n type: \"notificationclick\",\n listener: (event: {\n notification: { close(): void; data?: unknown };\n action?: string;\n waitUntil(promise: Promise<unknown>): void;\n }) => void,\n ): void;\n skipWaiting(): Promise<void>;\n location?: { origin: string };\n}\n\nfunction getSwScope(): SwGlobal {\n return globalThis as unknown as SwGlobal;\n}\n\n/**\n * A notification button as the push payload describes it.\n *\n * `action`, `title` and `icon` are forwarded to the browser as a\n * `NotificationAction`. `url` is not part of that type — the browser would drop\n * it — so {@link installPushHandler} moves it into `data.actionUrls`, where\n * {@link installNotificationClickHandler} reads it back when that button is\n * clicked.\n */\nexport interface PushNotificationAction {\n /** Identifier delivered as `event.action` when the button is clicked. */\n action: string;\n /** Button label. */\n title: string;\n /** Button icon URL. */\n icon?: string;\n /** URL opened when this button is clicked. Falls back to the top-level `url`. */\n url?: string;\n}\n\nexport interface PushPayload {\n title?: string;\n body?: string;\n icon?: string;\n badge?: string;\n image?: string;\n tag?: string;\n /** URL opened when the notification body is clicked. Default: `\"/\"`. */\n url?: string;\n /** Buttons rendered on the notification (browsers show up to `Notification.maxActions`). */\n actions?: PushNotificationAction[];\n /** Keep the notification on screen until the user interacts with it. */\n requireInteraction?: boolean;\n /** Vibration pattern in milliseconds (mobile). */\n vibrate?: number | number[];\n /** Show the notification without sound or vibration. */\n silent?: boolean;\n /** Alert again when a notification with the same `tag` replaces an older one. */\n renotify?: boolean;\n /** Event time in epoch milliseconds, shown by some platforms. */\n timestamp?: number;\n /** Text direction of the title and body. */\n dir?: NotificationDirection;\n /** BCP 47 language tag of the title and body. */\n lang?: string;\n /**\n * Arbitrary extra data forwarded to `event.notification.data`.\n *\n * Its `url` and `actionUrls` keys are overwritten by the top-level `url` and\n * the actions' `url`s: those two keys decide where a click goes, and a\n * free-form bag — often filled from the record that triggered the push — must\n * not be able to redirect it.\n */\n data?: Record<string, unknown>;\n}\n\n/**\n * `NotificationOptions` plus the members TypeScript's DOM lib leaves out\n * because not every engine implements them. Chromium honours all of them;\n * engines without support ignore the unknown keys.\n */\ntype ExtendedNotificationOptions = NotificationOptions & {\n image?: string;\n actions?: Omit<PushNotificationAction, \"url\">[];\n vibrate?: number | number[];\n renotify?: boolean;\n timestamp?: number;\n};\n\n/**\n * Payload keys copied verbatim onto the notification options when present.\n */\nconst FORWARDED_OPTIONS = [\n \"body\",\n \"image\",\n \"tag\",\n \"requireInteraction\",\n \"vibrate\",\n \"silent\",\n \"renotify\",\n \"timestamp\",\n \"dir\",\n \"lang\",\n] as const satisfies readonly (keyof PushPayload & keyof ExtendedNotificationOptions)[];\n\n/**\n * Build the `showNotification` options from a push payload.\n *\n * Keys the payload omits stay absent instead of being set to `undefined`, so\n * the browser applies its own defaults. Per-action `url`s are collected into\n * `data.actionUrls` (only when at least one action carries one) and stripped\n * from the actions handed to the browser.\n *\n * `payload.data` is spread **first**, so its own `url` or `actionUrls` can never\n * replace the destinations the payload declares: until 0.81.0 it was spread last,\n * and `{ url: \"/orders/9\", data: { url: \"https://elsewhere\" } }` opened\n * `https://elsewhere` on click.\n *\n * @param payload - The parsed (and transformed) push payload.\n * @param defaultIcon - Icon used when the payload omits one.\n * @param defaultBadge - Badge used when the payload omits one.\n * @returns The options object for `registration.showNotification`.\n */\nfunction buildNotificationOptions(\n payload: PushPayload,\n defaultIcon: string | undefined,\n defaultBadge: string | undefined,\n): ExtendedNotificationOptions {\n const options: Record<string, unknown> = {\n icon: payload.icon ?? defaultIcon,\n badge: payload.badge ?? defaultBadge,\n };\n for (const key of FORWARDED_OPTIONS) {\n if (payload[key] !== undefined) options[key] = payload[key];\n }\n\n const data: Record<string, unknown> = { ...(payload.data ?? {}), url: payload.url ?? \"/\" };\n delete data.actionUrls;\n if (payload.actions) {\n const actionUrls: Record<string, string> = {};\n options.actions = payload.actions.map(({ url, ...action }) => {\n if (url !== undefined) actionUrls[action.action] = url;\n return action;\n });\n if (Object.keys(actionUrls).length > 0) data.actionUrls = actionUrls;\n }\n options.data = data;\n return options as ExtendedNotificationOptions;\n}\n\nexport interface InstallPushHandlerOptions {\n /** Title used when the payload omits one. */\n defaultTitle?: string;\n /** Icon used when the payload omits one. */\n defaultIcon?: string;\n /** Badge image (mobile). */\n defaultBadge?: string;\n /**\n * Transform the raw payload before showing the notification. Return `null`\n * to suppress the notification entirely (e.g. silent pings).\n */\n transform?: (payload: PushPayload) => PushPayload | null;\n}\n\n/**\n * Install a `push` event listener that parses the payload as JSON (with a\n * plain-text fallback) and shows a notification.\n *\n * Every `NotificationOptions` member the payload carries is forwarded —\n * `actions`, `requireInteraction`, `vibrate`, `silent`, `renotify`,\n * `timestamp`, `dir` and `lang` alongside `body`, `icon`, `badge`, `image` and\n * `tag`. `data` receives the payload's own `data`, then the top-level `url`\n * (default `\"/\"`) and `actionUrls` mapping each action id to its `url` — in that\n * order, so `payload.data` cannot override where a click goes.\n */\nexport function installPushHandler(options: InstallPushHandlerOptions = {}): void {\n const sw = getSwScope();\n const { defaultTitle = \"Notificação\", defaultIcon, defaultBadge, transform } = options;\n\n sw.addEventListener(\"push\", (event) => {\n if (!event.data) return;\n\n let raw: PushPayload;\n try {\n raw = event.data.json() as PushPayload;\n } catch {\n raw = { title: defaultTitle, body: event.data.text() };\n }\n\n const payload = transform ? transform(raw) : raw;\n if (!payload) return;\n\n const title = payload.title ?? defaultTitle;\n const notification = buildNotificationOptions(payload, defaultIcon, defaultBadge);\n\n event.waitUntil(sw.registration.showNotification(title, notification));\n });\n}\n\nexport interface InstallNotificationClickHandlerOptions {\n /**\n * Resolve the destination URL from the notification data and the clicked\n * action id (`\"\"` or `undefined` for a click on the body). Default: the\n * action's entry in `data.actionUrls`, else `data.url`, else `\"/\"`.\n *\n * The default only ever opens a page of the worker's **own origin**: a URL on\n * another origin, or with a scheme other than `http:`/`https:`, becomes `\"/\"`.\n * A resolver of your own is how an app opts into another origin — what it\n * returns may point anywhere `http:`/`https:`, and any other scheme still\n * becomes `\"/\"`.\n */\n resolveUrl?: (data: unknown, action: string | undefined) => string;\n}\n\n/**\n * Default URL resolution for a notification click.\n *\n * @param data - `event.notification.data`, as {@link installPushHandler} wrote it.\n * @param action - `event.action`; empty or `undefined` for a click on the body.\n * @returns The action's URL when it has one, else `data.url`, else `\"/\"`.\n */\nfunction defaultResolveUrl(data: unknown, action: string | undefined): string {\n if (typeof data === \"string\") return data;\n if (!data || typeof data !== \"object\") return \"/\";\n const record = data as Record<string, unknown>;\n if (action && record.actionUrls && typeof record.actionUrls === \"object\") {\n const actionUrl = (record.actionUrls as Record<string, unknown>)[action];\n if (typeof actionUrl === \"string\") return actionUrl;\n }\n return typeof record.url === \"string\" ? record.url : \"/\";\n}\n\n/**\n * The URL a click may open, given the worker's origin.\n *\n * The push payload is written by the app's server, but its `url` is often\n * copied from the record that triggered the push — a chat message, a ticket —\n * and `openWindow` opens it with the app's notification as the only context the\n * user sees. So the default resolver keeps to the worker's own origin, the same\n * line `safeLinkUrl` draws for rendered links, and a custom resolver is the\n * explicit opt-in to leave it. Neither may open a non-`http(s)` scheme.\n *\n * The URL comes back as given, not resolved, so a relative path stays relative —\n * `openWindow` resolves it against the worker's location exactly like this check\n * did.\n *\n * @param target - What the resolver returned.\n * @param origin - The worker's origin; `undefined` outside a worker, where\n * nothing can be judged and only `\"/\"` is opened.\n * @param sameOriginOnly - Whether another origin is refused too.\n * @returns `target` when it may be opened, `\"/\"` otherwise.\n */\nfunction allowedTarget(\n target: string,\n origin: string | undefined,\n sameOriginOnly: boolean,\n): string {\n if (origin === undefined) return \"/\";\n let url: URL;\n try {\n url = new URL(target, origin);\n } catch {\n return \"/\";\n }\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") return \"/\";\n if (sameOriginOnly && url.origin !== origin) return \"/\";\n return target;\n}\n\n/**\n * Whether an open window client is already showing `target`.\n *\n * `target` is resolved against the client's own URL, so a relative path\n * compares on the client's origin, and the comparison is on the full href.\n * A substring test would let `/events/1` match a client on `/events/10`, and\n * `/` match every client.\n *\n * @param clientUrl - The window client's absolute URL.\n * @param target - The URL the click resolved to, absolute or relative.\n * @returns `true` when both point at the same document.\n */\nfunction clientShowsTarget(clientUrl: string, target: string): boolean {\n try {\n return new URL(target, clientUrl).href === new URL(clientUrl).href;\n } catch {\n return false;\n }\n}\n\n/**\n * Install a `notificationclick` handler that focuses an existing client when\n * possible and falls back to opening a new window.\n *\n * A click on an action button opens that action's URL (see\n * {@link PushNotificationAction.url}); a click on the body, or on an action\n * without one, opens the top-level `url`. Only a same-origin `http(s)` URL is\n * opened by default; see {@link InstallNotificationClickHandlerOptions.resolveUrl}\n * for opting into another origin.\n */\nexport function installNotificationClickHandler(\n options: InstallNotificationClickHandlerOptions = {},\n): void {\n const sw = getSwScope();\n const resolveUrl = options.resolveUrl ?? defaultResolveUrl;\n const sameOriginOnly = options.resolveUrl === undefined;\n\n sw.addEventListener(\"notificationclick\", (event) => {\n event.notification.close();\n const target = allowedTarget(\n resolveUrl(event.notification.data, event.action),\n sw.location?.origin,\n sameOriginOnly,\n );\n\n event.waitUntil(\n (async () => {\n const clients = await sw.clients.matchAll({\n type: \"window\",\n includeUncontrolled: true,\n });\n for (const client of clients) {\n if (clientShowsTarget(client.url, target)) {\n return client.focus();\n }\n }\n return sw.clients.openWindow(target);\n })(),\n );\n });\n}\n\n/**\n * Install a `message` listener that activates a waiting worker when the host\n * app sends `{ type: \"SKIP_WAITING\" }`.\n */\nexport function installSkipWaitingListener(): void {\n const sw = getSwScope() as SwGlobal & {\n addEventListener(\n type: \"message\",\n listener: (event: { data?: { type?: string } }) => void,\n ): void;\n };\n sw.addEventListener(\"message\", (event) => {\n if (event.data?.type === \"SKIP_WAITING\") {\n void sw.skipWaiting();\n }\n });\n}\n"],"mappings":";AA+CA,SAASA,IAAuB;CAC5B,OAAO;AACX;AA0EA,IAAMC,IAAoB;CACtB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACJ;AAoBA,SAASC,EACL,GACA,GACA,GAC2B;CAC3B,IAAMC,IAAmC;EACrC,MAAMC,EAAQ,QAAQC;EACtB,OAAOD,EAAQ,SAASE;CAC5B;CACA,KAAK,IAAMC,KAAON,GACd,AAAIG,EAAQG,OAAS,KAAA,MAAW,EAAQA,KAAOH,EAAQG;CAG3D,IAAMC,IAAgC;EAAE,GAAIJ,EAAQ,QAAQ,CAAC;EAAI,KAAKA,EAAQ,OAAO;CAAI;CAEzF,IADA,OAAOI,EAAK,YACRJ,EAAQ,SAAS;EACjB,IAAMK,IAAqC,CAAC;EAK5C,AAJA,EAAQ,UAAUL,EAAQ,QAAQ,KAAK,EAAE,QAAK,GAAGM,SACzCC,MAAQ,KAAA,MAAW,EAAWD,EAAO,UAAUC,IAC5CD,EACV,GACG,OAAO,KAAKD,CAAU,CAAC,CAAC,SAAS,MAAG,EAAK,aAAaA;CAC9D;CAEA,OADA,EAAQ,OAAOD,GACRL;AACX;AA2BA,SAAgBS,EAAmB,IAAqC,CAAC,GAAS;CAC9E,IAAMC,IAAKb,EAAW,GAChB,EAAE,kBAAe,eAAe,gBAAa,iBAAc,iBAAcG;CAE/E,EAAG,iBAAiB,SAAS,MAAU;EACnC,IAAI,CAACW,EAAM,MAAM;EAEjB,IAAIC;EACJ,IAAI;GACA,IAAMD,EAAM,KAAK,KAAK;EAC1B,QAAQ;GACJ,IAAM;IAAE,OAAOE;IAAc,MAAMF,EAAM,KAAK,KAAK;GAAE;EACzD;EAEA,IAAMV,IAAUa,IAAYA,EAAUF,CAAG,IAAIA;EAC7C,IAAI,CAACX,GAAS;EAEd,IAAMc,IAAQd,EAAQ,SAASY,GACzBG,IAAejB,EAAyBE,GAASC,GAAaC,CAAY;EAEhF,EAAM,UAAUO,EAAG,aAAa,iBAAiBK,GAAOC,CAAY,CAAC;CACzE,CAAC;AACL;AAwBA,SAASC,EAAkB,GAAe,GAAoC;CAC1E,IAAI,OAAOZ,KAAS,UAAU,OAAOA;CACrC,IAAI,CAACA,KAAQ,OAAOA,KAAS,UAAU,OAAO;CAC9C,IAAMa,IAASb;CACf,IAAIE,KAAUW,EAAO,cAAc,OAAOA,EAAO,cAAe,UAAU;EACtE,IAAMC,IAAaD,EAAO,WAAuCX;EACjE,IAAI,OAAOY,KAAc,UAAU,OAAOA;CAC9C;CACA,OAAO,OAAOD,EAAO,OAAQ,WAAWA,EAAO,MAAM;AACzD;AAsBA,SAASE,EACL,GACA,GACA,GACM;CACN,IAAIC,MAAW,KAAA,GAAW,OAAO;CACjC,IAAIb;CACJ,IAAI;EACA,IAAM,IAAI,IAAIc,GAAQD,CAAM;CAChC,QAAQ;EACJ,OAAO;CACX;CAGA,OAFIb,EAAI,aAAa,WAAWA,EAAI,aAAa,YAC7Ce,KAAkBf,EAAI,WAAWa,IAAe,MAC7CC;AACX;AAcA,SAASE,EAAkB,GAAmB,GAAyB;CACnE,IAAI;EACA,OAAO,IAAI,IAAIF,GAAQG,CAAS,CAAC,CAAC,SAAS,IAAI,IAAIA,CAAS,CAAC,CAAC;CAClE,QAAQ;EACJ,OAAO;CACX;AACJ;AAYA,SAAgBC,EACZ,IAAkD,CAAC,GAC/C;CACJ,IAAMhB,IAAKb,EAAW,GAChB8B,IAAa3B,EAAQ,cAAciB,GACnCM,IAAiBvB,EAAQ,eAAe,KAAA;CAE9C,EAAG,iBAAiB,sBAAsB,MAAU;EAChD,EAAM,aAAa,MAAM;EACzB,IAAMsB,IAASF,EACXO,EAAWhB,EAAM,aAAa,MAAMA,EAAM,MAAM,GAChDD,EAAG,UAAU,QACba,CACJ;EAEA,EAAM,WACD,YAAY;GACT,IAAMK,IAAU,MAAMlB,EAAG,QAAQ,SAAS;IACtC,MAAM;IACN,qBAAqB;GACzB,CAAC;GACD,KAAK,IAAMmB,KAAUD,GACjB,IAAIJ,EAAkBK,EAAO,KAAKP,CAAM,GACpC,OAAOO,EAAO,MAAM;GAG5B,OAAOnB,EAAG,QAAQ,WAAWY,CAAM;EACvC,EAAA,CAAG,CACP;CACJ,CAAC;AACL;AAMA,SAAgBQ,IAAmC;CAC/C,IAAMpB,IAAKb,EAAW;CAMtB,EAAG,iBAAiB,YAAY,MAAU;EACtC,AAAIc,EAAM,MAAM,SAAS,kBACrBD,EAAQ,YAAY;CAE5B,CAAC;AACL"}
|
package/dist/sw.d.ts
CHANGED
|
@@ -258,6 +258,43 @@ export declare interface InstallBackgroundSyncOptions {
|
|
|
258
258
|
* persisting and replaying every header, credentials included.
|
|
259
259
|
*/
|
|
260
260
|
credentialHeaders?: readonly string[];
|
|
261
|
+
/**
|
|
262
|
+
* Origins besides the worker's own that a replayed request may carry a
|
|
263
|
+
* credential header to — typically the API host when it is not the app's
|
|
264
|
+
* origin (`["https://api.acme.com"]`). Default `[]`: only the worker's origin.
|
|
265
|
+
*
|
|
266
|
+
* A credential re-applied at replay comes from the page, so it is the app's
|
|
267
|
+
* credential, whatever the request originally carried under the same header
|
|
268
|
+
* name. Before 0.81.0 the worker checked only the name, and a mutation to a
|
|
269
|
+
* third party that failed offline carrying that party's `Authorization` was
|
|
270
|
+
* replayed to it with the app's bearer (#523). Now, with the rule
|
|
271
|
+
* `isTrustedCredentialTarget` enforces on the live `ApiClient`:
|
|
272
|
+
* - an entry that carried a credential header to an origin outside this
|
|
273
|
+
* scope is dropped before `beforeReplay` runs — re-sending it without the
|
|
274
|
+
* credential would be a request the app never made, and with the app's
|
|
275
|
+
* credential, a leak;
|
|
276
|
+
* - a request returned by `beforeReplay` (custom or default) that carries a
|
|
277
|
+
* header from `credentialHeaders` to an origin outside this scope is
|
|
278
|
+
* dropped instead of sent.
|
|
279
|
+
*
|
|
280
|
+
* Both drops warn once per origin per replay in development builds. Entries
|
|
281
|
+
* with no credential header are not affected: the browser attaches that
|
|
282
|
+
* origin's own cookies, as it did on the original attempt.
|
|
283
|
+
*/
|
|
284
|
+
trustedOrigins?: readonly string[];
|
|
285
|
+
/**
|
|
286
|
+
* How an `"unknown"` owner is handled on an entry with no stripped
|
|
287
|
+
* credentials (see {@link BackgroundSyncOwnerPolicy}). Default `"strict"`.
|
|
288
|
+
*
|
|
289
|
+
* `"lenient"` restores the replay-as-is of 0.77.0 and earlier for an app without sign-in,
|
|
290
|
+
* where no page ever reports a principal. Do not use it with a cookie
|
|
291
|
+
* session: the browser attaches on replay the cookie of whoever is signed in
|
|
292
|
+
* at that moment, so a mutation queued by A whose owner was never stamped
|
|
293
|
+
* (issuing tab closed, empty `clientId`, stamp lost, legacy entry) goes out
|
|
294
|
+
* with B's session. Entries with stripped credentials ask the pages either
|
|
295
|
+
* way. Ignored when a custom `beforeReplay` is given.
|
|
296
|
+
*/
|
|
297
|
+
ownerPolicy?: BackgroundSyncOwnerPolicy;
|
|
261
298
|
/**
|
|
262
299
|
* Prepare each queued request right before it is replayed: return the
|
|
263
300
|
* request to send (typically with the current credentials applied) or
|
|
@@ -285,20 +322,11 @@ export declare interface InstallBackgroundSyncOptions {
|
|
|
285
322
|
* `maxRetentionMinutes` (and warns once per replay in development builds) —
|
|
286
323
|
* so a page that never calls {@link provideBackgroundSyncCredentials} leaves
|
|
287
324
|
* every entry it queued `"unknown"`, and none of them is replayed.
|
|
288
|
-
*/
|
|
289
|
-
/**
|
|
290
|
-
* How an `"unknown"` owner is handled on an entry with no stripped
|
|
291
|
-
* credentials (see {@link BackgroundSyncOwnerPolicy}). Default `"strict"`.
|
|
292
325
|
*
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
* at that moment, so a mutation queued by A whose owner was never stamped
|
|
297
|
-
* (issuing tab closed, empty `clientId`, stamp lost, legacy entry) goes out
|
|
298
|
-
* with B's session. Entries with stripped credentials ask the pages either
|
|
299
|
-
* way. Ignored when a custom `beforeReplay` is given.
|
|
326
|
+
* Whatever the hook returns is still held to `trustedOrigins`: a credential
|
|
327
|
+
* header bound for an origin outside the worker's own and `trustedOrigins` is
|
|
328
|
+
* never sent.
|
|
300
329
|
*/
|
|
301
|
-
ownerPolicy?: BackgroundSyncOwnerPolicy;
|
|
302
330
|
beforeReplay?: (request: Request, context: BackgroundSyncReplayContext) => Request | null | Promise<Request | null>;
|
|
303
331
|
}
|
|
304
332
|
|
|
@@ -308,7 +336,9 @@ export declare interface InstallBackgroundSyncOptions {
|
|
|
308
336
|
*
|
|
309
337
|
* A click on an action button opens that action's URL (see
|
|
310
338
|
* {@link PushNotificationAction.url}); a click on the body, or on an action
|
|
311
|
-
* without one, opens the top-level `url`.
|
|
339
|
+
* without one, opens the top-level `url`. Only a same-origin `http(s)` URL is
|
|
340
|
+
* opened by default; see {@link InstallNotificationClickHandlerOptions.resolveUrl}
|
|
341
|
+
* for opting into another origin.
|
|
312
342
|
*/
|
|
313
343
|
export declare function installNotificationClickHandler(options?: InstallNotificationClickHandlerOptions): void;
|
|
314
344
|
|
|
@@ -317,6 +347,12 @@ export declare interface InstallNotificationClickHandlerOptions {
|
|
|
317
347
|
* Resolve the destination URL from the notification data and the clicked
|
|
318
348
|
* action id (`""` or `undefined` for a click on the body). Default: the
|
|
319
349
|
* action's entry in `data.actionUrls`, else `data.url`, else `"/"`.
|
|
350
|
+
*
|
|
351
|
+
* The default only ever opens a page of the worker's **own origin**: a URL on
|
|
352
|
+
* another origin, or with a scheme other than `http:`/`https:`, becomes `"/"`.
|
|
353
|
+
* A resolver of your own is how an app opts into another origin — what it
|
|
354
|
+
* returns may point anywhere `http:`/`https:`, and any other scheme still
|
|
355
|
+
* becomes `"/"`.
|
|
320
356
|
*/
|
|
321
357
|
resolveUrl?: (data: unknown, action: string | undefined) => string;
|
|
322
358
|
}
|
|
@@ -361,8 +397,9 @@ export declare interface InstallPrecacheOptions {
|
|
|
361
397
|
* Every `NotificationOptions` member the payload carries is forwarded —
|
|
362
398
|
* `actions`, `requireInteraction`, `vibrate`, `silent`, `renotify`,
|
|
363
399
|
* `timestamp`, `dir` and `lang` alongside `body`, `icon`, `badge`, `image` and
|
|
364
|
-
* `tag`. `data` receives the top-level `url`
|
|
365
|
-
*
|
|
400
|
+
* `tag`. `data` receives the payload's own `data`, then the top-level `url`
|
|
401
|
+
* (default `"/"`) and `actionUrls` mapping each action id to its `url` — in that
|
|
402
|
+
* order, so `payload.data` cannot override where a click goes.
|
|
366
403
|
*/
|
|
367
404
|
export declare function installPushHandler(options?: InstallPushHandlerOptions): void;
|
|
368
405
|
|
|
@@ -386,9 +423,10 @@ export declare interface InstallPushHandlerOptions {
|
|
|
386
423
|
* `respondWith`), so a later {@link installPrecache} can handle them.
|
|
387
424
|
*
|
|
388
425
|
* A request carrying a credential header (`Authorization`, `X-Api-Key`, … — the
|
|
389
|
-
* route's `credentialHeaders`)
|
|
426
|
+
* route's `credentialHeaders`), or a CORS `fetch` sent with
|
|
427
|
+
* `credentials: "include"`, is also left untouched unless its route sets
|
|
390
428
|
* `cacheAuthenticated`, and a response with `Cache-Control: no-store`
|
|
391
|
-
* (or `private`, without that opt-in) is served but never stored. `maxAgeSeconds`
|
|
429
|
+
* (or `private`, without that opt-in) or `Vary: *` is served but never stored. `maxAgeSeconds`
|
|
392
430
|
* applies to every read, including the offline fallback: an expired entry is a
|
|
393
431
|
* miss, never a last resort. Under `rangeRequests`, the full resource is cached
|
|
394
432
|
* once (the `Range` header stripped) and each range is sliced from it.
|
|
@@ -417,13 +455,25 @@ export declare function installSkipWaitingListener(): void;
|
|
|
417
455
|
* keeps an entry pending when one it stripped is missing), or `null` when there
|
|
418
456
|
* is no session — the worker then drops the entry instead of replaying it
|
|
419
457
|
* anonymously. A throw from `getHeaders` or `getPrincipal` is answered as an
|
|
420
|
-
* explicit `{ error: true }`.
|
|
421
|
-
*
|
|
422
|
-
*
|
|
458
|
+
* explicit `{ error: true }`.
|
|
459
|
+
*
|
|
460
|
+
* The worker applies only the header names the original request carried, and
|
|
461
|
+
* only to the worker's own origin or the `trustedOrigins` given to
|
|
462
|
+
* {@link installBackgroundSync}: an entry bound for any other origin is dropped
|
|
463
|
+
* without asking this page. Matching the name is not enough on its own — a
|
|
464
|
+
* request to a third party carries *that party's* `Authorization`, and until
|
|
465
|
+
* 0.81.0 its replay received the app's bearer (#523). `getHeaders` still gets
|
|
466
|
+
* the URL, for an app whose trusted origins need different credentials.
|
|
423
467
|
*
|
|
424
468
|
* Pass `getPrincipal` to bind each entry to whoever queued it: the worker asks
|
|
425
469
|
* the issuing tab right after queueing, asks every tab again before replaying,
|
|
426
|
-
* and drops the entry when no signed-in tab is its owner.
|
|
470
|
+
* and drops the entry when no signed-in tab is its owner. Without it every
|
|
471
|
+
* entry is `"unbound"` and replays with whichever session answers — the next
|
|
472
|
+
* account's bearer included, unless `clearSessionData()` ran on logout — so a
|
|
473
|
+
* development build warns once per call (#531). Treating an unbound
|
|
474
|
+
* credentialed entry as `"unknown"` instead would change nothing: an
|
|
475
|
+
* `"unknown"` entry replays when no answering tab reports a principal, which is
|
|
476
|
+
* exactly the page that omits `getPrincipal`.
|
|
427
477
|
*
|
|
428
478
|
* @param getHeaders - Resolves the headers for one queued request at replay.
|
|
429
479
|
* @param options - `getPrincipal` binds entries to their owner.
|
|
@@ -514,7 +564,14 @@ export declare interface PushPayload {
|
|
|
514
564
|
dir?: NotificationDirection;
|
|
515
565
|
/** BCP 47 language tag of the title and body. */
|
|
516
566
|
lang?: string;
|
|
517
|
-
/**
|
|
567
|
+
/**
|
|
568
|
+
* Arbitrary extra data forwarded to `event.notification.data`.
|
|
569
|
+
*
|
|
570
|
+
* Its `url` and `actionUrls` keys are overwritten by the top-level `url` and
|
|
571
|
+
* the actions' `url`s: those two keys decide where a click goes, and a
|
|
572
|
+
* free-form bag — often filled from the record that triggered the push — must
|
|
573
|
+
* not be able to redirect it.
|
|
574
|
+
*/
|
|
518
575
|
data?: Record<string, unknown>;
|
|
519
576
|
}
|
|
520
577
|
|
|
@@ -645,8 +702,9 @@ export declare interface RuntimeRoute {
|
|
|
645
702
|
*/
|
|
646
703
|
rangeRequests?: boolean;
|
|
647
704
|
/**
|
|
648
|
-
* Opt in to caching requests that carry a credential
|
|
649
|
-
* `credentialHeaders`)
|
|
705
|
+
* Opt in to caching credentialed requests — those that carry a credential
|
|
706
|
+
* header (see `credentialHeaders`) or are a CORS-mode `fetch` sent with
|
|
707
|
+
* `credentials: "include"` — and responses marked `Cache-Control: private`.
|
|
650
708
|
* Default `false`.
|
|
651
709
|
*
|
|
652
710
|
* Cache Storage is keyed by URL and outlives the session: without this
|
|
@@ -663,11 +721,18 @@ export declare interface RuntimeRoute {
|
|
|
663
721
|
* `clearSessionData()` on logout, which removes the entries themselves.
|
|
664
722
|
* `Cache-Control: no-store` is never cached, opt-in or not.
|
|
665
723
|
*
|
|
666
|
-
* A cookie session is
|
|
667
|
-
*
|
|
668
|
-
*
|
|
724
|
+
* A cookie session is only partly visible here: the worker cannot see
|
|
725
|
+
* `Cookie`. What it can see is a `fetch` that asked for cookies explicitly —
|
|
726
|
+
* `mode: "cors"` with `credentials: "include"`, which is what `ApiClient`
|
|
727
|
+
* sends under `withCredentials` — and since 0.82.0 that request is treated
|
|
728
|
+
* as credentialed too (#531). `<img>`, `<audio>` and `<video>` also send
|
|
729
|
+
* `credentials: "include"`, but in `no-cors` mode, so they keep caching. A
|
|
730
|
+
* plain same-origin `fetch` (`credentials: "same-origin"`, the default)
|
|
731
|
+
* carries the cookie without saying so; such an API stays private only
|
|
669
732
|
* through `Cache-Control: private`/`no-store` on its responses and
|
|
670
|
-
* `clearSessionData()` on logout.
|
|
733
|
+
* `clearSessionData()` on logout. Opting a cookie request in caches it by
|
|
734
|
+
* URL — there is no credential header to vary on — so `clearSessionData()`
|
|
735
|
+
* is then its only isolation.
|
|
671
736
|
*/
|
|
672
737
|
cacheAuthenticated?: boolean;
|
|
673
738
|
/**
|