@omelhorsite/sdk 0.2.0 → 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.
Files changed (44) hide show
  1. package/dist/index.js +4939 -552
  2. package/dist/types/client.d.ts +60 -3
  3. package/dist/types/http.d.ts +444 -19
  4. package/dist/types/index.d.ts +4 -1
  5. package/dist/types/resources/account.d.ts +66 -3
  6. package/dist/types/resources/admin.d.ts +1837 -0
  7. package/dist/types/resources/auth/index.d.ts +39 -0
  8. package/dist/types/resources/auth/passkeys.d.ts +652 -0
  9. package/dist/types/resources/auth/sessions.d.ts +847 -0
  10. package/dist/types/resources/chests.d.ts +54 -3
  11. package/dist/types/resources/content.d.ts +2970 -0
  12. package/dist/types/resources/dynamicQrs.d.ts +39 -3
  13. package/dist/types/resources/forms.d.ts +176 -35
  14. package/dist/types/resources/index.d.ts +19 -8
  15. package/dist/types/resources/ipLookup.d.ts +20 -4
  16. package/dist/types/resources/jobs.d.ts +62 -21
  17. package/dist/types/resources/library.d.ts +1435 -0
  18. package/dist/types/resources/linkTrees.d.ts +142 -30
  19. package/dist/types/resources/media.d.ts +351 -0
  20. package/dist/types/resources/movies.d.ts +1186 -0
  21. package/dist/types/resources/music/artists.d.ts +1066 -0
  22. package/dist/types/resources/music/imports.d.ts +940 -0
  23. package/dist/types/resources/music/index.d.ts +61 -0
  24. package/dist/types/resources/music/playlists.d.ts +1026 -0
  25. package/dist/types/resources/music/social.d.ts +1132 -0
  26. package/dist/types/resources/music/songs.d.ts +1183 -0
  27. package/dist/types/resources/notepads.d.ts +4 -1
  28. package/dist/types/resources/quotas.d.ts +7 -1
  29. package/dist/types/resources/realtime.d.ts +855 -0
  30. package/dist/types/resources/shortLinks.d.ts +45 -4
  31. package/dist/types/resources/social.d.ts +1330 -0
  32. package/dist/types/resources/storage/upload.d.ts +158 -11
  33. package/dist/types/resources/storage.d.ts +88 -22
  34. package/dist/types/resources/tickets.d.ts +82 -3
  35. package/dist/types/resources/tools/backgroundRemoval.d.ts +18 -3
  36. package/dist/types/resources/tools/captions.d.ts +448 -21
  37. package/dist/types/resources/tools/downloader.d.ts +21 -0
  38. package/dist/types/resources/tools/index.d.ts +57 -15
  39. package/dist/types/resources/tools/jumpstyle.d.ts +50 -17
  40. package/dist/types/resources/tools/transcription.d.ts +35 -13
  41. package/dist/types/resources/tools/upscale.d.ts +23 -3
  42. package/dist/types/resources/tools/vocalSeparation.d.ts +30 -13
  43. package/dist/types/types.d.ts +249 -17
  44. package/package.json +2 -1
