@ultimat3/pwa 26.0.0 → 26.1.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/CLAUDE.md +16 -5
- package/README.md +96 -3
- package/package.json +9 -3
- package/src/client.ts +37 -0
- package/src/errors.ts +156 -1
- package/src/index.ts +64 -20
- package/src/install.ts +2 -1
- package/src/push-actions.ts +104 -0
- package/src/push-bytes.ts +39 -0
- package/src/push-client.ts +182 -0
- package/src/push-decrypt.ts +92 -0
- package/src/push-encrypt.ts +161 -0
- package/src/push-hosts.ts +34 -0
- package/src/push-runtime.ts +87 -0
- package/src/push-schema.ts +27 -0
- package/src/push-send.ts +191 -0
- package/src/push-store-pg.ts +86 -0
- package/src/push-store.ts +54 -0
- package/src/skew.ts +39 -0
- package/src/vapid-keys.ts +72 -0
- package/src/vapid.ts +167 -0
- package/src/version-skew.ts +2 -32
- package/src/web-push.ts +124 -0
package/CLAUDE.md
CHANGED
|
@@ -4,16 +4,23 @@ Owns: `sw.js` generation, caching strategies, precache manifest, version skew, w
|
|
|
4
4
|
manifest, icons, offline fallback, background-sync trigger, push, install prompt.
|
|
5
5
|
|
|
6
6
|
Tier 4. May import tiers 0–3: `core`, `schema`, `i18n`, `money`, `time`, `cache`, `seo`,
|
|
7
|
-
`entity`, `policy`, `http`, `action`, `query`. **Never** `render`, `mcp`, `ai`, `manifest
|
|
8
|
-
(sideways), never `ui`/`cli` (upward).
|
|
7
|
+
`entity`, `policy`, `http`, `action`, `query`. **Never** `render`, `mcp`, `ai`, `manifest`,
|
|
8
|
+
`notify` (sideways), never `ui`/`cli` (upward). Tier 4 is its FLOOR since 26.1.0 — `pushSubscribe`
|
|
9
|
+
builds an `action` — so it has no `FLOOR_ABOVE` row; `render` keeps one and holds the pair level.
|
|
10
|
+
|
|
11
|
+
Three entry points: `.` (server), `./client` (`client.ts` — THE browser entry: install prompt,
|
|
12
|
+
skew, the browser half of push; its graph is `install.ts`, `skew.ts`, `push-client.ts` and
|
|
13
|
+
`@ultimat3/core/page`, held by `client-bundle.test.ts` — never import `@ultimat3/core`'s barrel or
|
|
14
|
+
`errors.ts` from those three) and `./schema` (`push-schema.ts` — the one DDL leaf every
|
|
15
|
+
role's boot loads, `@ultimat3/mcp/schema`'s shape; `serve-graph.test.ts` exempts it by name).
|
|
9
16
|
|
|
10
17
|
| Rule | Detail |
|
|
11
18
|
|---|---|
|
|
12
19
|
| `sw.js` | generated, never hand-written, never committed. No file in this package is a SW. |
|
|
13
|
-
| Determinism | no `Date.now()`, no randomness, sort every collection. Same input → same bytes. `subscriptionState` took `now = Date.now()` and was the one exception; it takes a `Clock` `As of 2026-08-23`, so a frozen clock freezes this package too. |
|
|
20
|
+
| Determinism | no `Date.now()`, no randomness, sort every collection. Same input → same bytes. `subscriptionState` took `now = Date.now()` and was the one exception; it takes a `Clock` `As of 2026-08-23`, so a frozen clock freezes this package too. **Key material is the one exception, and it is injectable**: RFC 8291 needs a fresh salt and one-message ECDH key per push (reusing either reuses a GCM nonce under one key) — `encryptPushMessage`'s `options.salt` / `options.ephemeral` are the seam the Appendix A test pins — and `generateVapidKeys` rolls a pair. Every clock read in push (`vapidAuthorization`'s `exp`, expiry) takes a `Clock`. |
|
|
14
21
|
| Rule ORDER | most specific first, then the path. `ruleFor` in the emitted worker returns the FIRST pattern that matches and has no notion of specificity, so the sort IS the routing decision. Alphabetical, `:` (0x3A) and `*` (0x2A) sort before every letter: `/posts/:id` shadowed `/posts/new`, and one `/*` catch-all shadowed the whole table — every `PRECACHE_MANIFEST` entry downloaded at install and never looked up, and a cacheable route served `network-only`, which offline is the `/offline` document. The order is `@ultimat3/core`'s `routeRank` — the ONE route precedence, also `@ultimat3/render`'s `CompiledPattern.specificity`: the request router's order, segment by segment. Never a local copy, never a sum. |
|
|
15
22
|
| A generated header | names something that regenerates the file. `sw.js` says `regenerate: x build`, and `As of 2026-08` a build really writes it (#390); it named the CALL for as long as none did. |
|
|
16
|
-
| What is WIRED, and what is not | **every generator here has a caller, `As of 2026-08`.** `generateWebManifest`, `renderThemeColorMeta`, `planIcons`, `appleTouchLinks` and `BuiltinImagePipeline` through `pwa-artifacts.ts` / `runtime-assets.ts` (#362); `generateServiceWorker` — and with it `buildPrecacheManifest`, `offlineFallbackSource`, `backgroundSyncSource` and `pushSource` — through `sw-artifacts.ts` (#390). So `x dev`, the container and the static export emit `manifest.webmanifest`, `sw.js` and `x-sw-register.js` alike. The worker half landed a release later because a bad `sw.js` is sticky: it waited on a real browser check (`packages/cli/e2e/service-worker.e2e.test.ts`), which nothing in the gate could run until #400. **`pwa.push` is
|
|
23
|
+
| What is WIRED, and what is not | **every generator here has a caller, `As of 2026-08`.** `generateWebManifest`, `renderThemeColorMeta`, `planIcons`, `appleTouchLinks` and `BuiltinImagePipeline` through `pwa-artifacts.ts` / `runtime-assets.ts` (#362); `generateServiceWorker` — and with it `buildPrecacheManifest`, `offlineFallbackSource`, `backgroundSyncSource` and `pushSource` — through `sw-artifacts.ts` (#390). So `x dev`, the container and the static export emit `manifest.webmanifest`, `sw.js` and `x-sw-register.js` alike. The worker half landed a release later because a bad `sw.js` is sticky: it waited on a real browser check (`packages/cli/e2e/service-worker.e2e.test.ts`), which nothing in the gate could run until #400. **`pwa.push` is wired too, `As of 26.1.0`** (#710): the boot (`@ultimat3/cli`'s `runtime-push.ts`) resolves the VAPID pair and calls `installWebPush`, the web role hands `installedVapid()` to `generateServiceWorker` (push handler) and the head (`<meta name="x-push-key">`), and a static export — no runtime behind it — is the one build `serviceWorkerWarnings` still reports |
|
|
17
24
|
| Route input | `PwaRoute` is a **structural** view of render's `RouteDescriptor`. Never import render. |
|
|
18
25
|
| Strategy choice | derived from render mode via `MODE_STRATEGY`. Per-route override only. Every row is `network-first` `As of 22.3.2`: every rule is a DOCUMENT, and `static: 'cache-first'` / `isr`,`stream: 'stale-while-revalidate'` answered online navigations with the old deploy's HTML (and so its old hashed assets) until Shift+F5. Precache ≠ cache-first — the precached copy is the offline answer. |
|
|
19
26
|
| Worker update | `self.skipWaiting()` in `install`, `As of 22.3.2` — worker-side, because the browsers that must converge run a 22.3.1 worker and register script that never post `skip-waiting`. Safe for an open tab: the worker routes no chunk. Never a reload — the next navigation is fresh. The `skip-waiting` message stays. |
|
|
@@ -44,7 +51,11 @@ Tier 4. May import tiers 0–3: `core`, `schema`, `i18n`, `money`, `time`, `cach
|
|
|
44
51
|
| The installing page | cached on `activate`. It loaded before the worker controlled it, so no strategy saw it and an `offline: 'runtime'` route was unavailable offline until a second online visit. After `clients.claim()` each controlled window's URL runs through its OWN route's strategy (`warm`) — only rules with `c: 'pages'`, same origin, not `NEVER_CACHE` — never a second caching rule. Best effort; a failed warm-up never fails activation. It runs AFTER activation, never inside its `waitUntil`: a fetch event waits for the worker to finish activating, so a warm-up copying a streamed page's whole body there held the claimed tab's every request — the `/feed` e2e hang. The offline page read (`pageOffline`) awaits it instead. `service-worker-runtime.test.ts` runs it. |
|
|
45
52
|
| Outbox drain | the worker SENDS NOTHING, `As of 21.0.0`. The outbox is `@ultimat3/realtime`'s and lives in the page (IndexedDB), so the `sync` event — and the `{type:'flush-outbox'}` fallback message, posted by `registerBackgroundSyncSource` wherever `registration.sync` is absent (Safari, Firefox) — both run `drainOutbox`, which posts `OUTBOX_DRAIN_MESSAGE` (`@ultimat3/core`) to EVERY open window client; realtime's listener drains. No client open = the handler resolves and the next load drains; it never fakes work. `/_x/outbox/flush`, `DEFAULT_FLUSH_ENDPOINT`, and `BackgroundSyncOptions` were deleted, and `X_PWA_SYNC_FLUSH_FAILED` / `X_PWA_SYNC_INCOMPLETE` are still registered but thrown by nothing (shipped codes are stable forever): the worker POSTed to a route nothing in the framework ever mounted. The fallback branch lives in the shared message handler, so `CAPABILITY_SW_MARKERS.backgroundSync` carrying `d.type==='flush-outbox'` is what keeps it gated with the `drainOutbox` it calls. `background-sync.test.ts` and `service-worker-runtime.test.ts` run the emitted code and assert the posted messages. |
|
|
46
53
|
| Outbox | queue, replay, retry and idempotency are all `@ultimat3/realtime`'s. This package only registers the sync trigger and schedules **no retry** — `RetryPolicy`, `DEFAULT_RETRY`, `retryDelayMs`, `shouldRetry` and `BackgroundSyncOptions.retry` were deleted 2026-08-23. |
|
|
47
|
-
| Push strings | i18n keys only, rendered per subscriber locale. Never a literal. |
|
|
54
|
+
| Push strings | i18n keys only, rendered per subscriber locale. Never a literal. The runtime's `translate` defaults to `@ultimat3/i18n`'s `translatorFor(subscription.locale)` — the app's registered catalogs. |
|
|
55
|
+
| Web Push crypto | WebCrypto only, no dependency (`docs/idea/18-build-vs-wrap.md`). `push-encrypt.ts` is RFC 8291 + RFC 8188 (one `aes128gcm` record, delimiter `0x02`, no padding) and is pinned by RFC 8291 Appendix A byte for byte, every intermediate included; it is `scripts/seal-calls.ts`'s `STANDARD_CIPHERS` row, because the key is agreed with the subscriber's browser and is never the app's — `seal()` cannot be what it calls. `vapid.ts` is RFC 8292: `aud` is the endpoint's ORIGIN, `exp` 12 h (≤ 24 h), the signature WebCrypto's raw `r‖s` (= JWS ES256). WebCrypto does not check that `d` belongs to `x`/`y`, so `assertVapidPair` signs and verifies a probe — the boot runs it once. |
|
|
56
|
+
| VAPID keys | env, never config: `ULTIMATE_VAPID_PUBLIC_KEY` + `ULTIMATE_VAPID_PRIVATE_KEY`, printable formats (`web-push`'s). `resolveVapidKeys(env)`: both set → checked pair; neither, locally → `DEV_VAPID_KEYS` (published); neither or the dev key when `devSecretsRefused` (core's rule, fails closed), or half a pair anywhere → `X_PWA_VAPID_KEY_MISSING`. `pwa.vapid` holds the `subject` only. |
|
|
57
|
+
| Push sending | `sendPushMessage` → `delivered` (2xx), `gone` (404/410 — delete, never retry), or a throw: `X_PWA_PUSH_FAILED` (`retry-after`: 429 / 5xx / no connection, `Retry-After` in `meta`), `X_PWA_PUSH_REJECTED` (terminal). `pushToActor` tries EVERY device, deletes the gone, the expired and the 401/403-refused (made with another VAPID key), logs the refused, and rethrows the first retryable failure LAST — so the notifier job retries, and devices that got it get it again; the `tag` is what makes that a replacement. The endpoint must be `https:` on a LISTED push service (`push-hosts.ts`: the built-in four plus `pwa.vapid.pushHosts`, dot-boundary suffix match), checked at subscribe and at send: it came from a request body, and a hostname resolves wherever its owner points it — the list, never the name's shape or its DNS answer, is the screen. `redirect: 'manual'`, a `PUSH_SEND_TIMEOUT_MS` deadline per POST, and a caller's abort rethrown as is — never the retryable class. |
|
|
58
|
+
| Subscriptions | keyed by ENDPOINT (a browser subscribed by a second person is theirs now), stored against `ctx.actor.id` — never an id from the body. `pushSubscribe` / `pushUnsubscribe` are factories over `action` (`PRIMITIVE_FACTORIES` rows); an agent is refused in the policy. `x_push_subscriptions` is DDL applied by the boot (`FRAMEWORK_SCHEMA`), never an `entity()`. |
|
|
48
59
|
| `renotify` | never without a `tag`, at BOTH ends. The Notifications spec makes `showNotification` reject with a `TypeError` for the pair, so the device shows **nothing** — the failure a `renotify: true` with no collapse key produces is the whole notification disappearing. `renderPushPayload` drops the flag and files a `warnings` entry (a knob with no effect is worse than no knob), and the emitted handler recomputes it as `!!d.renotify&&!!d.tag` because a push body is composed by whatever holds the VAPID key. `push.test.ts`'s `showNotification` stub throws the spec's TypeError, so a stub that tolerates the pair cannot go green. |
|
|
49
60
|
| Push URLs | `PushPayload.url` is a PATH and `WindowClient.url` is absolute, so `notificationclick` resolves against `self.location.origin` before comparing. Unresolved, the focus-existing-tab loop matched nothing and every tap opened a second window. A URL that does not resolve to `self.location.origin` (another host, a scheme, malformed, a non-string) opens the root instead (`tapTarget`). The `push` body is parsed INSIDE `waitUntil` (`pushData`): plain text is the body, JSON that is no object is `{}`. `push-handler.test.ts`. |
|
|
50
61
|
| Colours | `@ultimat3/core`'s `PwaColors`, passed in as `WebManifestInput.tokens`. Never a hex literal in this package — a test fixture asserting the parser is the one exception. This package declared `ThemeTokens`/`SchemeColors` itself until 2026-08-27: structurally identical to what `app.config.ts` already carried, with no map between them and nothing asserting they agreed, which is the axiom-1 defect the two `PwaConfig`s were. Deleted; tier 0 owns the shape. |
|
package/README.md
CHANGED
|
@@ -8,6 +8,32 @@ the second copy is the one nobody updates.
|
|
|
8
8
|
const { source, precache, warnings } = generateServiceWorker(describePages(), config, buildId);
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
## Browser entry: `@ultimat3/pwa/client`
|
|
12
|
+
|
|
13
|
+
`As of 26.1.0`. Everything a page or an island calls comes from **`@ultimat3/pwa/client`**, never
|
|
14
|
+
from `@ultimat3/pwa`: the barrel reaches the icon pipeline (core's image transform, Node-only), the
|
|
15
|
+
service-worker generator and the push sender, and an island importing it does not build.
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import {
|
|
19
|
+
detectSkew,
|
|
20
|
+
installController,
|
|
21
|
+
MIN_ENGAGEMENT_MS,
|
|
22
|
+
subscribeToPush,
|
|
23
|
+
} from '@ultimat3/pwa/client';
|
|
24
|
+
import { APP_UPDATE_MESSAGE, CLIENT_BUILD_META } from '@ultimat3/core/page';
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
| Need | From `@ultimat3/pwa/client` |
|
|
28
|
+
|---|---|
|
|
29
|
+
| the install prompt, never on first paint | `installController` (+ `InstallOptions`, `InstallHost`, `InstallController`, `InstallOutcome`, `BeforeInstallPromptEventLike`, `ReadSignal`), `iosInstallGuidance` (+ `IosGuidance`), `MIN_ENGAGEMENT_MS` (30 000 — the default `minEngagementMs`) |
|
|
30
|
+
| "a new version is live" | `AppUpdateAvailable` (the worker's message), `detectSkew` (+ `SkewState`). The message's `type` value is core's `APP_UPDATE_MESSAGE`, from `@ultimat3/core/page` — one name, one home |
|
|
31
|
+
| Web Push | `subscribeToPush`, `unsubscribeFromPush`, `pushPermission`, `browserPushHost`, `PUSH_KEY_META` (+ their types) |
|
|
32
|
+
|
|
33
|
+
Its graph is three modules of this package plus `@ultimat3/core/page`; `client-bundle.test.ts`
|
|
34
|
+
bundles it for the browser and refuses a fourth, an image module or a `node:` import. The server
|
|
35
|
+
barrel keeps exporting the same names for server code and tests.
|
|
36
|
+
|
|
11
37
|
## Render mode → runtime strategy
|
|
12
38
|
|
|
13
39
|
| Render mode | Strategy | Why |
|
|
@@ -72,7 +98,7 @@ placeholders a failed image or font request gets — each served only from the p
|
|
|
72
98
|
|
|
73
99
|
| Capability | Manifest member | SW code |
|
|
74
100
|
|---|---|---|
|
|
75
|
-
| `push` | — | `push` + `notificationclick` listeners; a tap opens a same-origin URL only |
|
|
101
|
+
| `push` | — | `push` + `notificationclick` listeners; a tap opens a same-origin URL only. Emitted only when a `vapid` key comes with it — the web role passes `installedVapid()` |
|
|
76
102
|
| `backgroundSync` | — | `sync` listener that tells every open tab to drain realtime's outbox (`OUTBOX_DRAIN_MESSAGE`) |
|
|
77
103
|
| `badging` | — | `navigator.setAppBadge` after a push |
|
|
78
104
|
| `shareTarget` | `share_target` | — |
|
|
@@ -88,6 +114,59 @@ gate. `CAPABILITY_SW_MARKERS` is checked against the emitted `sw.js` in both dir
|
|
|
88
114
|
here that the generator does not honour is a failing test rather than an installed app announcing a
|
|
89
115
|
capability nothing implements.
|
|
90
116
|
|
|
117
|
+
## Web Push
|
|
118
|
+
|
|
119
|
+
`pwa: { push: true, vapid: { subject: 'mailto:ops@example.com' } }` in `app.config.ts`, and the rest
|
|
120
|
+
is wiring the framework does: the boot resolves the VAPID pair from the environment and installs the
|
|
121
|
+
runtime, the worker carries the handlers, every page carries `<meta name="x-push-key">`. RFC 8291
|
|
122
|
+
encryption and RFC 8292 (ES256) signing, on WebCrypto — no dependency.
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
// apps/web/app/push/actions.ts — two real actions: route, OpenAPI, typed client, contract tests.
|
|
126
|
+
import { pushSubscribe, pushUnsubscribe } from '@ultimat3/pwa';
|
|
127
|
+
|
|
128
|
+
export const subscribePush = pushSubscribe({ permission: 'push:subscribe' });
|
|
129
|
+
export const unsubscribePush = pushUnsubscribe({ permission: 'push:subscribe' });
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
// an island, on a click — the browser entry, `@ultimat3/pwa/client`.
|
|
134
|
+
import { subscribeToPush } from '@ultimat3/pwa/client';
|
|
135
|
+
|
|
136
|
+
declare const client: { subscribePush(input: unknown): Promise<unknown> };
|
|
137
|
+
|
|
138
|
+
const outcome = await subscribeToPush({ save: client.subscribePush });
|
|
139
|
+
// { status: 'subscribed', endpoint } | { status: 'denied' } | { status: 'unsupported' } | { status: 'unconfigured' }
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
// a notifier — every device of the recipient, each in the locale it subscribed with.
|
|
144
|
+
import { pushChannel } from '@ultimat3/notify';
|
|
145
|
+
import { webPush } from '@ultimat3/pwa';
|
|
146
|
+
|
|
147
|
+
export const channel = pushChannel<{ postId: string }>({
|
|
148
|
+
pusher: webPush(),
|
|
149
|
+
message: ({ event }) => ({
|
|
150
|
+
titleKey: 'push.comment.title',
|
|
151
|
+
bodyKey: 'push.comment.body',
|
|
152
|
+
url: `/posts/${event.params.postId}`,
|
|
153
|
+
tag: `post:${event.params.postId}`,
|
|
154
|
+
}),
|
|
155
|
+
});
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
| Piece | Rule |
|
|
159
|
+
|---|---|
|
|
160
|
+
| Keys | `ULTIMATE_VAPID_PUBLIC_KEY` + `ULTIMATE_VAPID_PRIVATE_KEY`, env only. `x vapid create` seals both in one write; locally, neither set signs with `DEV_VAPID_KEYS`, which a deployed boot refuses (`X_PWA_VAPID_KEY_MISSING`). Two halves that do not sign for each other: `X_PWA_VAPID_KEY_INVALID`, at boot |
|
|
161
|
+
| Subscriptions | `x_push_subscriptions`, applied by the boot; keyed by endpoint, stored against `ctx.actor.id`, with the locale and zone they subscribed in. An agent never subscribes |
|
|
162
|
+
| Strings | catalog keys (`titleKey`, `bodyKey`, `actions[].titleKey`), rendered per subscription locale through `@ultimat3/i18n`'s `translatorFor` |
|
|
163
|
+
| Size | one 4096-byte record: 3993 bytes of notification (`X_PWA_PUSH_PAYLOAD_TOO_LARGE`). Send a path, never the content |
|
|
164
|
+
| Travel | `ttlSeconds` (default 86 400), `urgency`, `topic` — RFC 8030 headers; the topic is any string, sent as 32 URL-safe characters of its SHA-256 |
|
|
165
|
+
| 201 / 404, 410 / 429, 5xx / 400, 401, 403, 413 | delivered / subscription deleted / `X_PWA_PUSH_FAILED`, retried by the job after every other device was tried / `X_PWA_PUSH_REJECTED`, logged, never retried — and on 401/403 the subscription is deleted: it was made with a key this server no longer signs with |
|
|
166
|
+
| Key rotation | `subscribeToPush` compares a browser's existing subscription with the page's `x-push-key`; one made with another key is unsubscribed and replaced, never re-saved |
|
|
167
|
+
| Endpoints | dialled only on a push service: `https:`, and a host that is one of `PUSH_SERVICE_HOSTS` (FCM, Mozilla, Apple, WNS) or `pwa.vapid.pushHosts`, or a subdomain of one on a dot boundary — checked when stored and before every send (`assertPushEndpoint`), never by where the name resolves, since the endpoint came from a request body (`X_PWA_PUSH_HOST_UNLISTED`; a stored one is deleted unsent). One POST waits at most `PUSH_SEND_TIMEOUT_MS` (30 s); a caller's abort is rethrown as is, never the retryable class |
|
|
168
|
+
| `renotify` | dropped, with a warning, without a `tag` — at both ends |
|
|
169
|
+
|
|
91
170
|
## Error classes
|
|
92
171
|
|
|
93
172
|
Every error class `src/index.ts` exports, for `instanceof` inside one process. Across a wire or
|
|
@@ -99,7 +178,14 @@ a job boundary the class is gone and the `code` is what survives — match on th
|
|
|
99
178
|
| `PwaIconMissingError` | `X_PWA_ICON_MISSING` | `src/errors.ts` |
|
|
100
179
|
| `PwaManifestInvalidError` | `X_PWA_MANIFEST_INVALID` | `src/errors.ts` |
|
|
101
180
|
| `PwaNoOfflineFallbackError` | `X_PWA_NO_OFFLINE_FALLBACK` | `src/errors.ts` |
|
|
181
|
+
| `PwaPushFailedError` | `X_PWA_PUSH_FAILED` | `src/errors.ts` |
|
|
182
|
+
| `PwaPushPayloadTooLargeError` | `X_PWA_PUSH_PAYLOAD_TOO_LARGE` | `src/errors.ts` |
|
|
183
|
+
| `PwaPushRejectedError` | `X_PWA_PUSH_REJECTED` | `src/errors.ts` |
|
|
184
|
+
| `PwaPushSubscriptionInvalidError` | `X_PWA_PUSH_SUBSCRIPTION_INVALID` | `src/errors.ts` |
|
|
185
|
+
| `PwaPushUnconfiguredError` | `X_PWA_PUSH_UNCONFIGURED` | `src/errors.ts` |
|
|
102
186
|
| `PwaStrategyExhaustedError` | `X_PWA_STRATEGY_EXHAUSTED` | `src/errors.ts` |
|
|
187
|
+
| `PwaVapidKeyInvalidError` | `X_PWA_VAPID_KEY_INVALID` | `src/errors.ts` |
|
|
188
|
+
| `PwaVapidKeyMissingError` | `X_PWA_VAPID_KEY_MISSING` | `src/errors.ts` |
|
|
103
189
|
| `SwScopeInvalidError` | `X_SW_SCOPE_INVALID` | `src/errors.ts` |
|
|
104
190
|
|
|
105
191
|
## Public API
|
|
@@ -111,14 +197,21 @@ a job boundary the class is gone and the `code` is what survives — match on th
|
|
|
111
197
|
| `routeRules`, `assetRules` | the worker's rule list: routes most specific first, runtime asset prefixes (`/islands/`) ahead of them. `routeRules(routes, personalPages)` — `'last-member'` gives a personal page a `pages` rule; a pattern matches the browser's percent-encoded pathname, and a catch-all its bare prefix |
|
|
112
198
|
| `CLEAR_PAGES_MESSAGE`, `PAGES_CLEARED_MESSAGE` | `{ type: 'clear-pages' }` — post it to the worker on sign-out; it empties every pages cache and answers `{ type: 'pages-cleared' }` |
|
|
113
199
|
| `buildPrecacheManifest` | precache entries (url + content-hash revision), size warnings |
|
|
114
|
-
| `buildId`, `detectSkew`, `retentionPlan` | version skew |
|
|
200
|
+
| `buildId`, `detectSkew`, `retentionPlan` | version skew (`detectSkew` also on `./client`) |
|
|
115
201
|
| `generateWebManifest` | the manifest + `theme-color` metas for both schemes, from a `WebManifestInput`. Called by `@ultimat3/cli` (`pwa-artifacts.ts`) `As of 2026-08-27`, so `x dev`, the container and the static export all emit `manifest.webmanifest` |
|
|
116
202
|
| `planIcons`, `requireSourceIcon`, `maskableSafeZone` | icons and splashes from one source |
|
|
117
203
|
| `BuiltinImagePipeline` | renders that plan: one square PNG per entry, deterministic |
|
|
118
204
|
| `requireOfflineFallback` | the mandatory offline route |
|
|
119
205
|
| `backgroundSyncSource`, `registerBackgroundSyncSource` | the Background Sync trigger. No retry policy: the handler rejects and the PLATFORM reschedules it |
|
|
120
206
|
| `renderPushPayload`, `pushSource`, `subscribeSource` | Web Push, per-locale bodies |
|
|
121
|
-
| `
|
|
207
|
+
| `pushSubscribe`, `pushUnsubscribe` | factories over `action`: store / forget this browser's subscription for `ctx.actor` |
|
|
208
|
+
| `webPush`, `pushToActor` | one notification to every device of one person — `webPush()` is notify's `Pusher` |
|
|
209
|
+
| `sendPushMessage`, `encryptPushMessage`, `vapidAuthorization` | one message to one subscription; RFC 8291; RFC 8292 |
|
|
210
|
+
| `installWebPush`, `installedVapid`, `webPushRuntime`, `resetWebPush` | the runtime the boot installs (store, signer, transport, translator) |
|
|
211
|
+
| `memoryPushSubscriptionStore`, `postgresPushSubscriptionStore` | where subscriptions live; `x_push_subscriptions` DDL is `@ultimat3/pwa/schema` |
|
|
212
|
+
| `resolveVapidKeys`, `generateVapidKeys`, `assertVapidPair`, `DEV_VAPID_KEYS` | the key pair: from env, minted, checked, and the published development one |
|
|
213
|
+
| `@ultimat3/pwa/client` | THE browser entry — [Browser entry](#browser-entry-ultimat3pwaclient) |
|
|
214
|
+
| `installController`, `iosInstallGuidance`, `MIN_ENGAGEMENT_MS` | install prompt, never on first paint (also on `./client`) |
|
|
122
215
|
| `PwaStrategyExhaustedError` and the other `errors.ts` classes | the codes this package throws, catchable by an app |
|
|
123
216
|
|
|
124
217
|
## Notes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/pwa",
|
|
3
|
-
"version": "26.
|
|
3
|
+
"version": "26.1.0",
|
|
4
4
|
"description": "Generated service worker, web manifest, icons, push and version-skew handling.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
"provenance": true
|
|
15
15
|
},
|
|
16
16
|
"exports": {
|
|
17
|
-
".": "./src/index.ts"
|
|
17
|
+
".": "./src/index.ts",
|
|
18
|
+
"./client": "./src/client.ts",
|
|
19
|
+
"./schema": "./src/push-schema.ts"
|
|
18
20
|
},
|
|
19
21
|
"files": [
|
|
20
22
|
"src",
|
|
@@ -32,6 +34,10 @@
|
|
|
32
34
|
"test": "bun test"
|
|
33
35
|
},
|
|
34
36
|
"dependencies": {
|
|
35
|
-
"@ultimat3/
|
|
37
|
+
"@ultimat3/action": "26.1.0",
|
|
38
|
+
"@ultimat3/core": "26.1.0",
|
|
39
|
+
"@ultimat3/i18n": "26.1.0",
|
|
40
|
+
"@ultimat3/policy": "26.1.0",
|
|
41
|
+
"@ultimat3/schema": "26.1.0"
|
|
36
42
|
}
|
|
37
43
|
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// `@ultimat3/pwa/client` — THE browser entry of this package: everything an island or a page script
|
|
2
|
+
// may call, and nothing that drags the server half along. The `.` barrel reaches the icon pipeline
|
|
3
|
+
// (core's image transform, Node-only), the service-worker generator and the push sender; this
|
|
4
|
+
// entry's graph is these four files and `@ultimat3/core/page`, held there by `client-bundle.test.ts`.
|
|
5
|
+
//
|
|
6
|
+
// import { detectSkew, installController, subscribeToPush } from '@ultimat3/pwa/client';
|
|
7
|
+
//
|
|
8
|
+
// The worker's message TYPE (`'AppUpdateAvailable'`) is core's `APP_UPDATE_MESSAGE`, imported from
|
|
9
|
+
// `@ultimat3/core/page` — one name, one home (`X_HELPER_COPY`); `AppUpdateAvailable` types it here.
|
|
10
|
+
|
|
11
|
+
export type {
|
|
12
|
+
BeforeInstallPromptEventLike,
|
|
13
|
+
InstallController,
|
|
14
|
+
InstallHost,
|
|
15
|
+
InstallOptions,
|
|
16
|
+
InstallOutcome,
|
|
17
|
+
IosGuidance,
|
|
18
|
+
ReadSignal,
|
|
19
|
+
} from './install';
|
|
20
|
+
export { installController, iosInstallGuidance, MIN_ENGAGEMENT_MS } from './install';
|
|
21
|
+
export type {
|
|
22
|
+
PushClientHost,
|
|
23
|
+
PushPermission,
|
|
24
|
+
PushRegistrationLike,
|
|
25
|
+
PushSubscribeOutcome,
|
|
26
|
+
PushSubscriptionInput,
|
|
27
|
+
PushSubscriptionLike,
|
|
28
|
+
} from './push-client';
|
|
29
|
+
export {
|
|
30
|
+
browserPushHost,
|
|
31
|
+
PUSH_KEY_META,
|
|
32
|
+
pushPermission,
|
|
33
|
+
subscribeToPush,
|
|
34
|
+
unsubscribeFromPush,
|
|
35
|
+
} from './push-client';
|
|
36
|
+
export type { AppUpdateAvailable, SkewState } from './skew';
|
|
37
|
+
export { detectSkew } from './skew';
|
package/src/errors.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* fallback, a missing build id, a bad scope — fails here at build time instead.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { registerErrorCodes, UltimateError } from '@ultimat3/core';
|
|
6
|
+
import { registerErrorCodes, registerErrorRetry, UltimateError } from '@ultimat3/core';
|
|
7
7
|
|
|
8
8
|
/** Codes this package declares and owns. */
|
|
9
9
|
export const PWA_OWNED_ERROR_CODES = [
|
|
@@ -17,6 +17,14 @@ export const PWA_OWNED_ERROR_CODES = [
|
|
|
17
17
|
// POSTing to `/_x/outbox/flush` (`background-sync.ts`). Registered so an old log still explains.
|
|
18
18
|
'X_PWA_SYNC_FLUSH_FAILED',
|
|
19
19
|
'X_PWA_SYNC_INCOMPLETE',
|
|
20
|
+
'X_PWA_VAPID_KEY_MISSING',
|
|
21
|
+
'X_PWA_VAPID_KEY_INVALID',
|
|
22
|
+
'X_PWA_PUSH_UNCONFIGURED',
|
|
23
|
+
'X_PWA_PUSH_FAILED',
|
|
24
|
+
'X_PWA_PUSH_REJECTED',
|
|
25
|
+
'X_PWA_PUSH_PAYLOAD_TOO_LARGE',
|
|
26
|
+
'X_PWA_PUSH_SUBSCRIPTION_INVALID',
|
|
27
|
+
'X_PWA_PUSH_HOST_UNLISTED',
|
|
20
28
|
] as const;
|
|
21
29
|
|
|
22
30
|
/**
|
|
@@ -41,6 +49,14 @@ export const PWA_ERROR_TITLES: Readonly<Record<PwaOwnedErrorCode, string>> = {
|
|
|
41
49
|
X_PWA_STRATEGY_EXHAUSTED: 'a caching strategy had no cache, no network and no fallback',
|
|
42
50
|
X_PWA_SYNC_FLUSH_FAILED: 'the background-sync outbox flush was rejected',
|
|
43
51
|
X_PWA_SYNC_INCOMPLETE: 'the background-sync outbox flush left mutations queued',
|
|
52
|
+
X_PWA_VAPID_KEY_MISSING: 'no VAPID key pair for push',
|
|
53
|
+
X_PWA_VAPID_KEY_INVALID: 'the VAPID key pair is malformed or not a pair',
|
|
54
|
+
X_PWA_PUSH_UNCONFIGURED: 'push is not configured in this process',
|
|
55
|
+
X_PWA_PUSH_FAILED: 'the push service did not accept the message, and may later',
|
|
56
|
+
X_PWA_PUSH_REJECTED: 'the push service refused the message and will refuse it again',
|
|
57
|
+
X_PWA_PUSH_PAYLOAD_TOO_LARGE: 'the push message does not fit one 4096-byte record',
|
|
58
|
+
X_PWA_PUSH_SUBSCRIPTION_INVALID: 'the push subscription is not one a browser produced',
|
|
59
|
+
X_PWA_PUSH_HOST_UNLISTED: 'the push endpoint is not on a push service this app sends to',
|
|
44
60
|
};
|
|
45
61
|
|
|
46
62
|
// One unconditional call, so a second package claiming one of pwa's codes throws
|
|
@@ -131,3 +147,142 @@ export class PwaStrategyExhaustedError extends UltimateError {
|
|
|
131
147
|
});
|
|
132
148
|
}
|
|
133
149
|
}
|
|
150
|
+
|
|
151
|
+
// Web Push. `X_PWA_PUSH_FAILED` is the one a retry can cure — the same message landing later —
|
|
152
|
+
// so it is `retry-after` (a 429's `Retry-After` is honoured through `meta.retryAfterSeconds`); the
|
|
153
|
+
// rest describe the key, the subscription or the body, and every attempt meets them unchanged.
|
|
154
|
+
registerErrorRetry({
|
|
155
|
+
X_PWA_PUSH_FAILED: 'retry-after',
|
|
156
|
+
X_PWA_PUSH_REJECTED: 'terminal',
|
|
157
|
+
X_PWA_PUSH_PAYLOAD_TOO_LARGE: 'terminal',
|
|
158
|
+
X_PWA_PUSH_SUBSCRIPTION_INVALID: 'terminal',
|
|
159
|
+
X_PWA_PUSH_HOST_UNLISTED: 'terminal',
|
|
160
|
+
X_PWA_VAPID_KEY_MISSING: 'terminal',
|
|
161
|
+
X_PWA_VAPID_KEY_INVALID: 'terminal',
|
|
162
|
+
X_PWA_PUSH_UNCONFIGURED: 'terminal',
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
/** The boot refuses push with no key pair in a deployed environment. */
|
|
166
|
+
export class PwaVapidKeyMissingError extends UltimateError {
|
|
167
|
+
static readonly code = 'X_PWA_VAPID_KEY_MISSING' as const;
|
|
168
|
+
constructor(input: { missing: readonly string[]; deployed: boolean }) {
|
|
169
|
+
const unset = `${input.missing.join(' and ')} ${input.missing.length === 1 ? 'is' : 'are'} unset`;
|
|
170
|
+
super({
|
|
171
|
+
code: PwaVapidKeyMissingError.code,
|
|
172
|
+
cause: input.deployed
|
|
173
|
+
? `pwa.push is true and ${unset} (or holds the published development key), and a deployed environment never signs with the development pair`
|
|
174
|
+
: `pwa.push is true and ${unset} while its other half is set — half a pair is never completed with the development one`,
|
|
175
|
+
fix: "x vapid create # seals a fresh pair into secrets.enc.json; or set ULTIMATE_VAPID_PUBLIC_KEY and ULTIMATE_VAPID_PRIVATE_KEY in the deployment's environment",
|
|
176
|
+
meta: { missing: [...input.missing] },
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** A key that is not a P-256 key, or a private key that does not sign for its public half. */
|
|
182
|
+
export class PwaVapidKeyInvalidError extends UltimateError {
|
|
183
|
+
static readonly code = 'X_PWA_VAPID_KEY_INVALID' as const;
|
|
184
|
+
constructor(input: { key: string; reason: string }) {
|
|
185
|
+
super({
|
|
186
|
+
code: PwaVapidKeyInvalidError.code,
|
|
187
|
+
cause: `${input.key} ${input.reason}`,
|
|
188
|
+
fix: 'x vapid create # one command writes both halves, so they cannot disagree',
|
|
189
|
+
meta: { key: input.key },
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Push used in a process whose boot installed no Web Push runtime. */
|
|
195
|
+
export class PwaPushUnconfiguredError extends UltimateError {
|
|
196
|
+
static readonly code = 'X_PWA_PUSH_UNCONFIGURED' as const;
|
|
197
|
+
constructor(input: { operation: string }) {
|
|
198
|
+
super({
|
|
199
|
+
code: PwaPushUnconfiguredError.code,
|
|
200
|
+
cause: `${input.operation} ran in a process whose boot installed no Web Push runtime: pwa.push is false, or pwa.enabled is`,
|
|
201
|
+
fix: "pwa: { enabled: true, push: true, vapid: { subject: 'mailto:ops@example.com' } } # in app.config.ts; a test calls installWebPush({ store, keys, subject }) first",
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** The push service may accept the same message later: a 429, a 5xx, or no connection at all. */
|
|
207
|
+
export class PwaPushFailedError extends UltimateError {
|
|
208
|
+
static readonly code = 'X_PWA_PUSH_FAILED' as const;
|
|
209
|
+
constructor(input: {
|
|
210
|
+
origin: string;
|
|
211
|
+
status: number | null;
|
|
212
|
+
detail: string;
|
|
213
|
+
retryAfterSeconds?: number | undefined;
|
|
214
|
+
}) {
|
|
215
|
+
super({
|
|
216
|
+
code: PwaPushFailedError.code,
|
|
217
|
+
cause: `the push service at ${input.origin} answered ${input.status === null ? input.detail : `status ${input.status}`}`,
|
|
218
|
+
fix: 'x jobs list --json # the job retries on its policy; a stated Retry-After rides in meta.retryAfterSeconds',
|
|
219
|
+
meta: {
|
|
220
|
+
origin: input.origin,
|
|
221
|
+
status: input.status,
|
|
222
|
+
...(input.retryAfterSeconds === undefined
|
|
223
|
+
? {}
|
|
224
|
+
: { retryAfterSeconds: input.retryAfterSeconds }),
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** The push service refused the message, and will refuse it again. */
|
|
231
|
+
export class PwaPushRejectedError extends UltimateError {
|
|
232
|
+
static readonly code = 'X_PWA_PUSH_REJECTED' as const;
|
|
233
|
+
constructor(input: { origin: string; status: number; body: string }) {
|
|
234
|
+
super({
|
|
235
|
+
code: PwaPushRejectedError.code,
|
|
236
|
+
cause: `the push service at ${input.origin} refused the message with status ${input.status}${input.body === '' ? '' : `: ${input.body}`}`,
|
|
237
|
+
fix:
|
|
238
|
+
input.status === 403 || input.status === 401
|
|
239
|
+
? 'x vapid show # the subscription was made with another public key: a key change means every subscriber subscribes again'
|
|
240
|
+
: "vapid: { subject: 'mailto:ops@example.com' } # pwa.vapid in app.config.ts — the push service's answer in the cause names the field it refused",
|
|
241
|
+
meta: { origin: input.origin, status: input.status },
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** One record is 4096 bytes; RFC 8291 leaves 3993 of them for the notification. */
|
|
247
|
+
export class PwaPushPayloadTooLargeError extends UltimateError {
|
|
248
|
+
static readonly code = 'X_PWA_PUSH_PAYLOAD_TOO_LARGE' as const;
|
|
249
|
+
constructor(input: { bytes: number; limit: number }) {
|
|
250
|
+
super({
|
|
251
|
+
code: PwaPushPayloadTooLargeError.code,
|
|
252
|
+
cause: `the rendered notification is ${input.bytes} bytes and one Web Push record carries ${input.limit}`,
|
|
253
|
+
fix: "url: '/posts/1' # send a path to the content and shorten the catalog strings — never the content itself",
|
|
254
|
+
meta: { bytes: input.bytes, limit: input.limit },
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** A subscription no browser produced: a non-https endpoint, or keys of the wrong size. */
|
|
260
|
+
export class PwaPushSubscriptionInvalidError extends UltimateError {
|
|
261
|
+
static readonly code = 'X_PWA_PUSH_SUBSCRIPTION_INVALID' as const;
|
|
262
|
+
constructor(input: { field: string; reason: string }) {
|
|
263
|
+
super({
|
|
264
|
+
code: PwaPushSubscriptionInvalidError.code,
|
|
265
|
+
cause: `${input.field} ${input.reason}`,
|
|
266
|
+
fix: "import { subscribeToPush } from '@ultimat3/pwa/client'; // it sends PushSubscription.toJSON() unchanged",
|
|
267
|
+
meta: { field: input.field },
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* An endpoint whose host is not a push service this app sends to. The endpoint arrives in a request
|
|
274
|
+
* body, and a hostname can resolve anywhere — a private address, the metadata service — so the one
|
|
275
|
+
* screen that holds is a list of the services themselves (`push-hosts.ts`), not a check of where a
|
|
276
|
+
* name points today.
|
|
277
|
+
*/
|
|
278
|
+
export class PwaPushHostUnlistedError extends UltimateError {
|
|
279
|
+
static readonly code = 'X_PWA_PUSH_HOST_UNLISTED' as const;
|
|
280
|
+
constructor(input: { host: string }) {
|
|
281
|
+
super({
|
|
282
|
+
code: PwaPushHostUnlistedError.code,
|
|
283
|
+
cause: `the endpoint's host ${input.host} is neither a built-in push service (PUSH_SERVICE_HOSTS: FCM, Mozilla, Apple, WNS, or a subdomain of one) nor listed in pwa.vapid.pushHosts`,
|
|
284
|
+
fix: "vapid: { subject: 'mailto:ops@example.com', pushHosts: ['push.example.com'] } # pwa.vapid.pushHosts in app.config.ts, for a push service the built-in list does not name",
|
|
285
|
+
meta: { host: input.host },
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -30,7 +30,15 @@ export {
|
|
|
30
30
|
// `staleWhileRevalidate` is public and throws this, so an app that catches it can name it. The
|
|
31
31
|
// two X_PWA_SYNC_* classes are not here on purpose: the emitted `sw.js` builds its own local
|
|
32
32
|
// class in a realm with no bundler, so no instance of theirs can ever reach an app.
|
|
33
|
+
PwaPushFailedError,
|
|
34
|
+
PwaPushHostUnlistedError,
|
|
35
|
+
PwaPushPayloadTooLargeError,
|
|
36
|
+
PwaPushRejectedError,
|
|
37
|
+
PwaPushSubscriptionInvalidError,
|
|
38
|
+
PwaPushUnconfiguredError,
|
|
33
39
|
PwaStrategyExhaustedError,
|
|
40
|
+
PwaVapidKeyInvalidError,
|
|
41
|
+
PwaVapidKeyMissingError,
|
|
34
42
|
SwScopeInvalidError,
|
|
35
43
|
} from './errors';
|
|
36
44
|
export type {
|
|
@@ -62,7 +70,7 @@ export type {
|
|
|
62
70
|
IosGuidance,
|
|
63
71
|
ReadSignal,
|
|
64
72
|
} from './install';
|
|
65
|
-
export { installController, iosInstallGuidance } from './install';
|
|
73
|
+
export { installController, iosInstallGuidance, MIN_ENGAGEMENT_MS } from './install';
|
|
66
74
|
export type {
|
|
67
75
|
DisplayMode,
|
|
68
76
|
FileHandler,
|
|
@@ -103,10 +111,50 @@ export {
|
|
|
103
111
|
subscribeSource,
|
|
104
112
|
subscriptionState,
|
|
105
113
|
} from './push';
|
|
114
|
+
export type { PushSubscriptionActionOptions } from './push-actions';
|
|
115
|
+
export { pushSubscribe, pushUnsubscribe } from './push-actions';
|
|
116
|
+
// The browser entry is `@ultimat3/pwa/client` (`client.ts`), never this barrel: an island
|
|
117
|
+
// importing it would pull the service-worker generator into a page bundle. Its meta name is
|
|
118
|
+
// re-exported here because the SERVER writes the tag the client reads.
|
|
119
|
+
export { PUSH_KEY_META } from './push-client';
|
|
120
|
+
export type { PushReceiverKeys } from './push-decrypt';
|
|
121
|
+
export { decryptPushMessage } from './push-decrypt';
|
|
122
|
+
export type { PushEncryptionKeys, PushEncryptOptions } from './push-encrypt';
|
|
123
|
+
export { encryptPushMessage, PUSH_RECORD_SIZE, pushMessageCapacity } from './push-encrypt';
|
|
124
|
+
export { PUSH_SERVICE_HOSTS, pushHostAllowed } from './push-hosts';
|
|
125
|
+
export type { WebPushOptions, WebPushRuntime } from './push-runtime';
|
|
126
|
+
export { installedVapid, installWebPush, resetWebPush, webPushRuntime } from './push-runtime';
|
|
127
|
+
export type {
|
|
128
|
+
PushDelivery,
|
|
129
|
+
PushSendInput,
|
|
130
|
+
PushSendOutcome,
|
|
131
|
+
PushTarget,
|
|
132
|
+
PushUrgency,
|
|
133
|
+
VapidSigner,
|
|
134
|
+
} from './push-send';
|
|
135
|
+
export {
|
|
136
|
+
assertPushEndpoint,
|
|
137
|
+
DEFAULT_PUSH_TTL_SECONDS,
|
|
138
|
+
PUSH_SEND_TIMEOUT_MS,
|
|
139
|
+
PUSH_URGENCIES,
|
|
140
|
+
pushEndpointProblem,
|
|
141
|
+
sendPushMessage,
|
|
142
|
+
} from './push-send';
|
|
143
|
+
export type { PushSubscriptionStore } from './push-store';
|
|
144
|
+
export { memoryPushSubscriptionStore } from './push-store';
|
|
145
|
+
export type { PostgresPushSubscriptionStoreOptions } from './push-store-pg';
|
|
146
|
+
export { postgresPushSubscriptionStore } from './push-store-pg';
|
|
106
147
|
export type { RouteRule } from './route-rules';
|
|
107
148
|
export { assetRules, routeRules } from './route-rules';
|
|
108
149
|
export type { ServiceWorkerConfig, ServiceWorkerOutput } from './service-worker';
|
|
109
150
|
export { assertScope, generateServiceWorker, PRECACHE_CONCURRENCY } from './service-worker';
|
|
151
|
+
// The forced-reload half of this module is gone as of 9.0.0 — `updateSignal`, `updatePolicy`,
|
|
152
|
+
// `DEFAULT_GRACE_MS`, `ForceReason`, `UpdatePolicy`, `UpdatePolicyInput`, `UpdateSignalInput`.
|
|
153
|
+
// It computed `forced`/`deadlineAt` for a client-side reload no code in the framework performed,
|
|
154
|
+
// from a caller that never existed. What remains is what runs: an id, a comparison, a retention
|
|
155
|
+
// plan, and the message the generated worker really posts.
|
|
156
|
+
export type { AppUpdateAvailable, SkewState } from './skew';
|
|
157
|
+
export { detectSkew } from './skew';
|
|
110
158
|
export type {
|
|
111
159
|
PwaRoute,
|
|
112
160
|
StrategyCache,
|
|
@@ -124,23 +172,19 @@ export {
|
|
|
124
172
|
staleWhileRevalidate,
|
|
125
173
|
strategyFor,
|
|
126
174
|
} from './strategies';
|
|
127
|
-
|
|
128
|
-
// `DEFAULT_GRACE_MS`, `ForceReason`, `UpdatePolicy`, `UpdatePolicyInput`, `UpdateSignalInput`.
|
|
129
|
-
// It computed `forced`/`deadlineAt` for a client-side reload no code in the framework performed,
|
|
130
|
-
// from a caller that never existed. What remains is what runs: an id, a comparison, a retention
|
|
131
|
-
// plan, and the message the generated worker really posts.
|
|
132
|
-
export type {
|
|
133
|
-
AppUpdateAvailable,
|
|
134
|
-
BuildIdInput,
|
|
135
|
-
Deploy,
|
|
136
|
-
DeployChannel,
|
|
137
|
-
RetentionPlan,
|
|
138
|
-
SkewState,
|
|
139
|
-
} from './version-skew';
|
|
175
|
+
export type { VapidKeyPair, VapidTokenInput } from './vapid';
|
|
140
176
|
export {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
177
|
+
assertVapidPair,
|
|
178
|
+
generateVapidKeys,
|
|
179
|
+
importVapidKeys,
|
|
180
|
+
VAPID_PRIVATE_KEY_ENV,
|
|
181
|
+
VAPID_PUBLIC_KEY_ENV,
|
|
182
|
+
VAPID_TOKEN_TTL_SECONDS,
|
|
183
|
+
vapidAuthorization,
|
|
184
|
+
} from './vapid';
|
|
185
|
+
export type { ResolvedVapidKeys } from './vapid-keys';
|
|
186
|
+
export { DEV_VAPID_KEYS, resolveVapidKeys, usesDevVapidKeys } from './vapid-keys';
|
|
187
|
+
export type { BuildIdInput, Deploy, DeployChannel, RetentionPlan } from './version-skew';
|
|
188
|
+
export { assertBuildId, buildId, cacheNamespace, retentionPlan } from './version-skew';
|
|
189
|
+
export type { PushNotification, PushReport, WebPusher } from './web-push';
|
|
190
|
+
export { pushToActor, webPush } from './web-push';
|
package/src/install.ts
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* permanently on some platforms.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
// `/page`: the browser-safe subpath — this file is in `@ultimat3/pwa/client`'s graph.
|
|
9
|
+
import { finiteCount } from '@ultimat3/core/page';
|
|
9
10
|
|
|
10
11
|
/** A minimal signal so this file needs no framework runtime. */
|
|
11
12
|
export interface ReadSignal<T> {
|