@bentoforge/umami-iam 0.2.3 → 0.3.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/dist/client.d.ts +26 -4
- package/dist/client.js +40 -4
- package/package.json +1 -1
- package/src/client.ts +48 -5
package/dist/client.d.ts
CHANGED
|
@@ -70,10 +70,32 @@ export declare class UmamiClient {
|
|
|
70
70
|
registerPasskey(): Promise<{
|
|
71
71
|
credentialId: string;
|
|
72
72
|
}>;
|
|
73
|
-
/** Passwordless login with a passkey via `navigator.credentials.get`; stores the token.
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
|
|
73
|
+
/** Passwordless login with a passkey via `navigator.credentials.get`; stores the token.
|
|
74
|
+
*
|
|
75
|
+
* Omit `username` for a discoverable login: the challenge then carries no allow-list and the
|
|
76
|
+
* authenticator picks both the credential and, with it, the user. Pass `api` to mint the token
|
|
77
|
+
* for a product API directly (default: umami); the session keeps that audience across
|
|
78
|
+
* refreshes. */
|
|
79
|
+
loginWithPasskey(username?: string, api?: string): Promise<void>;
|
|
80
|
+
/** Passkey autofill (WebAuthn conditional mediation).
|
|
81
|
+
*
|
|
82
|
+
* Offers the passkey inside the browser's own autofill UI for the username field instead of
|
|
83
|
+
* behind a button. This is the only way to have a passkey surface alongside — and above — a
|
|
84
|
+
* saved password, because how a password manager ranks its own suggestions is not something a
|
|
85
|
+
* page can influence.
|
|
86
|
+
*
|
|
87
|
+
* Resolves once the user picks a passkey, which may be never: call it and leave the promise
|
|
88
|
+
* pending for the lifetime of the login page, aborting via `signal` when it unmounts. Rejects
|
|
89
|
+
* with `AbortError` in that case. Resolves to `false` without starting anything when the
|
|
90
|
+
* browser has no conditional mediation, so the caller can keep the explicit button.
|
|
91
|
+
*
|
|
92
|
+
* Requires a field marked `autocomplete="username webauthn"` to attach to. */
|
|
93
|
+
loginWithPasskeyAutofill(options?: {
|
|
94
|
+
signal?: AbortSignal;
|
|
95
|
+
api?: string;
|
|
96
|
+
}): Promise<boolean>;
|
|
97
|
+
/** The shared start → `navigator.credentials.get` → finish ceremony. */
|
|
98
|
+
private passkeyCeremony;
|
|
77
99
|
/** Exchanges an `umk_…` API key for a short-lived token (stores it). Server-side/BFF use.
|
|
78
100
|
* `api` selects the target API when the key allows more than one (see `docs/AUDIENCES.md`). */
|
|
79
101
|
exchangeApiKey(apiKey: string, api?: string): Promise<ExchangeResponse>;
|
package/dist/client.js
CHANGED
|
@@ -191,14 +191,50 @@ export class UmamiClient {
|
|
|
191
191
|
}),
|
|
192
192
|
});
|
|
193
193
|
}
|
|
194
|
-
/** Passwordless login with a passkey via `navigator.credentials.get`; stores the token.
|
|
195
|
-
*
|
|
196
|
-
*
|
|
194
|
+
/** Passwordless login with a passkey via `navigator.credentials.get`; stores the token.
|
|
195
|
+
*
|
|
196
|
+
* Omit `username` for a discoverable login: the challenge then carries no allow-list and the
|
|
197
|
+
* authenticator picks both the credential and, with it, the user. Pass `api` to mint the token
|
|
198
|
+
* for a product API directly (default: umami); the session keeps that audience across
|
|
199
|
+
* refreshes. */
|
|
197
200
|
async loginWithPasskey(username, api) {
|
|
198
|
-
|
|
201
|
+
await this.passkeyCeremony(username, api);
|
|
202
|
+
}
|
|
203
|
+
/** Passkey autofill (WebAuthn conditional mediation).
|
|
204
|
+
*
|
|
205
|
+
* Offers the passkey inside the browser's own autofill UI for the username field instead of
|
|
206
|
+
* behind a button. This is the only way to have a passkey surface alongside — and above — a
|
|
207
|
+
* saved password, because how a password manager ranks its own suggestions is not something a
|
|
208
|
+
* page can influence.
|
|
209
|
+
*
|
|
210
|
+
* Resolves once the user picks a passkey, which may be never: call it and leave the promise
|
|
211
|
+
* pending for the lifetime of the login page, aborting via `signal` when it unmounts. Rejects
|
|
212
|
+
* with `AbortError` in that case. Resolves to `false` without starting anything when the
|
|
213
|
+
* browser has no conditional mediation, so the caller can keep the explicit button.
|
|
214
|
+
*
|
|
215
|
+
* Requires a field marked `autocomplete="username webauthn"` to attach to. */
|
|
216
|
+
async loginWithPasskeyAutofill(options) {
|
|
217
|
+
const available = await globalThis.PublicKeyCredential?.isConditionalMediationAvailable?.();
|
|
218
|
+
if (!available)
|
|
219
|
+
return false;
|
|
220
|
+
await this.passkeyCeremony(undefined, options?.api, {
|
|
221
|
+
mediation: "conditional",
|
|
222
|
+
signal: options?.signal,
|
|
223
|
+
});
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
/** The shared start → `navigator.credentials.get` → finish ceremony. */
|
|
227
|
+
async passkeyCeremony(username, api, get) {
|
|
228
|
+
const start = await this.request("/auth/webauthn/login/start",
|
|
229
|
+
// An absent username is what selects the discoverable flow server-side, so send the key
|
|
230
|
+
// only when there is one — `{ username: undefined }` would serialize it away anyway, but
|
|
231
|
+
// being explicit keeps the two flows visibly distinct.
|
|
232
|
+
{ method: "POST", body: JSON.stringify(username ? { username } : {}) }, false);
|
|
199
233
|
const publicKey = toRequestOptions(start.options.publicKey);
|
|
200
234
|
const credential = (await navigator.credentials.get({
|
|
201
235
|
publicKey,
|
|
236
|
+
mediation: get?.mediation,
|
|
237
|
+
signal: get?.signal,
|
|
202
238
|
}));
|
|
203
239
|
if (!credential)
|
|
204
240
|
throw new Error("Passkey login was cancelled");
|
package/package.json
CHANGED
package/src/client.ts
CHANGED
|
@@ -266,18 +266,61 @@ export class UmamiClient {
|
|
|
266
266
|
});
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
-
/** Passwordless login with a passkey via `navigator.credentials.get`; stores the token.
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
|
|
269
|
+
/** Passwordless login with a passkey via `navigator.credentials.get`; stores the token.
|
|
270
|
+
*
|
|
271
|
+
* Omit `username` for a discoverable login: the challenge then carries no allow-list and the
|
|
272
|
+
* authenticator picks both the credential and, with it, the user. Pass `api` to mint the token
|
|
273
|
+
* for a product API directly (default: umami); the session keeps that audience across
|
|
274
|
+
* refreshes. */
|
|
275
|
+
async loginWithPasskey(username?: string, api?: string): Promise<void> {
|
|
276
|
+
await this.passkeyCeremony(username, api);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Passkey autofill (WebAuthn conditional mediation).
|
|
280
|
+
*
|
|
281
|
+
* Offers the passkey inside the browser's own autofill UI for the username field instead of
|
|
282
|
+
* behind a button. This is the only way to have a passkey surface alongside — and above — a
|
|
283
|
+
* saved password, because how a password manager ranks its own suggestions is not something a
|
|
284
|
+
* page can influence.
|
|
285
|
+
*
|
|
286
|
+
* Resolves once the user picks a passkey, which may be never: call it and leave the promise
|
|
287
|
+
* pending for the lifetime of the login page, aborting via `signal` when it unmounts. Rejects
|
|
288
|
+
* with `AbortError` in that case. Resolves to `false` without starting anything when the
|
|
289
|
+
* browser has no conditional mediation, so the caller can keep the explicit button.
|
|
290
|
+
*
|
|
291
|
+
* Requires a field marked `autocomplete="username webauthn"` to attach to. */
|
|
292
|
+
async loginWithPasskeyAutofill(options?: {
|
|
293
|
+
signal?: AbortSignal;
|
|
294
|
+
api?: string;
|
|
295
|
+
}): Promise<boolean> {
|
|
296
|
+
const available = await globalThis.PublicKeyCredential?.isConditionalMediationAvailable?.();
|
|
297
|
+
if (!available) return false;
|
|
298
|
+
await this.passkeyCeremony(undefined, options?.api, {
|
|
299
|
+
mediation: "conditional",
|
|
300
|
+
signal: options?.signal,
|
|
301
|
+
});
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** The shared start → `navigator.credentials.get` → finish ceremony. */
|
|
306
|
+
private async passkeyCeremony(
|
|
307
|
+
username: string | undefined,
|
|
308
|
+
api: string | undefined,
|
|
309
|
+
get?: { mediation?: CredentialMediationRequirement; signal?: AbortSignal },
|
|
310
|
+
): Promise<void> {
|
|
273
311
|
const start = await this.request<{ ceremonyId: string; options: any }>(
|
|
274
312
|
"/auth/webauthn/login/start",
|
|
275
|
-
|
|
313
|
+
// An absent username is what selects the discoverable flow server-side, so send the key
|
|
314
|
+
// only when there is one — `{ username: undefined }` would serialize it away anyway, but
|
|
315
|
+
// being explicit keeps the two flows visibly distinct.
|
|
316
|
+
{ method: "POST", body: JSON.stringify(username ? { username } : {}) },
|
|
276
317
|
false,
|
|
277
318
|
);
|
|
278
319
|
const publicKey = toRequestOptions(start.options.publicKey);
|
|
279
320
|
const credential = (await navigator.credentials.get({
|
|
280
321
|
publicKey,
|
|
322
|
+
mediation: get?.mediation,
|
|
323
|
+
signal: get?.signal,
|
|
281
324
|
})) as PublicKeyCredential | null;
|
|
282
325
|
if (!credential) throw new Error("Passkey login was cancelled");
|
|
283
326
|
const data = await this.request<TokenResponse>(
|