@@ -0,0 +1,652 @@
1
+ /**
2
+ * The `passkeys` namespace: WebAuthn credentials, and the two ceremonies that
3
+ * create and spend them.
4
+ *
5
+ * Six routes hang off `/webauthn_credentials`, and they split in two:
6
+ *
7
+ * | route | credential | ceiling |
8
+ * | --------------------------- | ---------- | ------------------ |
9
+ * | `GET /` | required | general 600/min |
10
+ * | `DELETE /:id` | required | general 600/min |
11
+ * | `POST /registration_options` | required | general 600/min |
12
+ * | `POST /registration` | required | general 600/min |
13
+ * | `POST /authentication_options` | **none** | **20/min per IP** |
14
+ * | `POST /authentication` | **none** | **20/min per IP** |
15
+ *
16
+ * The last two share ONE bucket, not one each: `rack_attack.rb` throttles on
17
+ * `canonical_path(req).start_with?("/webauthn_credentials/authentication")`,
18
+ * and `/authentication_options` starts with that string. Twenty POSTs a minute
19
+ * from an IP covers both halves of every login attempt, so a NATed office gets
20
+ * ten sign-ins a minute between them. Budget accordingly, and see the note on
21
+ * {@link PasskeysNamespace.authenticate} for why neither of those two methods
22
+ * retries a `429` on its own.
23
+ *
24
+ * ## The payloads go over the wire verbatim, and that is load-bearing
25
+ *
26
+ * A WebAuthn ceremony payload is a nested record of base64url strings that the
27
+ * authenticator signed. Change one byte of it, anywhere, and the signature no
28
+ * longer matches something the server can reconstruct - and the server does not
29
+ * say so in those words. It says `500`, or it says "Passkey could not be
30
+ * verified", two minutes after the user pressed their fingerprint.
31
+ *
32
+ * The SDK's one body-shaped rewrite is the null sentinel, and this namespace is
33
+ * out of its reach BY CONSTRUCTION rather than by opting out: `NULL_SENTINEL`
34
+ * is written by `encodeQuery` in `http.ts` and by nothing else. A JSON body
35
+ * goes through `JSON.stringify` untouched (`ApiClient#buildInit`), and
36
+ * `buildFormData` omits a null field rather than sentinelling it. So the escape
37
+ * hatch is simply this: a ceremony travels in the BODY, never in `query`, and
38
+ * every method here passes the caller's object straight to `http.post` with no
39
+ * copy, no reshaping, and no key filtering. `test/passkeys.test.ts` pins the
40
+ * serialised body byte for byte, including a `null` that must survive as
41
+ * `null`.
42
+ *
43
+ * The corollary is that nothing here normalises for you either. If your
44
+ * platform is loose about base64 padding, run the credential through
45
+ * {@link normalizePasskeyRegistrationCredential} /
46
+ * {@link normalizePasskeyAssertionCredential} BEFORE calling, at the call site,
47
+ * where the change is visible. See {@link passkeyBase64Url} for why React
48
+ * Native needs that and a browser does not.
49
+ *
50
+ * ## What this namespace does not do
51
+ *
52
+ * It never calls `navigator.credentials` and never touches a native passkey
53
+ * module. That is the host's job, in the host's runtime, and it is the half
54
+ * that differs between the three clients:
55
+ *
56
+ * - browser: `@simplewebauthn/browser`'s `startRegistration` /
57
+ * `startAuthentication`, which take `optionsJSON` in the exact shape
58
+ * {@link PasskeyRegistrationOptions} / {@link PasskeyAuthenticationOptions}
59
+ * describe and hand back the exact shape the credential types describe;
60
+ * - React Native: `react-native-passkeys`, loaded lazily because
61
+ * `requireNativeModule` throws at import time when the module is not linked
62
+ * (Expo Go, a stale dev client);
63
+ * - Bun / a Worker: there is no authenticator. `list` and `remove` work,
64
+ * the ceremonies cannot.
65
+ *
66
+ * The SDK owns the transport and the types on both sides of that call.
67
+ */
68
+ import { Resource } from "../../http";
69
+ import type { AccountSession } from "../account";
70
+ import type { BaseRecord, Id, RequestOptions, Timestamp } from "../../types";
71
+ /**
72
+ * One registered passkey, as `WebauthnCredentialBlueprint` renders it.
73
+ *
74
+ * The blueprint declares two fields and inherits three from
75
+ * `ApplicationBlueprint`, so the payload is exactly `id`, `created_at`,
76
+ * `updated_at`, `nickname`, `last_used_at`. Its `:extended` view is an empty
77
+ * block, which in Blueprinter INHERITS the default view rather than replacing
78
+ * it, so `:extended` and the default are the same five fields here: the row
79
+ * `POST /registration` answers with and the rows `GET /` answers with are
80
+ * identical, despite the controller asking for different views.
81
+ *
82
+ * Nothing about the credential itself is ever rendered. `external_id`,
83
+ * `public_key` and `sign_count` stay on the server; there is no field here that
84
+ * identifies the authenticator, so a UI cannot say "your YubiKey" or "this
85
+ * phone" unless the user typed a nickname.
86
+ *
87
+ * `id` is a STRING (`create_table :webauthn_credentials, id: :string`), like
88
+ * users and sessions and unlike songs and playlists. Never do arithmetic on it.
89
+ */
90
+ export interface Passkey extends BaseRecord {
91
+ /**
92
+ * User-chosen label, or `null`.
93
+ *
94
+ * `normalizes :nickname, with: ->(n) { n.to_s.strip.presence }` runs on the
95
+ * model, so `" "` is stored as `null` and comes back as `null`, not as the
96
+ * spaces that were sent. A UI that echoes what it just submitted will show
97
+ * the wrong thing; read the answer instead.
98
+ */
99
+ readonly nickname: string | null;
100
+ /**
101
+ * When this passkey last completed a sign-in, or `null` if it never has.
102
+ *
103
+ * Written by `touch_usage!` through `update_columns`, which skips validations
104
+ * AND callbacks but does bump `updated_at` by hand, so a passkey's
105
+ * `updated_at` tracks its last use rather than its last edit. There is no
106
+ * edit: the record has no update route.
107
+ */
108
+ readonly last_used_at: Timestamp | null;
109
+ }
110
+ /**
111
+ * What `POST /webauthn_credentials/authentication` answers with: the session,
112
+ * plus the token, exactly as `POST /sessions` answers a password sign-in.
113
+ *
114
+ * `SessionBlueprint`'s `:token` view adds one field to the base view, so this
115
+ * is an {@link AccountSession} with a `token`. That token is the new
116
+ * credential, and it is shown ONCE. Persist it here or lose it.
117
+ *
118
+ * A browser gets the same token a second way, as an httpOnly cookie the
119
+ * controller sets alongside the body (`set_session_cookie`). That is why the
120
+ * web client can ignore `token` entirely and still be signed in after the
121
+ * post-login reload: it authenticates by cookie and deliberately never stores a
122
+ * token where a script could read it. A native or CLI client does the opposite
123
+ * and reads `token`.
124
+ */
125
+ export interface PasskeySession extends AccountSession {
126
+ /** The bearer token for the new session. Rendered once, here, and never again. */
127
+ readonly token: string;
128
+ }
129
+ /**
130
+ * An `ArrayBuffer` after the only encoding this API speaks: unpadded base64url.
131
+ *
132
+ * `config.encoding = :base64url` in `config/initializers/webauthn.rb` decides
133
+ * this for BOTH directions. Everything the server sends
134
+ * (`WebAuthn::Encoders::Base64UrlEncoder#encode`) has its `=` padding chomped
135
+ * off, and everything it reads back it decodes the same way.
136
+ *
137
+ * A type alias, not a branded type, because a brand would force every caller to
138
+ * cast the strings their platform just handed them and would buy nothing: the
139
+ * check that matters happens on the server, over bytes. {@link passkeyBase64Url}
140
+ * is the runtime check, for callers who want one.
141
+ */
142
+ export type PasskeyBase64Url = string;
143
+ /** The three user-verification levels WebAuthn defines. */
144
+ export type PasskeyUserVerification = "required" | "preferred" | "discouraged";
145
+ /**
146
+ * How an authenticator is reachable. The registered set is small, but the spec
147
+ * grows it and a platform may report one this SDK has not heard of, so unknown
148
+ * strings are accepted rather than rejected: the server stores transports as
149
+ * opaque strings and never matches on them.
150
+ */
151
+ export type PasskeyTransport = "usb" | "nfc" | "ble" | "smart-card" | "hybrid" | "internal" | (string & {});
152
+ /**
153
+ * One credential the ceremony should exclude (registration) or allow
154
+ * (authentication).
155
+ *
156
+ * The server builds these with `as_public_key_descriptors`, which emits `type`
157
+ * and `id` and NEVER `transports`, so a descriptor that arrives from this API
158
+ * has exactly two keys. `transports` is here for the other direction, where a
159
+ * platform reports it.
160
+ */
161
+ export interface PasskeyCredentialDescriptor {
162
+ readonly type: "public-key";
163
+ /** The credential id, base64url. Matches `webauthn_credentials.external_id`. */
164
+ readonly id: PasskeyBase64Url;
165
+ readonly transports?: readonly PasskeyTransport[];
166
+ }
167
+ /** The relying party, from `WebAuthn.configure`: `"O Melhor Site"` at `omelhorsite.pt`. */
168
+ export interface PasskeyRelyingParty {
169
+ readonly name: string;
170
+ /**
171
+ * The registrable domain that owns the credential. Always sent: the
172
+ * initializer sets `rp_id`, and `RPEntity` falls back to it when the caller
173
+ * gives none.
174
+ *
175
+ * It is the domain of the UI, NOT of the API. Passkeys minted here are scoped
176
+ * to `omelhorsite.pt` and its subdomains; `backend.omelhorsite.pt` serves the
177
+ * ceremony but is not where it runs.
178
+ */
179
+ readonly id: string;
180
+ }
181
+ /**
182
+ * The account the passkey will belong to.
183
+ *
184
+ * `id` is `users.webauthn_id`, a server-minted opaque handle that exists for
185
+ * exactly this purpose, assigned lazily on the first call to
186
+ * {@link PasskeysNamespace.registrationOptions}. It is NOT `users.id`, and the
187
+ * distinction is the point: the value is stored on the authenticator, is
188
+ * readable by anyone who gets hold of the device, and must therefore say
189
+ * nothing about the account. `name` and `displayName`, by contrast, ARE the
190
+ * email and the real name, because the OS shows them in the account picker.
191
+ */
192
+ export interface PasskeyUserEntity {
193
+ readonly id: PasskeyBase64Url;
194
+ /** The account's email address. Shown by the OS. */
195
+ readonly name: string;
196
+ /** The account's display name. Shown by the OS. */
197
+ readonly displayName: string;
198
+ }
199
+ /** One COSE algorithm the relying party will accept, by its registered id. */
200
+ export interface PasskeyCredentialParameter {
201
+ readonly type: "public-key";
202
+ /** COSE identifier: `-7` ES256, `-37` PS256, `-257` RS256, in that order. */
203
+ readonly alg: number;
204
+ }
205
+ /** Constraints on which authenticator may answer, and how. */
206
+ export interface PasskeyAuthenticatorSelection {
207
+ readonly authenticatorAttachment?: "platform" | "cross-platform";
208
+ readonly residentKey?: "required" | "preferred" | "discouraged";
209
+ readonly requireResidentKey?: boolean;
210
+ readonly userVerification?: PasskeyUserVerification;
211
+ }
212
+ /**
213
+ * `POST /webauthn_credentials/registration_options` - the arguments for
214
+ * `navigator.credentials.create()`, camelCased and base64url-encoded by the
215
+ * gem's `JSONSerializer`.
216
+ *
217
+ * Every optional field below is optional because `JSONSerializer#to_hash` drops
218
+ * a falsy attribute, not because this deployment sometimes omits it. What this
219
+ * deployment actually sends, verified against
220
+ * `WebAuthn::PublicKeyCredential::CreationOptions`:
221
+ *
222
+ * - `challenge`, `timeout` (120000), `extensions` (`{}`), `rp`, `user`,
223
+ * `pubKeyCredParams` (ES256, PS256, RS256), `authenticatorSelection`
224
+ * (`residentKey` and `userVerification`, both `"preferred"`), and
225
+ * `excludeCredentials`, which is `[]` for an account with no passkeys yet
226
+ * rather than absent;
227
+ * - NOT `attestation`. The controller passes none, so the key is dropped
228
+ * entirely and the platform applies its own default (`"none"`).
229
+ *
230
+ * Pass this object to the platform whole. Do not rebuild it field by field
231
+ * unless your platform needs you to: the app does exactly that, and its reason
232
+ * is a native Record decoder that trips on `extensions: {}`, which is a
233
+ * property of `react-native-passkeys` rather than of WebAuthn.
234
+ */
235
+ export interface PasskeyRegistrationOptions {
236
+ readonly challenge: PasskeyBase64Url;
237
+ /** Milliseconds the platform may keep its sheet open. 120000. See the TTL warning on {@link PasskeysNamespace.registrationOptions}. */
238
+ readonly timeout?: number;
239
+ readonly extensions?: Record<string, unknown>;
240
+ readonly rp: PasskeyRelyingParty;
241
+ readonly user: PasskeyUserEntity;
242
+ readonly pubKeyCredParams: readonly PasskeyCredentialParameter[];
243
+ readonly attestation?: string;
244
+ readonly authenticatorSelection?: PasskeyAuthenticatorSelection;
245
+ /**
246
+ * The passkeys this account already has, so the authenticator refuses to
247
+ * enrol itself twice. Present but empty for a first passkey.
248
+ *
249
+ * This is what makes "register" idempotent from the user's point of view, and
250
+ * it is enforced by the AUTHENTICATOR, not by the server: re-registering an
251
+ * excluded device fails in the OS sheet, so the caller sees a platform error
252
+ * and never an HTTP one.
253
+ */
254
+ readonly excludeCredentials?: readonly PasskeyCredentialDescriptor[];
255
+ }
256
+ /**
257
+ * `options` from `POST /webauthn_credentials/authentication_options` - the
258
+ * arguments for `navigator.credentials.get()`.
259
+ *
260
+ * `allowCredentials` is always present and always `[]`: the controller calls
261
+ * `options_for_get(user_verification: "preferred")` with no allow list, and
262
+ * `RequestOptions#allow_credentials` falls back to `[]` rather than to nil. An
263
+ * empty allow list IS the feature - it makes the ceremony discoverable, so the
264
+ * OS offers whatever passkeys it holds for the domain and the user picks an
265
+ * account. That is why sign-in needs no email field.
266
+ *
267
+ * `rpId` is always present too (`RequestOptions` defaults it from the
268
+ * relying party), which matters because `react-native-passkeys` requires it.
269
+ */
270
+ export interface PasskeyAuthenticationOptions {
271
+ readonly challenge: PasskeyBase64Url;
272
+ readonly timeout?: number;
273
+ readonly extensions?: Record<string, unknown>;
274
+ /** Empty, and deliberately so: an empty allow list means discoverable login. */
275
+ readonly allowCredentials?: readonly PasskeyCredentialDescriptor[];
276
+ /** `"omelhorsite.pt"` in production, `"localhost"` in development. */
277
+ readonly rpId: string;
278
+ readonly userVerification?: PasskeyUserVerification;
279
+ }
280
+ /**
281
+ * The whole answer from `POST /webauthn_credentials/authentication_options`:
282
+ * the ceremony arguments plus the handle that identifies the challenge.
283
+ *
284
+ * The handle exists because the login ceremony has no session to key a
285
+ * challenge against. The server caches the challenge under
286
+ * `webauthn:auth:<handle>` and hands you the handle; you give it back with the
287
+ * assertion. Treat it as a single-use nonce: it is a `SecureRandom.uuid`, it
288
+ * lives two minutes, and {@link PasskeysNamespace.authenticate} spends it.
289
+ */
290
+ export interface PasskeyAuthenticationChallenge {
291
+ readonly handle: string;
292
+ readonly options: PasskeyAuthenticationOptions;
293
+ }
294
+ /**
295
+ * What the authenticator produced during registration, ready to be posted back.
296
+ *
297
+ * `id` and `rawId` are BOTH required, and both must be the same credential id
298
+ * in base64url. This is not belt-and-braces, it is
299
+ * `WebAuthn::PublicKeyCredential#valid_id?`, which decodes each one separately
300
+ * and compares the BYTES. Send them out of step and see the warning on
301
+ * {@link PasskeysNamespace.register}: the mismatch raises a bare `RuntimeError`
302
+ * inside the gem, which is not a `WebAuthn::Error`, so the controller's rescue
303
+ * does not catch it and you get a `500`.
304
+ *
305
+ * The gem reads `clientDataJSON`, `attestationObject` and `transports` from
306
+ * `response` and ignores anything else in it, so a browser's `getPublicKey()` /
307
+ * `publicKey` extras are harmless. They are also pointless bytes on a phone's
308
+ * connection.
309
+ */
310
+ export interface PasskeyRegistrationCredential {
311
+ readonly id: PasskeyBase64Url;
312
+ /** The same id again. Required, non-null: see the type's own note. */
313
+ readonly rawId: PasskeyBase64Url;
314
+ readonly type: "public-key";
315
+ readonly response: {
316
+ readonly clientDataJSON: PasskeyBase64Url;
317
+ readonly attestationObject: PasskeyBase64Url;
318
+ readonly transports?: readonly PasskeyTransport[];
319
+ };
320
+ readonly authenticatorAttachment?: string;
321
+ /** Whatever the client extensions returned. `{}` when there were none. */
322
+ readonly clientExtensionResults?: Record<string, unknown>;
323
+ }
324
+ /**
325
+ * What the authenticator produced during sign-in, ready to be posted back.
326
+ *
327
+ * Same `id` / `rawId` rule as {@link PasskeyRegistrationCredential}, same
328
+ * consequence for getting it wrong.
329
+ *
330
+ * `userHandle` is what a discoverable ceremony returns as the account the user
331
+ * picked, and this server does NOT read it: it looks the credential up by
332
+ * `external_id` instead. Send it anyway if the platform gave you one, but do
333
+ * not synthesise one, and never treat its absence as a failure.
334
+ */
335
+ export interface PasskeyAssertionCredential {
336
+ readonly id: PasskeyBase64Url;
337
+ /** The same id again. Required, non-null: see {@link PasskeyRegistrationCredential}. */
338
+ readonly rawId: PasskeyBase64Url;
339
+ readonly type: "public-key";
340
+ readonly response: {
341
+ readonly clientDataJSON: PasskeyBase64Url;
342
+ readonly authenticatorData: PasskeyBase64Url;
343
+ readonly signature: PasskeyBase64Url;
344
+ readonly userHandle?: PasskeyBase64Url;
345
+ };
346
+ readonly authenticatorAttachment?: string;
347
+ readonly clientExtensionResults?: Record<string, unknown>;
348
+ }
349
+ /** Arguments for {@link PasskeysNamespace.register}. */
350
+ export interface RegisterPasskeyInput {
351
+ /** The attestation the platform just produced. Sent verbatim. */
352
+ readonly credential: PasskeyRegistrationCredential;
353
+ /**
354
+ * Optional label. Blank and whitespace-only strings are stored as `null` by
355
+ * the model's `normalizes`, so there is no point sending `" "` to clear
356
+ * anything - there is nothing to clear, the record cannot be updated.
357
+ */
358
+ readonly nickname?: string;
359
+ }
360
+ /** Arguments for {@link PasskeysNamespace.authenticate}. */
361
+ export interface AuthenticatePasskeyInput {
362
+ /** The assertion the platform just produced. Sent verbatim. */
363
+ readonly credential: PasskeyAssertionCredential;
364
+ /** The `handle` from {@link PasskeysNamespace.authenticationOptions}. Single use. */
365
+ readonly handle: string;
366
+ }
367
+ /**
368
+ * Canonicalises one ceremony field into unpadded base64url.
369
+ *
370
+ * Accepts standard base64 and base64url, padded or not, and always emits
371
+ * unpadded base64url: `+` becomes `-`, `/` becomes `_`, trailing `=` goes away.
372
+ *
373
+ * ## Who needs this
374
+ *
375
+ * A browser does not. `@simplewebauthn/browser` already emits unpadded
376
+ * base64url on both sides, which is why the web frontend posts its credential
377
+ * straight through and has never needed a normaliser.
378
+ *
379
+ * React Native does. Each platform re-encodes in its own native layer on the
380
+ * way out of the OS, and they do not agree with each other about padding; iOS
381
+ * in particular re-pads on the way in and strips on the way out. That would be
382
+ * harmless if the server were lenient, and on most fields it is - the gem's
383
+ * decoder re-pads a short string and accepts either alphabet. It is NOT lenient
384
+ * about one thing: `valid_id?` decodes `id` and `rawId` SEPARATELY and demands
385
+ * identical bytes, so a platform that pads one and not the other loses the
386
+ * ceremony at the very last step, with a 500 and nothing in the message.
387
+ *
388
+ * Isolate-safe: two regexes and string methods, no platform API at all.
389
+ *
390
+ * @param field Name used in the error message. The VALUE is never included:
391
+ * these strings are signed material and a `clientDataJSON` in a log is a
392
+ * record of who signed in from where.
393
+ * @throws {TypeError} when the value is not a string, is empty, is outside both
394
+ * base64 alphabets, or has a length of `1 mod 4`. That last one is not a
395
+ * padding question: no base64 string is ever 1 mod 4 characters long, so such
396
+ * a value is truncated, and re-padding it would produce plausible bytes that
397
+ * fail verification much later.
398
+ */
399
+ export declare function passkeyBase64Url(value: unknown, field: string): PasskeyBase64Url;
400
+ /**
401
+ * Non-throwing predicate for a value that is ALREADY canonical unpadded
402
+ * base64url. Use it to decide whether normalising is needed at all;
403
+ * {@link passkeyBase64Url} is what does the normalising.
404
+ */
405
+ export declare function isPasskeyBase64Url(value: unknown): value is PasskeyBase64Url;
406
+ /**
407
+ * Rebuilds a registration credential in the canonical encoding, keeping only
408
+ * the fields the server reads.
409
+ *
410
+ * Call it at the call site, on what the platform handed you, and pass the
411
+ * result to {@link PasskeysNamespace.register}. It is NOT applied inside
412
+ * `register`, because the promise this namespace makes is that what you pass is
413
+ * what goes on the wire; a silent rewrite of signed material is exactly the
414
+ * thing that must not happen behind a caller's back.
415
+ *
416
+ * Drops `getPublicKey`, `publicKey`, `getPublicKeyAlgorithm` and
417
+ * `getAuthenticatorData` from `response`: `AuthenticatorAttestationResponse`
418
+ * reads `clientDataJSON`, `attestationObject` and `transports` and nothing
419
+ * else, and the dropped fields are large.
420
+ *
421
+ * @throws {TypeError} through {@link passkeyBase64Url} when a required field is
422
+ * missing or not base64.
423
+ */
424
+ export declare function normalizePasskeyRegistrationCredential(raw: unknown): PasskeyRegistrationCredential;
425
+ /**
426
+ * Rebuilds an assertion credential in the canonical encoding. The companion of
427
+ * {@link normalizePasskeyRegistrationCredential}, with the same contract and
428
+ * the same reason for not being applied automatically.
429
+ *
430
+ * `userHandle` is dropped when the platform reported it as `null` or `""`,
431
+ * which several do for a non-discoverable credential. The server does not read
432
+ * it, so an absent one costs nothing, whereas an empty string would have to
433
+ * survive `passkeyBase64Url` and could not.
434
+ *
435
+ * @throws {TypeError} when a required field is missing or not base64.
436
+ */
437
+ export declare function normalizePasskeyAssertionCredential(raw: unknown): PasskeyAssertionCredential;
438
+ /** The `passkeys` namespace, reachable as `oms.passkeys`. */
439
+ export declare class PasskeysNamespace extends Resource {
440
+ /**
441
+ * `GET /webauthn_credentials` - every passkey of the signed-in user, newest
442
+ * first.
443
+ *
444
+ * Returns a plain array, and that is the whole story: this action is
445
+ * hand-written rather than `CrudActions`, so unlike almost every other index
446
+ * in this API it has NO list DSL, NO `modifiers[page]`, NO `search` /
447
+ * `exact_search`, and NO `ETag` / `304`. Sending those parameters is not an
448
+ * error either - the controller never looks at `params`, so
449
+ * `reject_unknown_filter_keys!` is not in the chain and a filter is silently
450
+ * ignored rather than rejected with a `400`. The web frontend does exactly
451
+ * that today: `PasskeyService.list` accepts a `ListFilters` argument and
452
+ * forwards it, and it has never had any effect.
453
+ *
454
+ * Hence no `Paginated` and no `PageParams` here. An account's passkey count
455
+ * is bounded by how many devices a person owns, and the server would return
456
+ * all of them regardless of what was asked.
457
+ *
458
+ * The order is `created_at: :desc`, applied in SQL, so it does not change if
459
+ * a passkey is used.
460
+ *
461
+ * Requires a credential. General authenticated ceiling, 600/min.
462
+ *
463
+ * @throws {OmsAuthError} 401 with no credential.
464
+ */
465
+ list(options?: RequestOptions): Promise<Passkey[]>;
466
+ /**
467
+ * `DELETE /webauthn_credentials/:id` - removes one passkey. `204`, no body.
468
+ *
469
+ * The lookup is scoped to the caller (`Current.user.webauthn_credentials`),
470
+ * so somebody else's id is a `404` and never a `403`: the endpoint does not
471
+ * confirm that the id exists. `destroyable_by?` additionally lets an admin
472
+ * through, but only for a passkey they can already find, which by that
473
+ * scoping means their own.
474
+ *
475
+ * Nothing stops the last passkey being removed. There is no "you would lock
476
+ * yourself out" guard, because passwords and OAuth identities are still
477
+ * there; a caller whose UI presents passkeys as the only sign-in method owns
478
+ * that warning itself.
479
+ *
480
+ * Deleting is one-way and the credential on the device is NOT revoked - the
481
+ * user keeps a dead passkey in their OS keychain that will offer itself at
482
+ * the next sign-in and then fail with "Unknown passkey."
483
+ *
484
+ * Requires a credential. General authenticated ceiling, 600/min.
485
+ *
486
+ * @throws {OmsApiError} 404 `"Passkey not found."` for an unknown id, or for
487
+ * one belonging to someone else.
488
+ */
489
+ remove(id: Id, options?: RequestOptions): Promise<void>;
490
+ /**
491
+ * `POST /webauthn_credentials/registration_options` - starts enrolment and
492
+ * returns the arguments for `navigator.credentials.create()`.
493
+ *
494
+ * Two side effects, both of which matter:
495
+ *
496
+ * 1. **It assigns `users.webauthn_id` on first use.** An account that has
497
+ * never touched passkeys gets one minted here (`WebAuthn.generate_user_id`,
498
+ * written with `update_column`, so no callbacks and no `updated_at` bump).
499
+ * That handle is then permanent and is what every passkey on this account
500
+ * is bound to.
501
+ * 2. **It caches a challenge under a key that is per USER, not per call**
502
+ * (`webauthn:reg:<user id>`). Calling this twice for one account
503
+ * OVERWRITES the first challenge, so two registrations in flight at once
504
+ * means the older one fails verification with "Registration challenge
505
+ * expired" even though nothing expired. Do not call it speculatively, do
506
+ * not call it to warm a screen, and do not let a button fire it twice.
507
+ *
508
+ * The challenge lives **two minutes** (`CHALLENGE_TTL`), and the `timeout`
509
+ * inside the options is also 120000. Those are the same number, and the cache
510
+ * clock starts BEFORE the response is even sent, so a user who lets the OS
511
+ * sheet sit open for its full advertised timeout arrives after the challenge
512
+ * has gone. Treat 2 minutes as the budget for the whole round trip.
513
+ *
514
+ * Requires a credential. General authenticated ceiling, 600/min.
515
+ *
516
+ * An empty JSON object is sent as the body. The controller reads nothing from
517
+ * it; it is there so the request carries `Content-Type: application/json`
518
+ * like every other POST in this API rather than arriving bodyless.
519
+ */
520
+ registrationOptions(options?: RequestOptions): Promise<PasskeyRegistrationOptions>;
521
+ /**
522
+ * `POST /webauthn_credentials/registration` - finishes enrolment. `201` with
523
+ * the new {@link Passkey}.
524
+ *
525
+ * `input.credential` is posted VERBATIM. Nothing here copies it, reshapes it,
526
+ * filters its keys or rewrites a `null` inside it, because the server reads
527
+ * it with `params.require(:credential).to_unsafe_h` and hands the raw hash to
528
+ * the gem, which then checks a signature over those exact bytes.
529
+ *
530
+ * ## The failure modes, and which of them are honest
531
+ *
532
+ * The controller rescues `WebAuthn::Error` and turns it into a `400`. Some of
533
+ * what can go wrong here is NOT a `WebAuthn::Error`, and those reach you as a
534
+ * `500`:
535
+ *
536
+ * - `type` that is not exactly `"public-key"`, or an `id` and `rawId` whose
537
+ * decoded bytes differ: `PublicKeyCredential#verify` calls bare
538
+ * `raise("invalid type")` / `raise("invalid id")`, which are `RuntimeError`
539
+ * and slip past the rescue;
540
+ * - `rawId` absent or `null`: the decoder calls `end_with?` on it and raises
541
+ * `NoMethodError`. See {@link normalizePasskeyRegistrationCredential},
542
+ * which fills `rawId` from `id` for the Android case where the platform
543
+ * really does send `null`;
544
+ * - `credential` absent altogether: `params.require` raises
545
+ * `ParameterMissing`, which Rails renders as `400`.
546
+ *
547
+ * So a `500` from this endpoint is a malformed payload, not an outage, and it
548
+ * is the single most likely thing to be wrong on a client that hand-builds
549
+ * the credential. A genuine verification failure - wrong challenge, wrong
550
+ * origin, bad attestation - is the `400` `"Passkey registration could not be
551
+ * verified."`.
552
+ *
553
+ * The default retry policy applies, which for a POST means only a `429` is
554
+ * replayed. That is safe here: the challenge is only deleted after a
555
+ * successful save, so a rate-limited attempt leaves the ceremony intact.
556
+ *
557
+ * Requires a credential. General authenticated ceiling, 600/min - this route
558
+ * is NOT in the 20/min webauthn bucket, which covers `authentication*` only.
559
+ *
560
+ * @throws {OmsAuthError} 401 `"Registration challenge expired. Please try
561
+ * again."` when more than two minutes passed since
562
+ * {@link registrationOptions}, or when a second call to it overwrote this
563
+ * ceremony's challenge.
564
+ * @throws {OmsApiError} 400 when verification fails, or when the row will not
565
+ * save - which in practice means `external_id` is already taken, i.e. this
566
+ * authenticator is already registered and `excludeCredentials` did not stop
567
+ * it.
568
+ */
569
+ register(input: RegisterPasskeyInput, options?: RequestOptions): Promise<Passkey>;
570
+ /**
571
+ * `POST /webauthn_credentials/authentication_options` - starts a sign-in and
572
+ * returns the challenge handle plus the arguments for
573
+ * `navigator.credentials.get()`.
574
+ *
575
+ * **Send this with NO credential.** The route is
576
+ * `allow_unauthenticated_access`, and the whole point is that there is no
577
+ * session yet. The transport always attaches `Authorization` when the client
578
+ * holds a token, and the caller cannot strip it per request, so a client that
579
+ * might be signed in should build an anonymous one for the login flow.
580
+ *
581
+ * The ceremony is discoverable: `allowCredentials` comes back empty, the OS
582
+ * shows every passkey it holds for the domain, and the user picks the
583
+ * account. There is no email step, and asking for one would not help.
584
+ *
585
+ * Each call mints a fresh `handle` and caches a challenge under it for two
586
+ * minutes, so unlike {@link registrationOptions} concurrent calls do not
587
+ * fight: they are independent ceremonies. They do share the rate limit.
588
+ *
589
+ * **20 requests per minute per IP, shared with {@link authenticate}.**
590
+ *
591
+ * Retrying is off here, which deviates from the transport's default of
592
+ * replaying a `429`. The reason is the clock rather than safety: the replay
593
+ * sleeps out `Retry-After`, which Rack::Attack sets from a one-minute window,
594
+ * and then hands back a challenge that lives two minutes and still has an OS
595
+ * sheet and a second rate-limited request ahead of it. Waiting silently
596
+ * inside the SDK is likelier to produce a login that dies at the last step
597
+ * than one that succeeds. Surfacing the `429` lets the caller back off
598
+ * visibly and start a fresh ceremony. Pass `retry: {}` to opt back in.
599
+ *
600
+ * @throws {OmsQuotaError} 429 when the shared per-IP bucket is spent.
601
+ */
602
+ authenticationOptions(options?: RequestOptions): Promise<PasskeyAuthenticationChallenge>;
603
+ /**
604
+ * `POST /webauthn_credentials/authentication` - finishes a sign-in. `201`
605
+ * with a {@link PasskeySession} carrying the new token.
606
+ *
607
+ * **Send this with NO credential** and read `token` off the answer; that
608
+ * token is the credential from here on. A browser additionally receives the
609
+ * same token as an httpOnly cookie and can ignore the body field entirely.
610
+ *
611
+ * `input.credential` is posted VERBATIM, for the same reason and with the
612
+ * same care as {@link register}.
613
+ *
614
+ * ## An assertion is spent exactly once, so nothing here is replayed
615
+ *
616
+ * The controller deletes the cached challenge BEFORE it verifies anything.
617
+ * Once a request has reached the controller the handle is gone whatever the
618
+ * outcome, so a second attempt carrying the same body answers `401 "Login
619
+ * challenge expired."` and reports the wrong cause for whatever actually
620
+ * went wrong.
621
+ *
622
+ * The transport's default policy already declines to replay a `POST` after a
623
+ * torn connection or a `5xx`, which is exactly right for that reason. The one
624
+ * outcome it WOULD replay is a `429`, and this method turns that off as well.
625
+ * A `429` comes from Rack::Attack, ahead of the router, so it genuinely did
626
+ * not spend the handle; but `Retry-After` is set from a one-minute window,
627
+ * the challenge lives two minutes, and the OS sheet has already eaten part of
628
+ * that. Sleeping through the rate limit inside the SDK converts it into an
629
+ * expired challenge, silently, and reports the failure at the wrong endpoint.
630
+ * Surface it instead and start a new ceremony from
631
+ * {@link authenticationOptions}.
632
+ *
633
+ * A `500` here means the same malformed-payload family described on
634
+ * {@link register}: a missing `rawId`, a wrong `type`, an `id` that does not
635
+ * decode to the same bytes as `rawId`.
636
+ *
637
+ * **20 requests per minute per IP, shared with
638
+ * {@link authenticationOptions}.** Every sign-in attempt costs two.
639
+ *
640
+ * @throws {OmsAuthError} 401 for all five of: an expired or already-spent
641
+ * handle (`"Login challenge expired. Please try again."`); a credential the
642
+ * server has never seen (`"Unknown passkey."`); a deactivated account
643
+ * (`"This account is deactivated."`); a failed signature, wrong origin or
644
+ * wrong challenge (`"Passkey could not be verified."`); and a sign counter
645
+ * that did not advance, which the gem treats as a cloned authenticator and
646
+ * which reports as that same message. Note that passkeys synced through a
647
+ * keychain report a counter of `0` forever, and `valid_sign_count?` lets
648
+ * `0` against `0` through, so that last case does not fire for them.
649
+ * @throws {OmsQuotaError} 429 when the shared per-IP bucket is spent.
650
+ */
651
+ authenticate(input: AuthenticatePasskeyInput, options?: RequestOptions): Promise<PasskeySession>;
652
+ }