@hermesihq/react 0.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/CHANGELOG.md +87 -0
- package/LICENSE +21 -0
- package/README.md +128 -0
- package/dist/index.cjs +924 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.css +280 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.cts +444 -0
- package/dist/index.d.ts +444 -0
- package/dist/index.js +878 -0
- package/dist/index.js.map +1 -0
- package/package.json +71 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@hermesihq/react`.
|
|
4
|
+
|
|
5
|
+
This file describes what a consumer gets, not how it was built. The repository's
|
|
6
|
+
history is where the reasoning lives.
|
|
7
|
+
|
|
8
|
+
## Versioning
|
|
9
|
+
|
|
10
|
+
**`0.x` means the public API can still change.** A minor bump (`0.1` → `0.2`) may
|
|
11
|
+
contain a breaking change; a patch bump will not. That is the ordinary `0.x` reading
|
|
12
|
+
of semver and it is chosen deliberately rather than by default: this SDK's shape is
|
|
13
|
+
still being learned from the first integrations, and promising stability before anyone
|
|
14
|
+
has built against it would be a promise made to nobody and broken later.
|
|
15
|
+
|
|
16
|
+
`1.0.0` is the commitment that a breaking change requires a major bump. It waits until
|
|
17
|
+
at least one real integration exists.
|
|
18
|
+
|
|
19
|
+
Each release lists breaking changes first, because that is the only section that
|
|
20
|
+
decides whether an upgrade is a decision or a formality.
|
|
21
|
+
|
|
22
|
+
## 0.1.0 (2026-09-28)
|
|
23
|
+
|
|
24
|
+
The first published release. Nothing precedes it, so there is nothing to migrate from
|
|
25
|
+
and no deprecations; the entries below describe the surface rather than changes to one.
|
|
26
|
+
|
|
27
|
+
### The inbox
|
|
28
|
+
|
|
29
|
+
- `HermsClient`: the framework-agnostic core. Reads and writes the subscriber's inbox,
|
|
30
|
+
opens a realtime stream for new items and unread counts, and falls back to polling
|
|
31
|
+
when the runtime has no `EventSource` or the stream keeps failing. It imports nothing
|
|
32
|
+
from React, so a Vue or vanilla wrapper is a thin layer rather than a second
|
|
33
|
+
implementation.
|
|
34
|
+
- `HermsProvider` / `useHermsContext`: one shared connection per provider, fanned out
|
|
35
|
+
to every hook beneath it. Mounting three components does not open three streams.
|
|
36
|
+
- `useInbox`: the item list, with filtering, cursor pagination, and mark-read,
|
|
37
|
+
mark-all-read, archive and delete.
|
|
38
|
+
- `useUnreadCount`: the badge, kept live off the same stream.
|
|
39
|
+
- `HermsInbox`: a rendered bell and panel, keyboard-navigable, in English or French.
|
|
40
|
+
Optional: the hooks above are the whole API if you would rather build your own.
|
|
41
|
+
|
|
42
|
+
### Preferences
|
|
43
|
+
|
|
44
|
+
- `client.getPreferences()` and `client.updatePreference({ channel, enabled, categoryId })`,
|
|
45
|
+
plus the `usePreferences` hook.
|
|
46
|
+
- `PATCH` answers with the whole updated state, so a caller never refetches. The hook
|
|
47
|
+
does **not** update optimistically: turning off a channel is a consent decision, and
|
|
48
|
+
showing it as done before the server agreed tells somebody they have opted out when
|
|
49
|
+
they may not have.
|
|
50
|
+
- A setting of `null` means no preference was expressed and the default applies. It is
|
|
51
|
+
not `false`. Categories with `isCritical: true` are always delivered. Render them so
|
|
52
|
+
a subscriber can see what they receive, but not as a control.
|
|
53
|
+
- There is deliberately no `unsubscribe()`. `POST /v1/client/unsubscribe` is the target
|
|
54
|
+
of a `List-Unsubscribe` one-click link (RFC 8058) whose token is minted into an
|
|
55
|
+
outgoing email's headers and invoked by a mail client, not by application code. The
|
|
56
|
+
equivalent here is `updatePreference({ channel: 'email', enabled: false })`.
|
|
57
|
+
|
|
58
|
+
### Identities and errors
|
|
59
|
+
|
|
60
|
+
- `client.registerChannel` / `client.deregisterChannel`, and `HERMS_CHANNELS`: the
|
|
61
|
+
channels an identity can be registered for, exported as an array so a preference
|
|
62
|
+
centre can iterate them, with `HermsChannel` derived from it.
|
|
63
|
+
- `HermsApiError`: every failed request rejects with it, carrying the API's `type`,
|
|
64
|
+
`code`, `message`, `request_id`, `detail` and `docUrl`. A response that is not JSON
|
|
65
|
+
(a proxy's error page, an empty body where one was expected) arrives as this too,
|
|
66
|
+
rather than as a `TypeError` from inside the SDK.
|
|
67
|
+
- `decodeSubscriberTokenExp`: reads a subscriber token's expiry so a host can refresh
|
|
68
|
+
ahead of it. It parses; it does not validate a signature and cannot, since this
|
|
69
|
+
package never holds a key.
|
|
70
|
+
|
|
71
|
+
### Requirements
|
|
72
|
+
|
|
73
|
+
React 18 or newer, as a peer dependency with no upper bound. That is deliberate and
|
|
74
|
+
the opposite of how this repository pins its own dependencies: an application locks its
|
|
75
|
+
versions so two builds of one commit are identical, whereas a library that bounds a
|
|
76
|
+
peer range forces a warning on every consumer the day React ships a major, whether or
|
|
77
|
+
not anything actually broke. You control React; this package should not have an opinion
|
|
78
|
+
about which one you run.
|
|
79
|
+
|
|
80
|
+
Ships ESM and CommonJS builds with type declarations for both, and
|
|
81
|
+
`@hermesihq/react/styles.css` for `HermsInbox`.
|
|
82
|
+
|
|
83
|
+
### Known gaps
|
|
84
|
+
|
|
85
|
+
- No `<HermsPreferences />` component. The headless surface for one is here; the
|
|
86
|
+
rendered interface is not, because it wants a design pass rather than an invention.
|
|
87
|
+
- No Vue or vanilla wrapper yet.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hermesi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# @hermesihq/react
|
|
2
|
+
|
|
3
|
+
Hermesi's embeddable inbox SDK: a framework-agnostic
|
|
4
|
+
`HermsClient` core plus React bindings (`<HermsProvider>`, `useUnreadCount()`,
|
|
5
|
+
`useInbox()`, `<HermsInbox />`).
|
|
6
|
+
|
|
7
|
+
## Scope of this package, honestly
|
|
8
|
+
|
|
9
|
+
This repo has no monorepo tooling (no pnpm/Turborepo workspaces, no root
|
|
10
|
+
`package.json`). The dashboard (`frontend/`) still consumes this package as
|
|
11
|
+
TypeScript source in development, via a `resolve.alias` in
|
|
12
|
+
`frontend/vite.config.ts` (for the real bundler) and a matching `paths` entry
|
|
13
|
+
in `frontend/tsconfig.app.json` (for `tsc -b`), not through `node_modules`,
|
|
14
|
+
and not via a `file:` dependency. That wiring is deliberately left as-is: the
|
|
15
|
+
dashboard isn't this package's audience, external integrators are, and
|
|
16
|
+
changing how the dashboard resolves its own in-repo copy isn't needed to make
|
|
17
|
+
*this package* installable elsewhere.
|
|
18
|
+
|
|
19
|
+
`npm run build` now runs `tsup` (`tsup.config.ts`), which really does produce
|
|
20
|
+
`dist/index.js` (ESM), `dist/index.cjs` (CJS), `dist/index.d.ts`/`.d.cts`, and
|
|
21
|
+
`dist/index.css` (extracted from `HermsInbox.tsx`'s `import
|
|
22
|
+
'./HermsInbox.css'`). `main`/`module`/`types`/`exports` point at that `dist/`
|
|
23
|
+
output, `files` publishes only `dist/`, this `README.md`, and `LICENSE`
|
|
24
|
+
(MIT, see that file), and `prepublishOnly` reruns the typecheck and the
|
|
25
|
+
build before anything ships. `private: true` has been removed. `npm publish`
|
|
26
|
+
from this directory (after `npm login` with an account that has publish
|
|
27
|
+
rights on the `@hermesi` npm org/scope) now does the real thing.
|
|
28
|
+
|
|
29
|
+
The `repository`/`homepage`/`bugs` URLs were placeholders pointing at
|
|
30
|
+
`github.com/hermesi/hermesi`, a repository that is not this one. npm renders
|
|
31
|
+
those as the package's own links, so they would have sent every integrator
|
|
32
|
+
somewhere that does not exist. They point at the real remote now.
|
|
33
|
+
|
|
34
|
+
**Still worth deciding before v1:** semver policy, a bundle-size CI gate, and a
|
|
35
|
+
fuller SSR render test matrix (see "Not in this pass" below).
|
|
36
|
+
|
|
37
|
+
## What's real here
|
|
38
|
+
|
|
39
|
+
The packaging is real too, now: `npm run verify:package` builds, packs,
|
|
40
|
+
installs the tarball into a throwaway consumer and imports from it under ESM,
|
|
41
|
+
CJS and two TypeScript module resolutions, and CI runs it on every change.
|
|
42
|
+
Until that existed nothing had ever loaded `dist/`: the dashboard imports this
|
|
43
|
+
package's *source* through a bundler alias, so every test proved the source
|
|
44
|
+
works and nothing at all about what npm would serve.
|
|
45
|
+
|
|
46
|
+
`HermsClient` makes real HTTP requests
|
|
47
|
+
against `/v1/client/inbox/*`, opens a real `EventSource` against
|
|
48
|
+
`/v1/client/inbox/stream` (falling back to polling `/v1/client/inbox/counts`
|
|
49
|
+
every 60s when SSE isn't available or keeps failing), and
|
|
50
|
+
`<HermsInbox />` is a real, accessible bell + dropdown panel wired to that
|
|
51
|
+
client through the headless hooks.
|
|
52
|
+
|
|
53
|
+
A full working consumer lives in this package's repository, at
|
|
54
|
+
`frontend/src/pages/InboxWidgetDemoPage.tsx`. It is linked rather than
|
|
55
|
+
described because an integrator installing from npm does not have that
|
|
56
|
+
file. See the repository link in `package.json`.
|
|
57
|
+
|
|
58
|
+
## Exports a consumer needs and this README used to omit
|
|
59
|
+
|
|
60
|
+
`HermsApiError`: every failed request rejects with it, carrying the API's own
|
|
61
|
+
`type`, `code`, `message` and `request_id`. Catching it by type is the
|
|
62
|
+
difference between showing a user "rate limited, try again" and showing them
|
|
63
|
+
nothing. It was exported and undocumented, which means it was effectively
|
|
64
|
+
unavailable.
|
|
65
|
+
|
|
66
|
+
`HERMS_CHANNELS`: the channels an identity can be registered for, as an array
|
|
67
|
+
so a preference centre can iterate them, with `HermsChannel` derived from it.
|
|
68
|
+
|
|
69
|
+
`decodeSubscriberTokenExp`: reads a subscriber token's `exp` client-side so a
|
|
70
|
+
host can refresh ahead of expiry. It parses; it does **not** validate a
|
|
71
|
+
signature and cannot, since this package never holds a key.
|
|
72
|
+
|
|
73
|
+
`client.registerChannel({ channel, identifier })` and
|
|
74
|
+
`client.deregisterChannel(channel, identifier)`: where a subscriber can be
|
|
75
|
+
reached on a channel that needs an address.
|
|
76
|
+
|
|
77
|
+
`client.getPreferences()` / `client.updatePreference({ channel, enabled,
|
|
78
|
+
categoryId })` and the `usePreferences()` hook: the subscriber's own
|
|
79
|
+
notification settings, for building a preference centre. `PATCH` answers with
|
|
80
|
+
the whole updated state, so a caller never refetches, and the hook does **not**
|
|
81
|
+
update optimistically: turning off a channel is a consent decision, and showing
|
|
82
|
+
it as done before the server agreed shows somebody they have opted out when
|
|
83
|
+
they may not have.
|
|
84
|
+
|
|
85
|
+
A setting of `null` means no preference was expressed and the default applies.
|
|
86
|
+
It is not `false`, and collapsing the two would opt somebody out of something
|
|
87
|
+
they never declined. Categories with `isCritical: true` are always delivered.
|
|
88
|
+
Render them so a subscriber can see what they receive, but not as a control.
|
|
89
|
+
|
|
90
|
+
There is deliberately **no** `unsubscribe()`, although `POST
|
|
91
|
+
/v1/client/unsubscribe` exists. It is the target of a `List-Unsubscribe`
|
|
92
|
+
one-click link (RFC 8058) whose token Hermesi mints into an outgoing email's
|
|
93
|
+
headers, invoked by a mail client rather than by application code. Wrapping it
|
|
94
|
+
here would invite you to build a button on a token you cannot obtain. The
|
|
95
|
+
equivalent is `updatePreference({ channel: 'email', enabled: false })`.
|
|
96
|
+
|
|
97
|
+
## Not in this pass
|
|
98
|
+
|
|
99
|
+
For `<HermsPreferences />`, the headless surface is here
|
|
100
|
+
(`usePreferences()`), the rendered component is not: a preference centre is a
|
|
101
|
+
real interface design, and inventing one without that pass is how you get a
|
|
102
|
+
screen that technically works. A Vue/vanilla wrapper (the framework-agnostic core is
|
|
103
|
+
what makes one *possible* later, not a promise it exists), a category-filter
|
|
104
|
+
picker inside `<HermsInbox />`'s own chrome, and the real-repo-URL/semver/CI
|
|
105
|
+
follow-ups called out just above. See the task this package was built
|
|
106
|
+
against for the exact, deliberate cut lines.
|
|
107
|
+
|
|
108
|
+
## Layout
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
src/
|
|
112
|
+
core/ HermsClient: no React import anywhere in this directory.
|
|
113
|
+
HermsClient.ts
|
|
114
|
+
subscriberToken.ts Reads (never verifies/mints) a token's `exp` claim.
|
|
115
|
+
types.ts
|
|
116
|
+
react/
|
|
117
|
+
HermsProvider.tsx
|
|
118
|
+
useUnreadCount.ts
|
|
119
|
+
useInbox.ts
|
|
120
|
+
HermsInbox.tsx
|
|
121
|
+
HermsInbox.css Scoped under `.herms-inbox`, CSS custom properties only:
|
|
122
|
+
no Tailwind dependency, so a host app's Tailwind build
|
|
123
|
+
can never bleed into the widget (and vice versa).
|
|
124
|
+
locale.ts A tiny built-in EN/FR string table, not i18next, so this
|
|
125
|
+
package stays independent of whatever i18n stack (or
|
|
126
|
+
none) a host app runs.
|
|
127
|
+
index.ts
|
|
128
|
+
```
|