@saeris/hanko 0.0.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/LICENSE.md +21 -0
- package/README.md +346 -0
- package/dist/approve/index.d.mts +318 -0
- package/dist/approve/index.d.mts.map +1 -0
- package/dist/approve/index.mjs +393 -0
- package/dist/approve/index.mjs.map +1 -0
- package/dist/client/index.d.mts +101 -0
- package/dist/client/index.d.mts.map +1 -0
- package/dist/client/index.mjs +215 -0
- package/dist/client/index.mjs.map +1 -0
- package/dist/codes-Ba_qYH6u.mjs +93 -0
- package/dist/codes-Ba_qYH6u.mjs.map +1 -0
- package/dist/handlers.d.mts +113 -0
- package/dist/handlers.d.mts.map +1 -0
- package/dist/handlers.mjs +194 -0
- package/dist/handlers.mjs.map +1 -0
- package/dist/index.d.mts +5 -0
- package/dist/index.mjs +345 -0
- package/dist/index.mjs.map +1 -0
- package/dist/linking-DcQSMgem.mjs +177 -0
- package/dist/linking-DcQSMgem.mjs.map +1 -0
- package/dist/linking-nKoayyHf.d.mts +133 -0
- package/dist/linking-nKoayyHf.d.mts.map +1 -0
- package/dist/machine-CRHKjtoP.d.mts +223 -0
- package/dist/machine-CRHKjtoP.d.mts.map +1 -0
- package/dist/machine-D_5DAFxi.mjs +155 -0
- package/dist/machine-D_5DAFxi.mjs.map +1 -0
- package/dist/qr.d.mts +58 -0
- package/dist/qr.d.mts.map +1 -0
- package/dist/qr.mjs +27 -0
- package/dist/qr.mjs.map +1 -0
- package/dist/scan/index.d.mts +384 -0
- package/dist/scan/index.d.mts.map +1 -0
- package/dist/scan/index.mjs +409 -0
- package/dist/scan/index.mjs.map +1 -0
- package/dist/scan/worker.d.mts +2 -0
- package/dist/scan/worker.mjs +2 -0
- package/dist/server-BhoYRkCm.d.mts +257 -0
- package/dist/server-BhoYRkCm.d.mts.map +1 -0
- package/dist/stores/kv.d.mts +64 -0
- package/dist/stores/kv.d.mts.map +1 -0
- package/dist/stores/kv.mjs +87 -0
- package/dist/stores/kv.mjs.map +1 -0
- package/dist/stores/memory.d.mts +22 -0
- package/dist/stores/memory.d.mts.map +1 -0
- package/dist/stores/memory.mjs +42 -0
- package/dist/stores/memory.mjs.map +1 -0
- package/dist/types-BvBIFPH6.mjs +7 -0
- package/dist/types-BvBIFPH6.mjs.map +1 -0
- package/dist/types-C82lb-zX.d.mts +82 -0
- package/dist/types-C82lb-zX.d.mts.map +1 -0
- package/dist/worker-BdwaK1uX.mjs +5291 -0
- package/dist/worker-BdwaK1uX.mjs.map +1 -0
- package/dist/worker-DxbdBA2z.d.mts +164 -0
- package/dist/worker-DxbdBA2z.d.mts.map +1 -0
- package/package.json +116 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
## 0.2.1
|
|
5
|
+
<sub>2026-09-02</sub>
|
|
6
|
+
|
|
7
|
+
- Refresh the README, and give the example real navigation.
|
|
8
|
+
|
|
9
|
+
## 0.2.0
|
|
10
|
+
<sub>2026-09-02</sub>
|
|
11
|
+
|
|
12
|
+
- Add a zero-dependency QR decoder, and scan with it.
|
|
13
|
+
The library previously delegated scanning to a third-party dependency. It now
|
|
14
|
+
ships its own decoder: pixels in, string out, no camera and no DOM, so the same
|
|
15
|
+
code runs in a browser, on a worker, on a server, or in a test.
|
|
16
|
+
Recognition is measured against the 718-image BoofCV benchmark, where it reads
|
|
17
|
+
**74.4%**. Published results on the same corpus are BoofCV 60.69%, ZBar 38.95%,
|
|
18
|
+
ZXing 31.87% and jsQR 24.8%.
|
|
19
|
+
- `@saeris/hanko/scan` — `createQrDecoder` for a synchronous decode, and
|
|
20
|
+
`createProgressiveScanner` for a camera, which spreads the retry ladder
|
|
21
|
+
across frames rather than cramming it into one.
|
|
22
|
+
- `@saeris/hanko/scan/worker` — `serveDecoder`, to run a decode off the main
|
|
23
|
+
thread. A viewfinder needs this: the ladder deliberately outruns its own time
|
|
24
|
+
budget, so a synchronous decode stalls the preview on exactly the frames
|
|
25
|
+
someone is lining up.
|
|
26
|
+
Reed-Solomon correction also accepts erasures — damage whose position is known
|
|
27
|
+
but whose value is not — which is worth double the capacity when the imaging
|
|
28
|
+
layer can say where the damage is.
|
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Drake Costa
|
|
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,346 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# 💮 hanko「判子」
|
|
4
|
+
|
|
5
|
+
[![CI status][ci_badge]][ci] [![npm][npm_badge]][npm] [![License][license_badge]][license]
|
|
6
|
+
|
|
7
|
+
QR-assisted device sign-in for screens without a keyboard, with a QR decoder of its own.
|
|
8
|
+
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
**判子** (_hanko_) is the personal seal used in Japan in place of a signature —
|
|
14
|
+
pressed once to authorize something on your behalf. Which is the whole flow: a
|
|
15
|
+
TV asks, you approve it from your phone, the TV is signed in.
|
|
16
|
+
|
|
17
|
+
## 🎯 What it does
|
|
18
|
+
|
|
19
|
+
An implementation of [RFC 8628][rfc], the OAuth 2.0 Device Authorization Grant
|
|
20
|
+
— the flow behind Plex and Steam's TV sign-in, and Discord's device
|
|
21
|
+
authorization. A device that is awkward to type on shows a short code and a QR;
|
|
22
|
+
the user authorizes on a phone they are already signed in on; the device polls
|
|
23
|
+
until it hears back.
|
|
24
|
+
|
|
25
|
+
```mermaid
|
|
26
|
+
sequenceDiagram
|
|
27
|
+
participant TV as 📺 Device
|
|
28
|
+
participant API as ⚙️ Your API
|
|
29
|
+
participant Phone as 📱 Phone
|
|
30
|
+
|
|
31
|
+
TV->>API: POST /device/authorize
|
|
32
|
+
API-->>TV: user_code, device_code, verification_uri
|
|
33
|
+
Note over TV: shows WDJB-MJHT<br/>and a QR of the link
|
|
34
|
+
|
|
35
|
+
loop until approved or expired
|
|
36
|
+
TV->>API: POST /device/token
|
|
37
|
+
API-->>TV: authorization_pending
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
Phone->>Phone: scans the QR
|
|
41
|
+
Phone->>API: POST /link (approve)
|
|
42
|
+
API-->>Phone: approved
|
|
43
|
+
|
|
44
|
+
TV->>API: POST /device/token
|
|
45
|
+
API-->>TV: access_token
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Three entry points, one per device in the flow. No UI components for any of
|
|
49
|
+
them — state and lifecycle hooks instead, so React, Vue, Svelte, Solid, Angular
|
|
50
|
+
and React Native each bind it with their own conventions.
|
|
51
|
+
|
|
52
|
+
| Entry | Runs on | Gives you |
|
|
53
|
+
| ----------------------------- | ---------------------- | ------------------------------------------ |
|
|
54
|
+
| `@saeris/hanko` + `/handlers` | your API | grant lifecycle, `Request`→`Response` glue |
|
|
55
|
+
| `@saeris/hanko/client` | the device signing in | poll loop, QR rendering |
|
|
56
|
+
| `@saeris/hanko/approve` | the device granting it | QR reading, confirmation challenges |
|
|
57
|
+
|
|
58
|
+
## 📷 The QR decoder
|
|
59
|
+
|
|
60
|
+
`@saeris/hanko/scan` reads QR codes. Pixels in, string out — no camera, no DOM,
|
|
61
|
+
no dependencies — so the same code runs in a browser, in a worker, on a server,
|
|
62
|
+
or in a test.
|
|
63
|
+
|
|
64
|
+
It exists because every JavaScript alternative is unmaintained: jsQR's last code
|
|
65
|
+
commit was August 2021, qr-scanner's November 2022, and `BarcodeDetector` is
|
|
66
|
+
still a WICG incubation that Safari has never shipped.
|
|
67
|
+
|
|
68
|
+
Measured against the 718-image [BoofCV benchmark][boofcv] — photographs, not
|
|
69
|
+
renders:
|
|
70
|
+
|
|
71
|
+
| Decoder | Recognition |
|
|
72
|
+
| --------- | ----------- |
|
|
73
|
+
| **hanko** | **74.4%** |
|
|
74
|
+
| BoofCV | 60.69% |
|
|
75
|
+
| ZBar | 38.95% |
|
|
76
|
+
| ZXing | 31.87% |
|
|
77
|
+
| jsQR | 24.8% |
|
|
78
|
+
|
|
79
|
+
### Decoding one image
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { createQrDecoder, toGray } from "@saeris/hanko/scan";
|
|
83
|
+
|
|
84
|
+
const decoder = createQrDecoder();
|
|
85
|
+
const symbol = decoder.decode(toGray(rgba, width, height));
|
|
86
|
+
|
|
87
|
+
symbol?.value; // the payload, or null if nothing was found
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Scanning with a camera
|
|
91
|
+
|
|
92
|
+
Three pieces: a worker holding the decoder, something to turn a camera into
|
|
93
|
+
greyscale frames, and a loop between them.
|
|
94
|
+
|
|
95
|
+
The worker is not optional. The retry ladder deliberately outruns its own time
|
|
96
|
+
budget — the budget is checked between attempts, not inside them — so a
|
|
97
|
+
synchronous decode stalls the preview on exactly the frames someone is lining
|
|
98
|
+
up.
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
// decoder.worker.ts
|
|
102
|
+
import { serveDecoder } from "@saeris/hanko/scan/worker";
|
|
103
|
+
|
|
104
|
+
// No time budget: a worker has no preview to block, so the ladder may run to
|
|
105
|
+
// exhaustion. Worth roughly twenty points of recognition over the 120ms a
|
|
106
|
+
// synchronous decode has to respect.
|
|
107
|
+
serveDecoder(self, { timeBudgetMs: 0 });
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
// camera.ts
|
|
112
|
+
export const openCamera = async (video: HTMLVideoElement) => {
|
|
113
|
+
const stream = await navigator.mediaDevices.getUserMedia({
|
|
114
|
+
// A preference, not a guarantee — a laptop with one camera ignores it.
|
|
115
|
+
video: { facingMode: { ideal: `environment` } },
|
|
116
|
+
audio: false
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
video.srcObject = stream;
|
|
120
|
+
// Without this iOS opens the system player full-screen instead of playing
|
|
121
|
+
// inline, and there is no preview to aim with.
|
|
122
|
+
video.setAttribute(`playsinline`, ``);
|
|
123
|
+
await video.play();
|
|
124
|
+
|
|
125
|
+
const canvas = document.createElement(`canvas`);
|
|
126
|
+
// `willReadFrequently` matters: without it the browser keeps the canvas on
|
|
127
|
+
// the GPU and every `getImageData` is a synchronous readback, which is the
|
|
128
|
+
// most expensive thing in this loop.
|
|
129
|
+
const context = canvas.getContext(`2d`, { willReadFrequently: true })!;
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
grab: () => {
|
|
133
|
+
const width = video.videoWidth;
|
|
134
|
+
const height = video.videoHeight;
|
|
135
|
+
// Zero before the first paint, and on iOS while the element is paused.
|
|
136
|
+
if (width === 0 || height === 0) return null;
|
|
137
|
+
|
|
138
|
+
canvas.width = width;
|
|
139
|
+
canvas.height = height;
|
|
140
|
+
context.drawImage(video, 0, 0);
|
|
141
|
+
const { data } = context.getImageData(0, 0, width, height);
|
|
142
|
+
|
|
143
|
+
// Converted here rather than in the worker: it quarters the bytes
|
|
144
|
+
// transferred per frame.
|
|
145
|
+
const grey = new Uint8ClampedArray(width * height);
|
|
146
|
+
for (let i = 0, p = 0; i < grey.length; i++, p += 4) {
|
|
147
|
+
grey[i] = (data[p] * 77 + data[p + 1] * 150 + data[p + 2] * 29) >> 8;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return { data: grey, width, height };
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
stop: () => {
|
|
154
|
+
for (const track of stream.getTracks()) track.stop();
|
|
155
|
+
video.srcObject = null;
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
};
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
// the loop
|
|
163
|
+
import { createWorkerScanner } from "@saeris/hanko/scan";
|
|
164
|
+
import { openCamera } from "./camera";
|
|
165
|
+
|
|
166
|
+
const camera = await openCamera(document.querySelector(`video`)!);
|
|
167
|
+
const scanner = createWorkerScanner(
|
|
168
|
+
new Worker(new URL("./decoder.worker.ts", import.meta.url), {
|
|
169
|
+
type: `module`
|
|
170
|
+
})
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
let scanning = true;
|
|
174
|
+
|
|
175
|
+
while (scanning) {
|
|
176
|
+
const frame = camera.grab();
|
|
177
|
+
|
|
178
|
+
if (frame !== null) {
|
|
179
|
+
// Awaited one at a time on purpose. A camera produces frames faster than
|
|
180
|
+
// they decode, and `scan` drops anything offered while a decode is in
|
|
181
|
+
// flight rather than queueing it — the next frame is always a better
|
|
182
|
+
// input than a backlogged one.
|
|
183
|
+
const symbol = await scanner.scan(frame);
|
|
184
|
+
|
|
185
|
+
if (symbol !== null) {
|
|
186
|
+
console.log(symbol.value);
|
|
187
|
+
scanning = false;
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Yield to the compositor. The decode happens on another thread, but this
|
|
193
|
+
// loop still runs on the main one.
|
|
194
|
+
await new Promise((resolve) => requestAnimationFrame(resolve));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
scanner.close();
|
|
198
|
+
camera.stop();
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
A scanned payload is untrusted input. If you turn one into a link, restrict it
|
|
202
|
+
to `http:` and `https:` and show the raw text beside it — `new URL()` happily
|
|
203
|
+
accepts `javascript:`.
|
|
204
|
+
|
|
205
|
+
For a camera that should keep trying across frames rather than exhausting the
|
|
206
|
+
ladder on one, `createProgressiveScanner` spends a small budget per frame and
|
|
207
|
+
raises it while a symbol is in view.
|
|
208
|
+
|
|
209
|
+
Coverage per condition, and the negative results behind it, live in
|
|
210
|
+
[plan/qr-coverage.md](plan/qr-coverage.md). Both move often.
|
|
211
|
+
|
|
212
|
+
## 🏗️ How it works
|
|
213
|
+
|
|
214
|
+
hanko owns the **grant lifecycle** and nothing else. It never mints sessions or
|
|
215
|
+
touches your user table: it tells you a grant was approved and by whom, and
|
|
216
|
+
issuing a token from that is your application's decision.
|
|
217
|
+
|
|
218
|
+
```mermaid
|
|
219
|
+
flowchart LR
|
|
220
|
+
subgraph device["📺 Device"]
|
|
221
|
+
client["@saeris/hanko/client"]
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
subgraph api["⚙️ Your API"]
|
|
225
|
+
server["@saeris/hanko + /handlers"]
|
|
226
|
+
store[("DeviceGrantStore")]
|
|
227
|
+
server <--> store
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
subgraph phone["📱 Phone"]
|
|
231
|
+
approve["@saeris/hanko/approve"]
|
|
232
|
+
scan["@saeris/hanko/scan"]
|
|
233
|
+
approve --> scan
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
client -->|authorize, then poll| server
|
|
237
|
+
approve -->|approve| server
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Decisions worth knowing
|
|
241
|
+
|
|
242
|
+
- **Zero runtime dependencies.** The grant lifecycle is the product; rendering a
|
|
243
|
+
QR is a different class of problem, so it sits behind `@saeris/hanko/qr` and
|
|
244
|
+
defers to [`etiket`][etiket] as an optional peer. Reading one had no
|
|
245
|
+
maintained option at all, which is why `/scan` exists.
|
|
246
|
+
- **WinterTC primitives only.** `Request`, `Response`, `crypto.getRandomValues` —
|
|
247
|
+
so one build runs on Node, Deno, Bun, Cloudflare Workers and in a browser,
|
|
248
|
+
with no adapter per runtime.
|
|
249
|
+
- **A short code is a small keyspace.** That is the deliberate trade for
|
|
250
|
+
readability, and [RFC 8628 §5.1][rfc-security] **requires you to rate-limit
|
|
251
|
+
attempts**. hanko does not do it for you: that belongs at your HTTP boundary,
|
|
252
|
+
where you can see IPs.
|
|
253
|
+
- **Approval is two steps, not one.** Scanning a code that says "approve this"
|
|
254
|
+
and approving it are separate, because a QR is a link anyone can point a
|
|
255
|
+
camera at. `/approve` ships the confirmation challenge that closes that gap.
|
|
256
|
+
- **The store is four methods.** `create`, `findByDeviceCode`, `findByUserCode`
|
|
257
|
+
and `update`, plus an optional `prune` — small enough that an adapter for your
|
|
258
|
+
database is a short file. The bundled memory store is for development only.
|
|
259
|
+
|
|
260
|
+
## 📦 Install
|
|
261
|
+
|
|
262
|
+
```sh
|
|
263
|
+
yarn add @saeris/hanko
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Add `etiket` only if you render the device screen:
|
|
267
|
+
|
|
268
|
+
```sh
|
|
269
|
+
yarn add etiket
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Codes follow the spec's worked example: 8 characters from a 20-consonant
|
|
273
|
+
alphabet, shown as `WDJB-MJHT`. No vowels, so a code cannot spell a word; no
|
|
274
|
+
digits, so there is no `0`/`O` or `1`/`l`/`I` to misread.
|
|
275
|
+
|
|
276
|
+
## 🔧 Examples
|
|
277
|
+
|
|
278
|
+
The whole sign-in flow, wired end to end, lives in
|
|
279
|
+
[`examples/astro`](examples/astro) — three surfaces, a store, rate limiting and
|
|
280
|
+
the confirmation challenge, with the framework-specific parts explained in
|
|
281
|
+
[its README](examples/astro/README.md). That is the reference for building the
|
|
282
|
+
flow; this README covers the pieces rather than the assembly.
|
|
283
|
+
|
|
284
|
+
More are planned — Next.js first, mirroring the Astro one closely enough to
|
|
285
|
+
diff, then other frameworks and UI stacks.
|
|
286
|
+
|
|
287
|
+
| Route | What it is |
|
|
288
|
+
| ---------------- | -------------------------------------------------------------------- |
|
|
289
|
+
| `/` | the map — what to open where, and whether the URL reaches a phone |
|
|
290
|
+
| `/tv` | the device screen — a short code and a QR for a pending grant |
|
|
291
|
+
| `/signin` | the phone's own sign-in; this identity is what the device inherits |
|
|
292
|
+
| `/account` | approved devices, and revoking them |
|
|
293
|
+
| `/link` | the approval page — scans that QR, or takes a typed code |
|
|
294
|
+
| `/scanner` | a bare scanner, showing the payload raw and as a link |
|
|
295
|
+
| `/scanner-debug` | the same, reporting each stage on screen for a phone with no console |
|
|
296
|
+
|
|
297
|
+
```bash
|
|
298
|
+
yarn demo # builds the library, then serves the example
|
|
299
|
+
yarn demo:share # the same, over TLS so a phone can reach it
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
A camera needs a secure context, so a bare LAN IP will not do — `demo:share`
|
|
303
|
+
puts a TLS proxy in front, which is what makes the phone half testable at all.
|
|
304
|
+
|
|
305
|
+
## 🧪 Checks
|
|
306
|
+
|
|
307
|
+
| What | Command | Notes |
|
|
308
|
+
| -------------- | ------------------- | ----------------------------------------------- |
|
|
309
|
+
| Everything | `vp run ci` | `vp pack && vp check && vp test`, what CI runs. |
|
|
310
|
+
| Tests | `vp test` | Pure TypeScript; never imports react-native. |
|
|
311
|
+
| Recognition | `yarn bench:corpus` | The 718-image corpus, sharded across workers. |
|
|
312
|
+
| Decode profile | `yarn bench` | A still image, for `deoptkit`. |
|
|
313
|
+
|
|
314
|
+
`vp pack` has to run before `vp check`: the example resolves `@saeris/hanko`
|
|
315
|
+
through the `exports` map, which points at `dist/`, so a clean checkout cannot
|
|
316
|
+
typecheck it until the library is built.
|
|
317
|
+
|
|
318
|
+
## 🚀 Releasing
|
|
319
|
+
|
|
320
|
+
Driven by [bumpy][bumpy]. Every change carries a **bump file** in `.bumpy/`
|
|
321
|
+
saying what moved and how far, so the changelog cannot fall behind the code.
|
|
322
|
+
|
|
323
|
+
```bash
|
|
324
|
+
yarn bumpy add
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
Merging that opens a **Version PR**; merging _that_ tags the release and
|
|
328
|
+
publishes to npm over OIDC trusted publishing, so no token is stored. The
|
|
329
|
+
example deploys to Vercel on push, independently of releases.
|
|
330
|
+
|
|
331
|
+
## 🥂 License
|
|
332
|
+
|
|
333
|
+
[MIT][license] © [Drake Costa][personal-website]
|
|
334
|
+
|
|
335
|
+
[rfc]: https://datatracker.ietf.org/doc/html/rfc8628
|
|
336
|
+
[rfc-security]: https://datatracker.ietf.org/doc/html/rfc8628#section-5
|
|
337
|
+
[etiket]: https://github.com/productdevbook/etiket
|
|
338
|
+
[boofcv]: https://boofcv.org/index.php?title=Performance:QrCode
|
|
339
|
+
[bumpy]: https://github.com/dmno-dev/bumpy
|
|
340
|
+
[ci_badge]: https://github.com/Saeris/hanko/actions/workflows/ci.yml/badge.svg
|
|
341
|
+
[ci]: https://github.com/Saeris/hanko/actions/workflows/ci.yml
|
|
342
|
+
[npm_badge]: https://img.shields.io/npm/v/@saeris/hanko.svg
|
|
343
|
+
[npm]: https://www.npmjs.com/package/@saeris/hanko
|
|
344
|
+
[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg
|
|
345
|
+
[license]: https://github.com/Saeris/hanko/blob/main/LICENSE.md
|
|
346
|
+
[personal-website]: https://saeris.gg
|