@saeris/hanko 0.0.0 → 0.2.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.
Files changed (57) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/LICENSE.md +21 -0
  3. package/README.md +605 -0
  4. package/dist/approve/index.d.mts +318 -0
  5. package/dist/approve/index.d.mts.map +1 -0
  6. package/dist/approve/index.mjs +393 -0
  7. package/dist/approve/index.mjs.map +1 -0
  8. package/dist/client/index.d.mts +101 -0
  9. package/dist/client/index.d.mts.map +1 -0
  10. package/dist/client/index.mjs +215 -0
  11. package/dist/client/index.mjs.map +1 -0
  12. package/dist/codes-Ba_qYH6u.mjs +93 -0
  13. package/dist/codes-Ba_qYH6u.mjs.map +1 -0
  14. package/dist/handlers.d.mts +113 -0
  15. package/dist/handlers.d.mts.map +1 -0
  16. package/dist/handlers.mjs +194 -0
  17. package/dist/handlers.mjs.map +1 -0
  18. package/dist/index.d.mts +5 -0
  19. package/dist/index.mjs +345 -0
  20. package/dist/index.mjs.map +1 -0
  21. package/dist/linking-DcQSMgem.mjs +177 -0
  22. package/dist/linking-DcQSMgem.mjs.map +1 -0
  23. package/dist/linking-nKoayyHf.d.mts +133 -0
  24. package/dist/linking-nKoayyHf.d.mts.map +1 -0
  25. package/dist/machine-CRHKjtoP.d.mts +223 -0
  26. package/dist/machine-CRHKjtoP.d.mts.map +1 -0
  27. package/dist/machine-D_5DAFxi.mjs +155 -0
  28. package/dist/machine-D_5DAFxi.mjs.map +1 -0
  29. package/dist/qr.d.mts +58 -0
  30. package/dist/qr.d.mts.map +1 -0
  31. package/dist/qr.mjs +27 -0
  32. package/dist/qr.mjs.map +1 -0
  33. package/dist/scan/index.d.mts +381 -0
  34. package/dist/scan/index.d.mts.map +1 -0
  35. package/dist/scan/index.mjs +409 -0
  36. package/dist/scan/index.mjs.map +1 -0
  37. package/dist/scan/worker.d.mts +2 -0
  38. package/dist/scan/worker.mjs +2 -0
  39. package/dist/server-BhoYRkCm.d.mts +257 -0
  40. package/dist/server-BhoYRkCm.d.mts.map +1 -0
  41. package/dist/stores/kv.d.mts +64 -0
  42. package/dist/stores/kv.d.mts.map +1 -0
  43. package/dist/stores/kv.mjs +87 -0
  44. package/dist/stores/kv.mjs.map +1 -0
  45. package/dist/stores/memory.d.mts +22 -0
  46. package/dist/stores/memory.d.mts.map +1 -0
  47. package/dist/stores/memory.mjs +42 -0
  48. package/dist/stores/memory.mjs.map +1 -0
  49. package/dist/types-BvBIFPH6.mjs +7 -0
  50. package/dist/types-BvBIFPH6.mjs.map +1 -0
  51. package/dist/types-C82lb-zX.d.mts +82 -0
  52. package/dist/types-C82lb-zX.d.mts.map +1 -0
  53. package/dist/worker-BdwaK1uX.mjs +5291 -0
  54. package/dist/worker-BdwaK1uX.mjs.map +1 -0
  55. package/dist/worker-DxbdBA2z.d.mts +164 -0
  56. package/dist/worker-DxbdBA2z.d.mts.map +1 -0
  57. package/package.json +116 -3
@@ -0,0 +1,318 @@
1
+ import { n as ApprovalState } from "../machine-CRHKjtoP.mjs";
2
+ import { a as buildAppSchemeUrl, c as digitalAssetLinks, d as pwaLaunchHandler, i as appleAppSiteAssociation, l as expoLinkingConfig, n as LinkSource, o as buildApprovalUrl, r as ParsedApprovalLink, s as consumeLaunchTarget, t as LinkConfig, u as parseApprovalLink } from "../linking-nKoayyHf.mjs";
3
+ //#region src/scanner.d.ts
4
+ /**
5
+ * QR reading for the approving device.
6
+ *
7
+ * Generation is dependency-free; decoding cannot be. hanko defines the
8
+ * {@link QrScanner} interface — two methods — and lets the host bring a
9
+ * decoder rather than picking one for everybody.
10
+ *
11
+ * That indirection is not ceremony. `BarcodeDetector` looks like the obvious
12
+ * answer and is NOT a web standard: it is a WICG incubation that MDN flags as
13
+ * outside Baseline, Safari has never shipped it, and no vendor has committed
14
+ * to it. Building against it directly means the scanner silently does nothing
15
+ * on an iPhone — the device most people approve from.
16
+ *
17
+ * What works today, in rough order of preference:
18
+ *
19
+ * - **`qr-scanner`** — ~6 kB gzipped, self-contained, uses a native
20
+ * `BarcodeDetector` where one exists and its own worker otherwise. It owns
21
+ * the camera too, which is most of the work. See the Astro example.
22
+ * - **`barcode-detector`** — a ponyfill over ZXing-C++/WASM. More formats and
23
+ * actively maintained, but it fetches its WASM from a CDN at runtime, which
24
+ * is a poor default on an authentication screen.
25
+ * - **`expo-camera`** on React Native, where no web API exists at all.
26
+ *
27
+ * {@link createBarcodeDetectorScanner} remains for the native path and for
28
+ * ponyfills mirroring that API.
29
+ */
30
+ /** Minimal shape of a detected barcode. Mirrors the Barcode Detection API. */
31
+ /**
32
+ * A camera frame, as an opaque handle.
33
+ *
34
+ * The DOM's `ImageBitmapSource` in a browser — a `<video>`, `ImageBitmap`,
35
+ * `Blob`, or canvas — but declared locally rather than pulled from `lib.dom`.
36
+ * This package compiles without the DOM lib on purpose, so server code cannot
37
+ * reach a browser-only global by accident, and so the same types work for a
38
+ * React Native frame that is not an `ImageBitmapSource` at all.
39
+ *
40
+ * hanko never inspects the frame; it passes it straight to the detector.
41
+ */
42
+ type CameraFrame = unknown;
43
+ /** Minimal shape of a detected barcode. Mirrors the Barcode Detection API. */
44
+ interface DetectedBarcode {
45
+ rawValue: string;
46
+ }
47
+ /**
48
+ * What hanko needs from a scanner: turn a frame into strings.
49
+ *
50
+ * Deliberately narrower than `BarcodeDetector` — no bounding boxes, no
51
+ * formats — so an `expo-camera` or jsQR adapter is trivial to write.
52
+ */
53
+ interface QrScanner {
54
+ detect(source: CameraFrame): Promise<DetectedBarcode[]>;
55
+ }
56
+ /** The subset of the global `BarcodeDetector` this module uses. */
57
+ interface BarcodeDetectorLike {
58
+ detect(source: CameraFrame): Promise<DetectedBarcode[]>;
59
+ }
60
+ interface BarcodeDetectorConstructor {
61
+ new (options?: {
62
+ formats?: string[];
63
+ }): BarcodeDetectorLike;
64
+ }
65
+ /** Whether this runtime has a native `BarcodeDetector`. */
66
+ declare const hasNativeBarcodeDetector: () => boolean;
67
+ /**
68
+ * Scanner backed by the platform's `BarcodeDetector`.
69
+ *
70
+ * Pass the ponyfill's constructor where the native one is missing:
71
+ *
72
+ * ```ts
73
+ * import { BarcodeDetector } from "barcode-detector/ponyfill";
74
+ * const scanner = createBarcodeDetectorScanner({ detector: BarcodeDetector });
75
+ * ```
76
+ *
77
+ * Restricted to `qr_code`: a taplist beer can carries an EAN-13 barcode, and a
78
+ * scanner that reported it here would send a product code to the approval
79
+ * endpoint as though it were a user code.
80
+ */
81
+ declare const createBarcodeDetectorScanner: ({ detector }?: {
82
+ /** Constructor to use. Defaults to the global when present. */
83
+ detector?: BarcodeDetectorConstructor;
84
+ }) => QrScanner;
85
+ /** Everything hanko can pull out of a scanned payload. */
86
+ interface ScannedPayload {
87
+ /** The code to send to the approval endpoint. */
88
+ userCode: string;
89
+ /** The URL the QR encoded, when it was a URL. */
90
+ verificationUri?: string;
91
+ }
92
+ /**
93
+ * Read a `user_code` out of a scanned string.
94
+ *
95
+ * QRs in this flow carry `verification_uri_complete` — a URL with the code in
96
+ * it — but a scanner may also see a bare code from a hand-typed fallback, so
97
+ * both are accepted.
98
+ *
99
+ * Returns `null` rather than throwing: a camera pointed at the world sees
100
+ * unrelated codes constantly, and each one is a normal non-event, not an
101
+ * error.
102
+ */
103
+ declare const parseScannedCode: (raw: string, { param }?: {
104
+ param?: string;
105
+ }) => ScannedPayload | null;
106
+ //#endregion
107
+ //#region src/challenge.d.ts
108
+ /**
109
+ * Confirmation challenges for the approving device.
110
+ *
111
+ * RFC 8628 §5.4: the approval screen SHOULD show the code and ask the user to
112
+ * verify it matches the device in front of them. Scanning a QR skips the
113
+ * typing, which also skips the moment where the user would have noticed the
114
+ * code was wrong — so the check has to be reintroduced deliberately.
115
+ *
116
+ * How much friction that check deserves is a product decision, not a protocol
117
+ * one. Discord and Steam ship a bare "Approve?"; GitHub's sudo flow makes you
118
+ * type a code; Google shows three numbers and asks you to pick the one on the
119
+ * other screen. All are legitimate for different risk levels, so the strategy
120
+ * is pluggable and the machine only cares whether it passed.
121
+ *
122
+ * These run on the ALREADY AUTHENTICATED device. They do not authenticate the
123
+ * user — they establish that the user can see the screen requesting access.
124
+ */
125
+ /** Outcome of a challenge attempt. */
126
+ interface ChallengeResult {
127
+ ok: boolean;
128
+ /** Present when `ok` is false, for a precise message. */
129
+ reason?: `mismatch` | `cancelled` | `unavailable`;
130
+ }
131
+ /**
132
+ * A confirmation strategy.
133
+ *
134
+ * `present` gives the UI whatever it needs to render (the decoys for a
135
+ * triplet, say); `verify` checks the user's answer. Async because a biometric
136
+ * or WebAuthn prompt is inherently asynchronous.
137
+ */
138
+ interface ChallengeStrategy<TPrompt = unknown, TAnswer = unknown> {
139
+ /** Stable name, so a UI can switch on which challenge it is rendering. */
140
+ readonly kind: string;
141
+ /** What the UI should show. Called once per confirmation. */
142
+ present(userCode: string): TPrompt;
143
+ /** Check the user's answer. */
144
+ verify(answer: TAnswer, userCode: string): Promise<ChallengeResult>;
145
+ }
146
+ /**
147
+ * No challenge — approving is a single tap.
148
+ *
149
+ * What Discord and Steam ship. Appropriate when the QR was scanned inside your
150
+ * own authenticated app, which already proves possession of the phone. Note
151
+ * this is weaker than RFC 8628 §5.4 asks for: nothing stops a user approving a
152
+ * code they never actually looked at.
153
+ */
154
+ declare const noChallenge: () => ChallengeStrategy<null, void>;
155
+ /**
156
+ * The user types the code shown on the device.
157
+ *
158
+ * The RFC's own suggestion, and GitHub's sudo pattern. Highest friction,
159
+ * strongest guarantee: an attacker who phished the QR cannot supply a code the
160
+ * user can read off their own screen.
161
+ */
162
+ declare const codeEntryChallenge: () => ChallengeStrategy<{
163
+ kind: `code-entry`;
164
+ }, string>;
165
+ /** What a triplet challenge hands the UI. */
166
+ interface TripletPrompt {
167
+ kind: `triplet`;
168
+ /** The real code plus decoys, already shuffled. Render as-is. */
169
+ choices: string[];
170
+ }
171
+ /**
172
+ * The user picks the real code from a small set of decoys.
173
+ *
174
+ * Google's mobile approval pattern. Most of code-entry's guarantee at a
175
+ * fraction of the friction — one tap instead of eight keystrokes — which
176
+ * matters on a phone. A wrong pick is a real signal: the user is not looking
177
+ * at the device they are authorizing.
178
+ */
179
+ declare const tripletChallenge: ({ decoys, generate, shuffle }: {
180
+ /** Decoys alongside the real code. Two gives the classic three-up. */
181
+ decoys?: number;
182
+ /** Produces a decoy. Must match the real code's shape to be plausible. */
183
+ generate: () => string;
184
+ /** Injectable for deterministic tests. */
185
+ shuffle?: <T>(items: T[]) => T[];
186
+ } & {
187
+ generate: () => string;
188
+ }) => ChallengeStrategy<TripletPrompt, string>;
189
+ /**
190
+ * Delegate to something the platform provides — FaceID, Touch ID, WebAuthn, a
191
+ * device passcode.
192
+ *
193
+ * hanko cannot implement these: they are platform APIs with no common
194
+ * interface. What it can do is give them a slot in the same machine, so a host
195
+ * that has one is not forced outside the flow to use it.
196
+ *
197
+ * A biometric proves possession of the phone, NOT that the user looked at the
198
+ * device screen — so for a public taproom TV, pair it with `codeEntry` or
199
+ * `triplet` rather than using it alone.
200
+ */
201
+ declare const platformChallenge: ({ kind, authenticate }: {
202
+ kind?: string;
203
+ /** Resolves true when the platform accepted the user. */
204
+ authenticate: (userCode: string) => Promise<boolean>;
205
+ }) => ChallengeStrategy<{
206
+ kind: string;
207
+ }, void>;
208
+ /**
209
+ * Require several challenges in order.
210
+ *
211
+ * The composition that makes sense in practice: a biometric proves the phone,
212
+ * a code check proves the screen. Neither alone covers both.
213
+ *
214
+ * Short-circuits on the first failure, and its `present` returns every prompt
215
+ * so a UI can render them as steps.
216
+ */
217
+ declare const allOf: (strategies: ChallengeStrategy[]) => ChallengeStrategy<unknown[], unknown[]>;
218
+ //#endregion
219
+ //#region src/approve/index.d.ts
220
+ /** What the approval endpoint tells us about a code. */
221
+ interface ResolvedGrant {
222
+ /** The code, as the server has it. Display this for the user to check. */
223
+ userCode: string;
224
+ /** Optional detail a host may show: which client is asking, and for what. */
225
+ clientId?: string;
226
+ scope?: string;
227
+ }
228
+ interface ApprovalHooks {
229
+ /** Any successful transition. The main binding point for a UI. */
230
+ onTransition?: (from: ApprovalState, to: ApprovalState) => void;
231
+ /** A code was read. Fires before the server is asked about it. */
232
+ onCode?: (userCode: string) => void;
233
+ /** The server resolved the code. Carries what to show the user. */
234
+ onResolved?: (grant: ResolvedGrant, prompt: unknown) => void;
235
+ /** The challenge was answered incorrectly. The user may retry. */
236
+ onChallengeFailed?: (reason: string | undefined, attempts: number) => void;
237
+ /** Terminal: the decision was recorded. */
238
+ onSettled?: (state: ApprovalState) => void;
239
+ /** Recoverable failure — camera or network. */
240
+ onError?: (error: unknown, state: ApprovalState) => void;
241
+ }
242
+ interface ApprovalClientOptions {
243
+ /** Resolve a code to a grant. Usually `GET /link?user_code=…`. */
244
+ resolve: (userCode: string) => Promise<ResolvedGrant | null>;
245
+ /** Record the decision. Usually `POST /link`. */
246
+ submit: (userCode: string, approved: boolean) => Promise<void>;
247
+ /**
248
+ * How the user proves they are looking at the requesting screen.
249
+ *
250
+ * Defaults to {@link noChallenge} — one tap, what Discord and Steam ship.
251
+ * For a public screen prefer `codeEntryChallenge` or `tripletChallenge`,
252
+ * which is what RFC 8628 §5.4 actually asks for.
253
+ */
254
+ challenge?: ChallengeStrategy;
255
+ /** Needed only for the in-app scanning path. */
256
+ scanner?: QrScanner;
257
+ /** Query parameter carrying the code in scanned URLs. */
258
+ codeParam?: string;
259
+ hooks?: ApprovalHooks;
260
+ }
261
+ declare class ApprovalClient {
262
+ #private;
263
+ constructor({ resolve, submit, challenge, scanner, codeParam, hooks }: ApprovalClientOptions);
264
+ get state(): ApprovalState;
265
+ get settled(): boolean;
266
+ /** The resolved grant, once known. Show its code for the user to verify. */
267
+ get grant(): ResolvedGrant | undefined;
268
+ /** Whatever the challenge wants rendered — triplet choices, a prompt kind. */
269
+ get challengePrompt(): unknown;
270
+ get challengeKind(): string;
271
+ /** Whether the challenge has been satisfied, so approval may proceed. */
272
+ get confirmed(): boolean;
273
+ /** Open the scanner (in-app path). */
274
+ startScanning(): void;
275
+ /**
276
+ * Offer a camera frame to the scanner.
277
+ *
278
+ * Returns the code when this frame contained one. Call it per frame and
279
+ * ignore the nulls — a camera pointed at a room sees unrelated codes
280
+ * constantly, and each is a non-event rather than an error.
281
+ */
282
+ scan(source: CameraFrame): Promise<string | null>;
283
+ /**
284
+ * Hand over a code from the URL (Plex path) or a manual entry field.
285
+ *
286
+ * The same entry point as a successful scan, so both topologies converge on
287
+ * one flow from here.
288
+ */
289
+ submitCode(userCode: string): Promise<boolean>;
290
+ /**
291
+ * Answer the confirmation challenge.
292
+ *
293
+ * Separate from {@link approve} so a UI can verify as the user types (or as
294
+ * a biometric returns) and enable the approve button only once it passes.
295
+ */
296
+ confirm(answer?: unknown): Promise<boolean>;
297
+ /**
298
+ * Approve the sign-in.
299
+ *
300
+ * Refuses while the challenge is unsatisfied. That guard is the point of the
301
+ * challenge existing: a UI bug that wires the button straight to this method
302
+ * must not be able to skip the check.
303
+ */
304
+ approve(): Promise<boolean>;
305
+ /**
306
+ * Refuse the sign-in.
307
+ *
308
+ * Deliberately NOT gated on the challenge. A user who cannot confirm a code
309
+ * is exactly the user most likely to be looking at a phishing attempt, and
310
+ * they must always be able to say no.
311
+ */
312
+ deny(): Promise<boolean>;
313
+ /** Start over after a failure or an unrecognized code. */
314
+ reset(): void;
315
+ }
316
+ //#endregion
317
+ export { ApprovalClient, ApprovalClientOptions, ApprovalHooks, type CameraFrame, type ChallengeResult, type ChallengeStrategy, type DetectedBarcode, type LinkConfig, type LinkSource, type ParsedApprovalLink, type QrScanner, ResolvedGrant, type ScannedPayload, type TripletPrompt, allOf, appleAppSiteAssociation, buildAppSchemeUrl, buildApprovalUrl, codeEntryChallenge, consumeLaunchTarget, createBarcodeDetectorScanner, digitalAssetLinks, expoLinkingConfig, hasNativeBarcodeDetector, noChallenge, parseApprovalLink, parseScannedCode, platformChallenge, pwaLaunchHandler, tripletChallenge };
318
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/scanner.ts","../../src/challenge.ts","../../src/approve/index.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAuCY;;UAGK;EACf;;;;;;;;UASe;EACf,OAAO,QAAQ,cAAc,QAAQ;;;UAI7B;EACR,OAAO,QAAQ,cAAc,QAAQ;;UAG7B;OACH;IAAY;MAAuB;;;cAI7B;;;;;;;;;;;;;;;cAiBA,iCAAgC;;EAI3C,WAAW;MACJ;;UA2BQ;;EAEf;;EAEA;;;;;;;;;;;;;cAcW,mBACX,eACA;EAA2B;MAC1B;;;;;;;;;;;;;;;;;;;;;UCnHc;EACf;;EAEA;;;;;;;;;UAUe,kBAAkB,mBAAmB;;WAE3C;;EAET,QAAQ,mBAAmB;;EAE3B,OAAO,QAAQ,SAAS,mBAAmB,QAAQ;;;;;;;;;;cAWxC,mBAAkB;;;;;;;;cAalB,0BAAyB;EAClC;;;UAgBa;EACf;;EAEA;;;;;;;;;;cAWW,qBAAoB,QAAA,UAAA;;EAM/B;;EAEA;;EAEA,WAAW,GAAG,OAAO,QAAQ;;EACzB;MAA2B,kBAAkB;;;;;;;;;;;;;cAoCtC,sBAAqB,MAAA;EAIhC;;EAEA,eAAe,qBAAqB;MAClC;EAAoB;;;;;;;;;;;cAyBX,QACX,YAAY,wBACX;;;;UClJc;;EAEf;;EAEA;EACA;;UAGe;;EAEf,gBAAgB,MAAM,eAAe,IAAI;;EAEzC,UAAU;;EAEV,cAAc,OAAO,eAAe;;EAEpC,qBAAqB,4BAA4B;;EAEjD,aAAa,OAAO;;EAEpB,WAAW,gBAAgB,OAAO;;UAGnB;;EAEf,UAAU,qBAAqB,QAAQ;;EAEvC,SAAS,kBAAkB,sBAAsB;;;;;;;;EAQjD,YAAY;;EAEZ,UAAU;;EAEV;EACA,QAAQ;;cAGG;;EAcC,cACV,SACA,QACA,WACA,SACA,WACA,SACC;MASC,SAAS;MAIT;;MAKA,SAAS;;MAKT;MAIA;;MAKA;;EAgBJ;;;;;;;;EAWM,KAAK,QAAQ,cAAc;;;;;;;EA6B3B,WAAW,mBAAmB;;;;;;;EAoC9B,QAAQ,mBAAmB;;;;;;;;EAyB3B,WAAW;;;;;;;;EAYX,QAAQ;;EAqBd"}