@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.
- package/dist/index.js +4939 -552
- package/dist/types/client.d.ts +60 -3
- package/dist/types/http.d.ts +444 -19
- package/dist/types/index.d.ts +4 -1
- package/dist/types/resources/account.d.ts +66 -3
- package/dist/types/resources/admin.d.ts +1837 -0
- package/dist/types/resources/auth/index.d.ts +39 -0
- package/dist/types/resources/auth/passkeys.d.ts +652 -0
- package/dist/types/resources/auth/sessions.d.ts +847 -0
- package/dist/types/resources/chests.d.ts +54 -3
- package/dist/types/resources/content.d.ts +2970 -0
- package/dist/types/resources/dynamicQrs.d.ts +39 -3
- package/dist/types/resources/forms.d.ts +176 -35
- package/dist/types/resources/index.d.ts +19 -8
- package/dist/types/resources/ipLookup.d.ts +20 -4
- package/dist/types/resources/jobs.d.ts +62 -21
- package/dist/types/resources/library.d.ts +1435 -0
- package/dist/types/resources/linkTrees.d.ts +142 -30
- package/dist/types/resources/media.d.ts +351 -0
- package/dist/types/resources/movies.d.ts +1186 -0
- package/dist/types/resources/music/artists.d.ts +1066 -0
- package/dist/types/resources/music/imports.d.ts +940 -0
- package/dist/types/resources/music/index.d.ts +61 -0
- package/dist/types/resources/music/playlists.d.ts +1026 -0
- package/dist/types/resources/music/social.d.ts +1132 -0
- package/dist/types/resources/music/songs.d.ts +1183 -0
- package/dist/types/resources/notepads.d.ts +4 -1
- package/dist/types/resources/quotas.d.ts +7 -1
- package/dist/types/resources/realtime.d.ts +855 -0
- package/dist/types/resources/shortLinks.d.ts +45 -4
- package/dist/types/resources/social.d.ts +1330 -0
- package/dist/types/resources/storage/upload.d.ts +158 -11
- package/dist/types/resources/storage.d.ts +88 -22
- package/dist/types/resources/tickets.d.ts +82 -3
- package/dist/types/resources/tools/backgroundRemoval.d.ts +18 -3
- package/dist/types/resources/tools/captions.d.ts +448 -21
- package/dist/types/resources/tools/downloader.d.ts +21 -0
- package/dist/types/resources/tools/index.d.ts +57 -15
- package/dist/types/resources/tools/jumpstyle.d.ts +50 -17
- package/dist/types/resources/tools/transcription.d.ts +35 -13
- package/dist/types/resources/tools/upscale.d.ts +23 -3
- package/dist/types/resources/tools/vocalSeparation.d.ts +30 -13
- package/dist/types/types.d.ts +249 -17
- package/package.json +2 -1
|
@@ -0,0 +1,847 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `sessions` namespace: establishing a credential, ending it, and the
|
|
3
|
+
* two-step email flows that create an account, reset a password, move an
|
|
4
|
+
* address or delete the account.
|
|
5
|
+
*
|
|
6
|
+
* This is the module the rest of the SDK stands on. Every other namespace
|
|
7
|
+
* assumes a credential already exists; this one is where it comes from.
|
|
8
|
+
*
|
|
9
|
+
* ## What a credential actually is
|
|
10
|
+
*
|
|
11
|
+
* A `Session` row, and nothing more. `POST /sessions` writes one and hands back
|
|
12
|
+
* its `token`, which is a `SecureRandom.uuid`. That UUID IS the credential.
|
|
13
|
+
*
|
|
14
|
+
* There is no JWT, no signature to verify, no `exp`, no refresh token and no
|
|
15
|
+
* rotation. A session token never expires on its own: it lives until the row is
|
|
16
|
+
* deleted, which happens on sign-out, when an administrator deactivates the
|
|
17
|
+
* account (`User#deactivate!` runs `sessions.delete_all`), or when someone
|
|
18
|
+
* deletes it in the database. Do not build refresh logic against this - there
|
|
19
|
+
* is nothing to refresh, and a client that "renews" by signing in again just
|
|
20
|
+
* accumulates rows in the user's device list and fires a login alert each time.
|
|
21
|
+
*
|
|
22
|
+
* The OAuth 2 / OIDC tokens under `oms.auth` are a DIFFERENT credential with a
|
|
23
|
+
* different lifecycle (they do expire, they do refresh, they carry scopes).
|
|
24
|
+
* Both are accepted by the API. This module is only about the session kind.
|
|
25
|
+
*
|
|
26
|
+
* ## Three ways the server reads it, and the one that bites
|
|
27
|
+
*
|
|
28
|
+
* `Session.candidate_tokens` collects, in this order:
|
|
29
|
+
*
|
|
30
|
+
* 1. the `Authorization` header,
|
|
31
|
+
* 2. the `token` request parameter (query string or body),
|
|
32
|
+
* 3. the `oms_session` cookie.
|
|
33
|
+
*
|
|
34
|
+
* and `Session.resolve_from_request` tries each until one resolves to a LIVE
|
|
35
|
+
* row, so a stale header no longer permanently shadows a good cookie on API
|
|
36
|
+
* requests. (`ApplicationCable::Connection` is the exception: the WebSocket
|
|
37
|
+
* handshake takes the FIRST candidate, not the first live one, so a stale
|
|
38
|
+
* header there really does beat a good `?token=`.)
|
|
39
|
+
*
|
|
40
|
+
* The header is parsed as `header["Bearer:".length..]`, which is a BLIND SLICE
|
|
41
|
+
* OF THE FIRST SEVEN CHARACTERS. Nothing checks that those seven characters
|
|
42
|
+
* spell anything:
|
|
43
|
+
*
|
|
44
|
+
* - `Bearer <token>` works (7 chars: `Bearer` plus the space),
|
|
45
|
+
* - `Bearer:<token>` also works, which is where the constant's name comes from,
|
|
46
|
+
* - a bare token with NO prefix does NOT work. Its first seven characters are
|
|
47
|
+
* eaten, the remainder matches no row, and the request is answered as
|
|
48
|
+
* anonymous. The failure is a 401 on an endpoint that needs auth, or - far
|
|
49
|
+
* worse - a silently empty list on one that does not, with no hint anywhere
|
|
50
|
+
* that a credential was sent and mangled.
|
|
51
|
+
*
|
|
52
|
+
* The SDK's transport always writes `Bearer ${token}`, so this only matters if
|
|
53
|
+
* you build the header yourself, or if you store a token that already carries a
|
|
54
|
+
* prefix and then let the transport add a second one.
|
|
55
|
+
*
|
|
56
|
+
* ## Cookie mode and token mode
|
|
57
|
+
*
|
|
58
|
+
* Sign-in works in BOTH, and the difference is only in what you do with the
|
|
59
|
+
* answer. See {@link AuthSessionsNamespace.signIn}.
|
|
60
|
+
*
|
|
61
|
+
* The cookie is named `oms_session` ({@link SESSION_COOKIE_NAME}) and is set by
|
|
62
|
+
* every session-minting endpoint with `httpOnly`, `secure` in production,
|
|
63
|
+
* `SameSite=Lax`, `path=/`, a one-year expiry and NO `Domain` attribute, which
|
|
64
|
+
* makes it host-only: it belongs to `backend.omelhorsite.pt` alone and is never
|
|
65
|
+
* sent to a sibling subdomain. `omelhorsite.pt` and `backend.omelhorsite.pt`
|
|
66
|
+
* share a registrable domain, so a call from the web app is cross-ORIGIN but
|
|
67
|
+
* same-SITE, and `SameSite=Lax` still lets the cookie ride along. A page on a
|
|
68
|
+
* genuinely different site (the `pages.dev` staging build, where `pages.dev` is
|
|
69
|
+
* a public suffix) can never receive it, no CORS header can change that, and
|
|
70
|
+
* such a page must use token mode instead.
|
|
71
|
+
*
|
|
72
|
+
* ## No CSRF token exists
|
|
73
|
+
*
|
|
74
|
+
* The backend is an `ActionController::API` and `protect_from_forgery` is never
|
|
75
|
+
* enabled. There is no CSRF token to fetch, no header to echo, and nothing in
|
|
76
|
+
* this SDK that omits one. For browsers the entire cross-site defence is
|
|
77
|
+
* `SameSite=Lax` on the cookie; bearer clients are unaffected because a
|
|
78
|
+
* cross-site page cannot make the browser attach an `Authorization` header.
|
|
79
|
+
*
|
|
80
|
+
* ## What lives elsewhere
|
|
81
|
+
*
|
|
82
|
+
* Deliberately NOT re-implemented here, because `oms.account` already owns it
|
|
83
|
+
* and two implementations of one endpoint drift:
|
|
84
|
+
*
|
|
85
|
+
* - `oms.account.sessions.list()` / `.update()` - the device-management screen,
|
|
86
|
+
* `GET /sessions` and `PATCH /sessions/:id`;
|
|
87
|
+
* - `oms.account.me()`, `.get()`, `.byHandle()`, `.profile()`, `.search()`,
|
|
88
|
+
* `.update()`, `.follow()`, `.picture()` - the user read and write surface.
|
|
89
|
+
*
|
|
90
|
+
* `DELETE /users/:id` is also absent, and that one is not a delegation: the
|
|
91
|
+
* route cannot succeed for anybody. `UsersController#destroy` checks for an
|
|
92
|
+
* administrator and then calls `super`, and `CrudActions#destroy` asks
|
|
93
|
+
* `resource.destroyable_by?(Current.user)` - which on `User` is
|
|
94
|
+
* `alias destroyable_by? creatable_by?`, and `creatable_by?` returns `false`
|
|
95
|
+
* unconditionally. Every caller, administrator included, gets
|
|
96
|
+
* `401 "You are not authorized to destroy this resource"`. Use
|
|
97
|
+
* {@link AuthSessionsNamespace.deactivateUser} for the operational need, or
|
|
98
|
+
* {@link AuthSessionsNamespace.deleteAccountStart} for a user deleting
|
|
99
|
+
* themselves, which is a different code path and does work.
|
|
100
|
+
*/
|
|
101
|
+
import { Resource } from "../../http";
|
|
102
|
+
import type { AccountSession, User } from "../account";
|
|
103
|
+
import type { Id, Paginated, PageParams, RequestOptions } from "../../types";
|
|
104
|
+
/**
|
|
105
|
+
* Name of the httpOnly cookie the backend sets on every session-minting
|
|
106
|
+
* response.
|
|
107
|
+
*
|
|
108
|
+
* Exported for hosts that need to recognise it (a proxy forwarding it, a native
|
|
109
|
+
* client emptying its cookie jar), NOT for reading it: it is `httpOnly`, so
|
|
110
|
+
* `document.cookie` never contains it and no amount of trying will change that.
|
|
111
|
+
* That is the entire point of cookie mode.
|
|
112
|
+
*/
|
|
113
|
+
export declare const SESSION_COOKIE_NAME = "oms_session";
|
|
114
|
+
/**
|
|
115
|
+
* The seven characters the server slices off the `Authorization` header before
|
|
116
|
+
* looking the token up, spelled the way the Rails source spells them
|
|
117
|
+
* (`"Bearer:".length`).
|
|
118
|
+
*
|
|
119
|
+
* Present so the number 7 appears somewhere other than a comment. The transport
|
|
120
|
+
* writes `"Bearer "` (with a space), which is the same length; both forms work
|
|
121
|
+
* and a token with no prefix at all does not. See the module note.
|
|
122
|
+
*/
|
|
123
|
+
export declare const SESSION_BEARER_PREFIX_LENGTH = 7;
|
|
124
|
+
/**
|
|
125
|
+
* Digits in an email verification code (`EmailVerification::CODE_LENGTH`).
|
|
126
|
+
*
|
|
127
|
+
* Six, numeric only, zero-padded. This is a deliberate product decision on this
|
|
128
|
+
* project rather than an accident: a code a person can read off a phone and
|
|
129
|
+
* type on a numeric keypad, made safe by a hard attempt budget rather than by
|
|
130
|
+
* length. See {@link VERIFICATION_CODE_MAX_ATTEMPTS}.
|
|
131
|
+
*/
|
|
132
|
+
export declare const VERIFICATION_CODE_LENGTH = 6;
|
|
133
|
+
/**
|
|
134
|
+
* Wrong guesses an issued code survives (`EmailVerification::MAX_ATTEMPTS`).
|
|
135
|
+
*
|
|
136
|
+
* This is the counterweight to a six-digit code, and it is per CODE, not per IP:
|
|
137
|
+
* rack-attack only throttles by address, so an attacker rotating through a
|
|
138
|
+
* botnet would otherwise walk a million-key space at 10 guesses a minute per
|
|
139
|
+
* address. The budget closes that regardless of where the guesses come from.
|
|
140
|
+
*
|
|
141
|
+
* The exact arithmetic, because off-by-one matters when you are deciding
|
|
142
|
+
* whether to let a user try again: `register_failed_attempt` destroys the code
|
|
143
|
+
* when `attempts + 1 >= MAX_ATTEMPTS`, starting from `attempts = 0`. So four
|
|
144
|
+
* wrong guesses are survivable and the FIFTH burns the code. Burning is
|
|
145
|
+
* permanent - the row is deleted, not locked - and it also fires a
|
|
146
|
+
* `verification_burned` security alert to the owner. The user's only route
|
|
147
|
+
* forward is a fresh `*_start` call, which is throttled at 4 a minute and 20 an
|
|
148
|
+
* hour per IP, so a client that lets someone mash a code field will lock them
|
|
149
|
+
* out of the flow for the rest of the hour.
|
|
150
|
+
*
|
|
151
|
+
* Validate the shape locally with {@link isVerificationCode} before spending an
|
|
152
|
+
* attempt on something that cannot possibly be right.
|
|
153
|
+
*/
|
|
154
|
+
export declare const VERIFICATION_CODE_MAX_ATTEMPTS = 5;
|
|
155
|
+
/**
|
|
156
|
+
* How long an issued code stays valid (`EmailVerification::EXPIRES_IN`), in
|
|
157
|
+
* milliseconds. Fifteen minutes.
|
|
158
|
+
*
|
|
159
|
+
* Expiry is enforced by an `active` scope plus an opportunistic purge on every
|
|
160
|
+
* issue and every verify, so an expired code behaves exactly like a wrong one:
|
|
161
|
+
* `404 "Invalid Verification"`, indistinguishable from the status alone. Show
|
|
162
|
+
* the user a countdown rather than making them find out.
|
|
163
|
+
*/
|
|
164
|
+
export declare const VERIFICATION_CODE_TTL_MS: number;
|
|
165
|
+
/**
|
|
166
|
+
* How long an OAuth handoff ticket stays valid (`SessionsController::TICKET_TTL`),
|
|
167
|
+
* in milliseconds. Two minutes.
|
|
168
|
+
*
|
|
169
|
+
* The signature window is only half the story - the ticket is also one-time.
|
|
170
|
+
* See {@link AuthSessionsNamespace.adopt}.
|
|
171
|
+
*/
|
|
172
|
+
export declare const OAUTH_TICKET_TTL_MS: number;
|
|
173
|
+
/**
|
|
174
|
+
* Every value the `device_type` column accepts, as the server's own enum
|
|
175
|
+
* spells them.
|
|
176
|
+
*
|
|
177
|
+
* The server picks one by parsing the `User-Agent` of the sign-in request, and
|
|
178
|
+
* the user can rename it afterwards through `oms.account.sessions.update()`.
|
|
179
|
+
* The list is mostly a joke, with one entry that is not:
|
|
180
|
+
*
|
|
181
|
+
* **`"teapot"` suppresses alerts.** `Session#alert_login` and
|
|
182
|
+
* `#alert_returning_activity` both bail out for a teapot session, which is how
|
|
183
|
+
* the backend's own background jobs sign in without paging the owner on every
|
|
184
|
+
* run. Do not relabel a real user's device as a teapot to quieten notifications:
|
|
185
|
+
* you are turning off the only signal that a stolen token is being used.
|
|
186
|
+
*/
|
|
187
|
+
export declare const SESSION_DEVICE_TYPES: readonly ["tablet", "console", "fridge", "teapot", "toaster", "air_conditioner", "car", "blender", "vacuum_cleaner", "washing_machine", "lawn_mower", "microwave", "hair_dryer", "electric_toothbrush", "desktop", "laptop", "television", "mobile", "space_ship", "time_machine", "hoverboard", "teleporter", "magic_carpet", "unicorn", "flying_broom", "submarine", "hot_air_balloon", "keychain", "alarm_clock", "radio", "record_player", "other"];
|
|
188
|
+
/** One of {@link SESSION_DEVICE_TYPES}. */
|
|
189
|
+
export type SessionDeviceType = (typeof SESSION_DEVICE_TYPES)[number];
|
|
190
|
+
/**
|
|
191
|
+
* True when `value` has the shape of an email verification code: exactly
|
|
192
|
+
* {@link VERIFICATION_CODE_LENGTH} ASCII digits.
|
|
193
|
+
*
|
|
194
|
+
* Purely local, and worth calling before every `*Complete` method. A malformed
|
|
195
|
+
* code cannot match anything, but sending it still costs one of the five
|
|
196
|
+
* guesses the live code has ({@link VERIFICATION_CODE_MAX_ATTEMPTS}) and one of
|
|
197
|
+
* the ten requests a minute the IP is allowed. A user who pastes a code with a
|
|
198
|
+
* trailing space should not lose a fifth of their budget to whitespace.
|
|
199
|
+
*
|
|
200
|
+
* The server normalises with `strip`, so surrounding whitespace is forgiven
|
|
201
|
+
* there; this returns `false` for it anyway, so a caller can trim before
|
|
202
|
+
* sending rather than relying on the remote side to be lenient.
|
|
203
|
+
*/
|
|
204
|
+
export declare function isVerificationCode(value: string): boolean;
|
|
205
|
+
/** Credentials for {@link AuthSessionsNamespace.signIn}. */
|
|
206
|
+
export interface SignInInput {
|
|
207
|
+
/** Normalised server-side with `strip.downcase`; send it as the user typed it. */
|
|
208
|
+
readonly email: string;
|
|
209
|
+
/**
|
|
210
|
+
* Compared with `User.authenticate_by`, which is timing-safe: a wrong
|
|
211
|
+
* password and an unknown address take the same time and give the same
|
|
212
|
+
* message, so this endpoint cannot be used to test whether an account exists.
|
|
213
|
+
*/
|
|
214
|
+
readonly password: string;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* What `POST /sessions` answers with: the session record plus, ONCE, the token.
|
|
218
|
+
*
|
|
219
|
+
* This is `SessionBlueprint`'s `:token` view, which is the base view plus one
|
|
220
|
+
* field - Blueprinter views inherit, they do not replace - so everything an
|
|
221
|
+
* ordinary {@link AccountSession} carries is here too, including the inlined
|
|
222
|
+
* `user`. That inline user saves a round trip: there is no need to call
|
|
223
|
+
* `oms.account.me()` straight after signing in.
|
|
224
|
+
*
|
|
225
|
+
* `token` appears in this response and in NO other. Nothing else in the API
|
|
226
|
+
* ever renders it again: `GET /sessions` and `GET /sessions/mine` return the
|
|
227
|
+
* base view, which has no `token` field. Lose it and the only way back is to
|
|
228
|
+
* sign in again, minting another row.
|
|
229
|
+
*/
|
|
230
|
+
export interface SignedInSession extends AccountSession {
|
|
231
|
+
/**
|
|
232
|
+
* The credential. A bare UUID, no prefix.
|
|
233
|
+
*
|
|
234
|
+
* Store it where the platform stores secrets (Keychain / Keystore via
|
|
235
|
+
* SecureStore on React Native, the OS keyring for the CLI). Do NOT store it
|
|
236
|
+
* when you are in cookie mode - see {@link AuthSessionsNamespace.signIn}.
|
|
237
|
+
*/
|
|
238
|
+
readonly token: string;
|
|
239
|
+
/** The signed-in user, rendered inline. Always present on this view. */
|
|
240
|
+
readonly user: User;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* What `POST /sessions/adopt` answers with. Two fields short of a session: the
|
|
244
|
+
* endpoint returns only the token, not the record.
|
|
245
|
+
*/
|
|
246
|
+
export interface AdoptedSession {
|
|
247
|
+
/** The credential, same kind of value as {@link SignedInSession.token}. */
|
|
248
|
+
readonly token: string;
|
|
249
|
+
}
|
|
250
|
+
/** What `GET /sessions/oauth_ticket` answers with. */
|
|
251
|
+
export interface SessionOAuthTicket {
|
|
252
|
+
/**
|
|
253
|
+
* A signed id for the current session, scoped to the `oauth` purpose and good
|
|
254
|
+
* for {@link OAUTH_TICKET_TTL_MS}. It is NOT a session token and cannot
|
|
255
|
+
* authenticate an API call; the only thing that accepts it is the OAuth link
|
|
256
|
+
* flow, which trades it back for a session through
|
|
257
|
+
* {@link AuthSessionsNamespace.adopt}.
|
|
258
|
+
*/
|
|
259
|
+
readonly ticket: string;
|
|
260
|
+
}
|
|
261
|
+
/** Arguments for {@link AuthSessionsNamespace.signUpComplete}. */
|
|
262
|
+
export interface SignUpInput {
|
|
263
|
+
/** The address the code was sent to. Must match `signUpStart` exactly. */
|
|
264
|
+
readonly email: string;
|
|
265
|
+
/** The six digits from the email. */
|
|
266
|
+
readonly code: string;
|
|
267
|
+
/**
|
|
268
|
+
* Display name, 1 to 50 characters. The `handle` is NOT settable here: the
|
|
269
|
+
* server generates one from this name in a `before_create` hook. Change it
|
|
270
|
+
* afterwards with `oms.account.update({ handle })`.
|
|
271
|
+
*/
|
|
272
|
+
readonly name: string;
|
|
273
|
+
/** The password. The backend enforces no minimum beyond presence. */
|
|
274
|
+
readonly password: string;
|
|
275
|
+
}
|
|
276
|
+
/** Arguments for {@link AuthSessionsNamespace.resetPasswordComplete}. */
|
|
277
|
+
export interface ResetPasswordInput {
|
|
278
|
+
/** The address the code was sent to. */
|
|
279
|
+
readonly email: string;
|
|
280
|
+
/** The six digits from the email. */
|
|
281
|
+
readonly code: string;
|
|
282
|
+
/** The new password. This is the only field the call changes. */
|
|
283
|
+
readonly password: string;
|
|
284
|
+
}
|
|
285
|
+
/** Arguments for {@link AuthSessionsNamespace.changeEmailComplete}. */
|
|
286
|
+
export interface ChangeEmailInput {
|
|
287
|
+
/** The NEW address, identical to the one passed to `changeEmailStart`. */
|
|
288
|
+
readonly email: string;
|
|
289
|
+
/** The six digits sent to the address currently on the account. */
|
|
290
|
+
readonly prevEmailCode: string;
|
|
291
|
+
/** The six digits sent to the new address. */
|
|
292
|
+
readonly newEmailCode: string;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Filters for {@link AuthSessionsNamespace.listUsers}.
|
|
296
|
+
*
|
|
297
|
+
* `name` and `handle` are the ONLY filterable columns (`search_params :name,
|
|
298
|
+
* :handle` on the controller). Any other key is rejected with `400 "Unknown
|
|
299
|
+
* search filter"`, not ignored - this list DSL fails closed.
|
|
300
|
+
*/
|
|
301
|
+
export interface ListUsersParams extends PageParams {
|
|
302
|
+
/** Substring match on the display name, accent-folded and case-insensitive. */
|
|
303
|
+
readonly name?: string;
|
|
304
|
+
/** Substring match on the handle, accent-folded and case-insensitive. */
|
|
305
|
+
readonly handle?: string;
|
|
306
|
+
/** Exact handle. Cheaper and less surprising than `handle` for a lookup. */
|
|
307
|
+
readonly exactHandle?: string;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* The `sessions` namespace, reachable as `oms.sessions`.
|
|
311
|
+
*
|
|
312
|
+
* Named `AuthSessionsNamespace` rather than `SessionsNamespace` because the
|
|
313
|
+
* SDK already has an `AuthNamespace` for OAuth and the RFC 8628 device grant,
|
|
314
|
+
* and the two are genuinely different credentials rather than two spellings of
|
|
315
|
+
* one. Nothing here touches OAuth except {@link adopt} and {@link oauthTicket},
|
|
316
|
+
* which are the two places the browser OAuth flow hands control back to a
|
|
317
|
+
* session.
|
|
318
|
+
*/
|
|
319
|
+
export declare class AuthSessionsNamespace extends Resource {
|
|
320
|
+
/**
|
|
321
|
+
* `POST /sessions` - trades an email and a password for a session token.
|
|
322
|
+
*
|
|
323
|
+
* Answers `201` with {@link SignedInSession}: the session row, the signed-in
|
|
324
|
+
* user inline, and the token, which appears here and nowhere else ever again.
|
|
325
|
+
*
|
|
326
|
+
* ## What to do with the answer, per mode
|
|
327
|
+
*
|
|
328
|
+
* **Token mode** (React Native, the CLI, anything not served from
|
|
329
|
+
* `omelhorsite.pt`): store `token` in the platform's secret store and build a
|
|
330
|
+
* client with it. The client you called this on has no credential, and adding
|
|
331
|
+
* one to an existing client is not possible - `Oms` takes its token at
|
|
332
|
+
* construction.
|
|
333
|
+
*
|
|
334
|
+
* ```ts
|
|
335
|
+
* const anon = new Oms({ baseUrl });
|
|
336
|
+
* const session = await anon.sessions.signIn({ email, password });
|
|
337
|
+
* await secureStore.set("oms_token", session.token);
|
|
338
|
+
* const oms = new Oms({ baseUrl, token: session.token });
|
|
339
|
+
* ```
|
|
340
|
+
*
|
|
341
|
+
* **Cookie mode** (`new Oms({ sessionCookie: true })`, a first-party page on
|
|
342
|
+
* `omelhorsite.pt` or `music.omelhorsite.pt`): the response carries
|
|
343
|
+
* `Set-Cookie: oms_session=...` and, because the transport sends
|
|
344
|
+
* `credentials: "include"`, the browser stores it. The SAME client is
|
|
345
|
+
* authenticated from the next call onwards; there is nothing to construct and
|
|
346
|
+
* nothing to store.
|
|
347
|
+
*
|
|
348
|
+
* ```ts
|
|
349
|
+
* const oms = new Oms({ sessionCookie: true });
|
|
350
|
+
* await oms.sessions.signIn({ email, password });
|
|
351
|
+
* const me = await oms.account.me(); // already authenticated
|
|
352
|
+
* ```
|
|
353
|
+
*
|
|
354
|
+
* **In cookie mode, throw the token away.** It is still in the response body,
|
|
355
|
+
* in plain JSON, readable by any script on the page - the httpOnly cookie
|
|
356
|
+
* cannot hide what the body already said. Persisting it to `localStorage`
|
|
357
|
+
* re-creates precisely the XSS-exfiltratable copy the cookie mode exists to
|
|
358
|
+
* eliminate, and it also gives you a second credential that outlives the
|
|
359
|
+
* first: sign out, and the cookie dies while the stored token keeps working.
|
|
360
|
+
* The web app is explicit about this - `persistSessionToken` writes only a
|
|
361
|
+
* non-sensitive `authed` flag when `isCookieAuth()`, and actively purges any
|
|
362
|
+
* legacy token it finds.
|
|
363
|
+
*
|
|
364
|
+
* Note that an `Oms` cannot be both: passing `sessionCookie: true` together
|
|
365
|
+
* with a token throws a `TypeError` at construction, deliberately, so that no
|
|
366
|
+
* request ever carries two credentials and leaves the server to choose.
|
|
367
|
+
*
|
|
368
|
+
* ## Cost and failure
|
|
369
|
+
*
|
|
370
|
+
* Throttled to **10 POSTs per minute per IP** (`login/ip`), which is a
|
|
371
|
+
* password-guessing bound and is keyed by address, so several users behind
|
|
372
|
+
* one NAT share it. The throttle matches a normalised path, so `/sessions/`,
|
|
373
|
+
* `//sessions` and `/sessions.json` all count against the same bucket - that
|
|
374
|
+
* gap was closed.
|
|
375
|
+
*
|
|
376
|
+
* Every successful sign-in creates a row AND fires a Discord alert to the
|
|
377
|
+
* owner. Signing in once per process invocation is how a device list fills up
|
|
378
|
+
* with a hundred identical entries; persist the token instead.
|
|
379
|
+
*
|
|
380
|
+
* Retries: an ambiguous network failure is NOT replayed, because this is a
|
|
381
|
+
* `POST` and the transport only replays safe methods by default. That is the
|
|
382
|
+
* right default here - a replay after a lost response mints a second session.
|
|
383
|
+
* A `429` IS still waited out and retried, which on a login screen can mean a
|
|
384
|
+
* silent minute-long pause; pass `retry: false` if you would rather show the
|
|
385
|
+
* user the rate-limit error immediately.
|
|
386
|
+
*
|
|
387
|
+
* Send a meaningful `clientName` on the `Oms` (it becomes `X-Oms-Client`) and,
|
|
388
|
+
* where the platform lets you set it, a real `User-Agent`: the server derives
|
|
389
|
+
* the session's `name`, `description` and `device_type` from the User-Agent of
|
|
390
|
+
* THIS request, and a blank one produces an unhelpful row in the user's own
|
|
391
|
+
* device list that they cannot fix except by renaming it.
|
|
392
|
+
*
|
|
393
|
+
* @throws {OmsAuthError} 401 `"Invalid email address or password."` for a
|
|
394
|
+
* wrong password, an unknown address, and a deactivated account alike. The
|
|
395
|
+
* three are not distinguishable, on purpose. (Deactivation is enforced as a
|
|
396
|
+
* validation on `Session` create, so it fails at the same place.)
|
|
397
|
+
* @throws {OmsQuotaError} 429 once the per-IP login budget is spent.
|
|
398
|
+
*/
|
|
399
|
+
signIn(input: SignInInput, options?: RequestOptions): Promise<SignedInSession>;
|
|
400
|
+
/**
|
|
401
|
+
* `DELETE /sessions/:id` - ends the session THIS credential is using.
|
|
402
|
+
*
|
|
403
|
+
* ## THE `:id` IS IGNORED. THIS ALWAYS DESTROYS THE CALLING SESSION.
|
|
404
|
+
*
|
|
405
|
+
* `SessionsController#destroy` does not look the path segment up. It does not
|
|
406
|
+
* call `resource`. It reads `Current.session`, destroys that, clears the
|
|
407
|
+
* cookie and answers `204`. So `DELETE /sessions/<any string at all>` means
|
|
408
|
+
* "log ME out", and there is no way through this API to revoke a different
|
|
409
|
+
* device. The web app's own "sign out this other device" button has always
|
|
410
|
+
* signed the user out of the browser they clicked it in.
|
|
411
|
+
*
|
|
412
|
+
* That is why this method takes no id. A signature that accepted one would be
|
|
413
|
+
* describing behaviour the server does not have, and the mistake it invites -
|
|
414
|
+
* passing a row from the device list - logs the user out of the wrong device
|
|
415
|
+
* with a `204` that looks like success.
|
|
416
|
+
*
|
|
417
|
+
* To actually end someone else's sessions there is exactly one lever, and it
|
|
418
|
+
* is administrative: {@link deactivateUser} runs `sessions.delete_all` on the
|
|
419
|
+
* target.
|
|
420
|
+
*
|
|
421
|
+
* ## What it does on the wire
|
|
422
|
+
*
|
|
423
|
+
* One request, to the literal path `/sessions/current`. The segment is a
|
|
424
|
+
* placeholder and it is spelled to read as one. `oms.account.sessions.revokeCurrent()`
|
|
425
|
+
* does the same thing in two requests, resolving the real id first so the call
|
|
426
|
+
* stays correct if the backend is ever fixed to honour the id; this one
|
|
427
|
+
* spends a single round trip and covers that case with a fallback instead,
|
|
428
|
+
* re-resolving through `GET /sessions/mine` and retrying if the placeholder
|
|
429
|
+
* ever starts answering 404.
|
|
430
|
+
*
|
|
431
|
+
* ## It does not throw when there was nothing to sign out of
|
|
432
|
+
*
|
|
433
|
+
* A dead, missing or already-revoked credential answers `404 "Session not
|
|
434
|
+
* found."` or `401`, and both are resolved rather than raised: you asked for
|
|
435
|
+
* the session to be gone and it is gone. Sign-out is idempotent, it usually
|
|
436
|
+
* runs while an app is tearing down, and a throw there strands clients with a
|
|
437
|
+
* credential they have decided to stop using. Everything else - a network
|
|
438
|
+
* failure, a 500, a 429 - is raised normally.
|
|
439
|
+
*
|
|
440
|
+
* Clear your own stored credential regardless of what this resolves to. The
|
|
441
|
+
* server side is best-effort; the client side is the part you control.
|
|
442
|
+
*
|
|
443
|
+
* In cookie mode the response also carries a cookie deletion, so the browser
|
|
444
|
+
* forgets it and the same client is anonymous from the next call onwards.
|
|
445
|
+
*/
|
|
446
|
+
signOut(options?: RequestOptions): Promise<void>;
|
|
447
|
+
/**
|
|
448
|
+
* `GET /sessions/mine` - the session the current credential resolves to.
|
|
449
|
+
*
|
|
450
|
+
* The cheapest liveness check there is, and the one the web app and the
|
|
451
|
+
* native app both boot with: a `200` means the stored credential still names
|
|
452
|
+
* a row, a `401` means it does not and the user must sign in again. Identical
|
|
453
|
+
* to `oms.account.sessions.current()`; both are here because "am I still
|
|
454
|
+
* signed in" belongs to the sign-in lifecycle and "which devices are signed
|
|
455
|
+
* in" belongs to the account screen.
|
|
456
|
+
*
|
|
457
|
+
* Returns the base `SessionBlueprint` view, with the owner inlined under
|
|
458
|
+
* `user` and WITHOUT `token`. There is no route that hands a token back.
|
|
459
|
+
*
|
|
460
|
+
* `/sessions/mine` is the whole spelling. There is NO `GET /sessions/current`:
|
|
461
|
+
* `resources :sessions` is declared `only: [:index, :create, :update,
|
|
462
|
+
* :destroy]`, so `show` is not routed and a GET to `/sessions/current` is a
|
|
463
|
+
* 404. (It IS a live path on DELETE, where it is the placeholder id
|
|
464
|
+
* {@link signOut} uses, which is exactly the sort of coincidence that makes
|
|
465
|
+
* the wrong spelling look plausible.)
|
|
466
|
+
*
|
|
467
|
+
* Note that reaching this endpoint at all rewrites `last_used_at` and can
|
|
468
|
+
* fire the "user is active again" alert, so it is not free of side effects
|
|
469
|
+
* and is not a good thing to poll.
|
|
470
|
+
*
|
|
471
|
+
* Counts against the general authenticated ceiling, 600 a minute. Careful
|
|
472
|
+
* with the failure case: a request whose token does not resolve is billed to
|
|
473
|
+
* the ANONYMOUS per-IP bucket of 120 a minute instead, so a client that
|
|
474
|
+
* retries a dead credential in a loop can rate-limit every user sharing that
|
|
475
|
+
* address.
|
|
476
|
+
*
|
|
477
|
+
* @throws {OmsAuthError} 401 when the credential is absent or dead.
|
|
478
|
+
* @throws {OmsApiError} 404 `"Session not found."` when the credential
|
|
479
|
+
* resolved to nothing but the request still reached the action.
|
|
480
|
+
*/
|
|
481
|
+
current(options?: RequestOptions): Promise<AccountSession>;
|
|
482
|
+
/**
|
|
483
|
+
* `POST /sessions/adopt` - trades a one-time OAuth ticket for a session.
|
|
484
|
+
*
|
|
485
|
+
* The last step of the browser OAuth flow. The provider round trip happens on
|
|
486
|
+
* the API host; its callback redirects the browser back to
|
|
487
|
+
* `https://omelhorsite.pt/account/oauth/callback?ticket=...` (hardcoded to
|
|
488
|
+
* `Rails.configuration.frontend_url`, not configurable per client), and the
|
|
489
|
+
* page hands that ticket here. The ticket exists so the session token itself
|
|
490
|
+
* never travels in a URL, a browser history entry or a `Referer`.
|
|
491
|
+
*
|
|
492
|
+
* Unauthenticated by design: this IS the sign-in step. Call it on a client
|
|
493
|
+
* with no credential. Answers `201 {"token": "..."}` and sets the cookie,
|
|
494
|
+
* exactly like {@link signIn}, so the same "which mode am I in" reasoning
|
|
495
|
+
* applies to the token it returns.
|
|
496
|
+
*
|
|
497
|
+
* ## MUST NOT BE RETRIED, and this method enforces that
|
|
498
|
+
*
|
|
499
|
+
* The ticket is one-time on the server. Redemption is claimed atomically with
|
|
500
|
+
* `Rails.cache.write(..., unless_exist: true)` before the session is adopted,
|
|
501
|
+
* and a second presentation of the same ticket gets the same `401 "Invalid or
|
|
502
|
+
* expired ticket."` as a forged one. So a retry after an ambiguous failure -
|
|
503
|
+
* a torn connection, a lost response - burns the ticket and reports a login
|
|
504
|
+
* failure for a login that actually SUCCEEDED. The user is left staring at an
|
|
505
|
+
* error page while the browser quietly holds a valid session cookie.
|
|
506
|
+
*
|
|
507
|
+
* This method therefore passes `retry: false` and a caller cannot override it
|
|
508
|
+
* back on. If the call fails ambiguously, the honest recovery is to check
|
|
509
|
+
* {@link current} before deciding anything: if it answers, you are signed in.
|
|
510
|
+
*
|
|
511
|
+
* The app-side documentation (`oms-music/docs/auth-account.md`, section 6)
|
|
512
|
+
* says the ticket is "not single-use server-side" and that the web enforces
|
|
513
|
+
* single use client-side with a sessionStorage nonce. THAT IS OUT OF DATE.
|
|
514
|
+
* The Rails code enforces it, and it is the enforcement that makes a retry
|
|
515
|
+
* destructive. When the doc and the Rails disagree, the Rails wins.
|
|
516
|
+
*
|
|
517
|
+
* Tickets are also short-lived, {@link OAUTH_TICKET_TTL_MS} (two minutes), so
|
|
518
|
+
* do not stash one to redeem later.
|
|
519
|
+
*
|
|
520
|
+
* @throws {OmsAuthError} 401 `"Invalid or expired ticket."` for a forged
|
|
521
|
+
* ticket, an expired one, and an already-redeemed one alike.
|
|
522
|
+
*/
|
|
523
|
+
adopt(ticket: string, options?: RequestOptions): Promise<AdoptedSession>;
|
|
524
|
+
/**
|
|
525
|
+
* `GET /sessions/oauth_ticket` - mints a short-lived ticket for the current
|
|
526
|
+
* session, so the session token itself never crosses a subdomain boundary.
|
|
527
|
+
*
|
|
528
|
+
* Only the web app needs this, and only for one thing: linking an OAuth
|
|
529
|
+
* provider to an account that is already signed in. That flow is a full-page
|
|
530
|
+
* navigation to `backend.omelhorsite.pt/auth/link/<provider>`, and a
|
|
531
|
+
* navigation cannot carry an `Authorization` header. The cookie is host-only
|
|
532
|
+
* on the API host, so it does not help either. The alternative would be
|
|
533
|
+
* `?token=<the session token>` in a URL that lands in browser history and in
|
|
534
|
+
* a `Referer`, which is exactly what this endpoint exists to avoid.
|
|
535
|
+
*
|
|
536
|
+
* Native and CLI clients have no such constraint and do not need this: they
|
|
537
|
+
* hold the token already.
|
|
538
|
+
*
|
|
539
|
+
* The result is scoped to the `oauth` purpose and expires after
|
|
540
|
+
* {@link OAUTH_TICKET_TTL_MS}. It authenticates nothing else - an API call
|
|
541
|
+
* carrying it as a bearer token is anonymous - and it is spent by the first
|
|
542
|
+
* {@link adopt} that redeems it. Mint one per navigation, never cache it.
|
|
543
|
+
*
|
|
544
|
+
* Requires a live session. Counts against the general authenticated ceiling.
|
|
545
|
+
*
|
|
546
|
+
* @throws {OmsApiError} 404 `"Session not found."` when there is no live
|
|
547
|
+
* session behind the credential.
|
|
548
|
+
*/
|
|
549
|
+
oauthTicket(options?: RequestOptions): Promise<SessionOAuthTicket>;
|
|
550
|
+
/**
|
|
551
|
+
* `POST /users/create_start` - emails a six-digit code to an address that
|
|
552
|
+
* does not have an account yet.
|
|
553
|
+
*
|
|
554
|
+
* Step one of two. Nothing is created here: the row appears only when
|
|
555
|
+
* {@link signUpComplete} presents the code back. Anonymous, and it must be -
|
|
556
|
+
* the caller has no account yet.
|
|
557
|
+
*
|
|
558
|
+
* Answers `200` with a BARE JSON STRING, not an object:
|
|
559
|
+
* `"Verification code sent to your email."`. Several endpoints in this
|
|
560
|
+
* namespace do that; the transport parses it into a `string`, so read it as
|
|
561
|
+
* one and do not reach for a `.message` that is not there.
|
|
562
|
+
*
|
|
563
|
+
* This is the one `*_start` in the family that leaks whether an address is
|
|
564
|
+
* registered: it answers `409 "Email already registered."` when it is. That
|
|
565
|
+
* is a deliberate trade for a usable signup form, and it is why
|
|
566
|
+
* {@link resetPasswordStart} does the opposite.
|
|
567
|
+
*
|
|
568
|
+
* Issuing a code DELETES any live code for the same address and reason
|
|
569
|
+
* (`where(reason:, email:).delete_all` before insert). One code per flow per
|
|
570
|
+
* address, always. A user who asks for a second code and then types the first
|
|
571
|
+
* one gets `404 "Invalid Verification"` and, worse, spends one of the five
|
|
572
|
+
* guesses belonging to the code they cannot see. Tell them the old code is
|
|
573
|
+
* dead when they request a new one.
|
|
574
|
+
*
|
|
575
|
+
* Throttled hard, and shared across all four `*_start` endpoints of the
|
|
576
|
+
* family: **4 per minute AND 20 per hour, per IP**. Both windows apply. This
|
|
577
|
+
* is anti-email-bombing, and the hourly one is what a "resend code" button
|
|
578
|
+
* with no cooldown will hit. Put a client-side cooldown on that button.
|
|
579
|
+
*
|
|
580
|
+
* @throws {OmsApiError} 409 when the address already has an account.
|
|
581
|
+
* @throws {OmsApiError} 422 with an array of validation messages when the
|
|
582
|
+
* address is not a valid email.
|
|
583
|
+
* @throws {OmsQuotaError} 429 on either the per-minute or the hourly bucket.
|
|
584
|
+
*/
|
|
585
|
+
signUpStart(email: string, options?: RequestOptions): Promise<string>;
|
|
586
|
+
/**
|
|
587
|
+
* `POST /users/create_end` - presents the code and creates the account.
|
|
588
|
+
*
|
|
589
|
+
* Answers `201` with the new {@link User}. Anonymous.
|
|
590
|
+
*
|
|
591
|
+
* ## IT DOES NOT SIGN YOU IN
|
|
592
|
+
*
|
|
593
|
+
* No session is created and no token is returned. The account exists and the
|
|
594
|
+
* caller is still anonymous. Follow it immediately with {@link signIn} using
|
|
595
|
+
* the same email and password - that is what both the web app and the native
|
|
596
|
+
* app do, and forgetting it is the classic "signup worked but the app is
|
|
597
|
+
* still on the login screen" bug.
|
|
598
|
+
*
|
|
599
|
+
* ```ts
|
|
600
|
+
* await oms.sessions.signUpStart(email);
|
|
601
|
+
* // user reads the email, types six digits
|
|
602
|
+
* await oms.sessions.signUpComplete({ email, code, name, password });
|
|
603
|
+
* const session = await oms.sessions.signIn({ email, password });
|
|
604
|
+
* ```
|
|
605
|
+
*
|
|
606
|
+
* `handle` cannot be chosen here even though the column exists: the parameter
|
|
607
|
+
* is not permitted on this action, and a `before_create` hook generates one
|
|
608
|
+
* from `name`. Let the user change it afterwards with
|
|
609
|
+
* `oms.account.update({ handle })`, where 15 characters is the ceiling.
|
|
610
|
+
*
|
|
611
|
+
* Consuming the code also stamps `email_verified_at`, inside the same create.
|
|
612
|
+
* The address is proven, so a freshly signed-up account is never in the
|
|
613
|
+
* "verify your email" limbo.
|
|
614
|
+
*
|
|
615
|
+
* Throttled to **10 a minute per IP**, shared with the other three `*_end`
|
|
616
|
+
* endpoints. Remember the per-code budget is separate and much smaller:
|
|
617
|
+
* {@link VERIFICATION_CODE_MAX_ATTEMPTS} wrong guesses destroy the code
|
|
618
|
+
* outright.
|
|
619
|
+
*
|
|
620
|
+
* @throws {OmsApiError} 404 `"Invalid Verification"` for a wrong code, an
|
|
621
|
+
* expired code, a code issued for a different address, and a code that has
|
|
622
|
+
* already been burned. All four look identical.
|
|
623
|
+
* @throws {OmsApiError} 422 with validation messages when the code was right
|
|
624
|
+
* but the account could not be created - a name that is too long, an
|
|
625
|
+
* address taken in the meantime. The code is CONSUMED by then, so the user
|
|
626
|
+
* has to restart at {@link signUpStart}.
|
|
627
|
+
*/
|
|
628
|
+
signUpComplete(input: SignUpInput, options?: RequestOptions): Promise<User>;
|
|
629
|
+
/**
|
|
630
|
+
* `POST /users/reset_password_start` - emails a reset code, if the address is
|
|
631
|
+
* registered.
|
|
632
|
+
*
|
|
633
|
+
* Anonymous. **Always answers `200` with the same bare string**, whether or
|
|
634
|
+
* not the address exists: `"If that email is registered, password reset
|
|
635
|
+
* instructions have been sent."`. That is anti-enumeration, and it is the
|
|
636
|
+
* deliberate opposite of {@link signUpStart}, which does tell you. Do not
|
|
637
|
+
* present this result to the user as confirmation that mail is on its way to
|
|
638
|
+
* a real account, because it is not evidence of that.
|
|
639
|
+
*
|
|
640
|
+
* A real send also fires a `password_reset_started` security alert to the
|
|
641
|
+
* owner.
|
|
642
|
+
*
|
|
643
|
+
* Same throttle family as every other `*_start`: **4 a minute and 20 an hour
|
|
644
|
+
* per IP**, shared.
|
|
645
|
+
*/
|
|
646
|
+
resetPasswordStart(email: string, options?: RequestOptions): Promise<string>;
|
|
647
|
+
/**
|
|
648
|
+
* `POST /users/reset_password_end` - presents the code and sets a new
|
|
649
|
+
* password.
|
|
650
|
+
*
|
|
651
|
+
* Anonymous, and answers `200` with the updated {@link User}. `password` is
|
|
652
|
+
* the ONLY field it writes; anything else in the body is dropped.
|
|
653
|
+
*
|
|
654
|
+
* It does NOT sign you in and it does NOT revoke other sessions. Resetting a
|
|
655
|
+
* password because it may have leaked leaves every existing session token
|
|
656
|
+
* alive, since a session is a row and not a signature over the password. If
|
|
657
|
+
* you are building a "my account was compromised" flow, changing the password
|
|
658
|
+
* is not enough on its own, and this API gives a user no way to end their own
|
|
659
|
+
* other sessions ({@link signOut} only ends the caller's). Escalating to an
|
|
660
|
+
* administrator and {@link deactivateUser} is the only lever that clears them.
|
|
661
|
+
*
|
|
662
|
+
* Consuming the code stamps `email_verified_at`: proving control of the
|
|
663
|
+
* mailbox verifies the address even if it never was verified before.
|
|
664
|
+
*
|
|
665
|
+
* Throttled to **10 a minute per IP**, shared with the other `*_end`
|
|
666
|
+
* endpoints, on top of the per-code budget of
|
|
667
|
+
* {@link VERIFICATION_CODE_MAX_ATTEMPTS}.
|
|
668
|
+
*
|
|
669
|
+
* @throws {OmsApiError} 404 `"Invalid Verification"` for a wrong, expired,
|
|
670
|
+
* burned or mismatched code; also `"User not found."` in the narrow race
|
|
671
|
+
* where the account is deleted between the code being verified and the row
|
|
672
|
+
* being loaded.
|
|
673
|
+
* @throws {OmsApiError} 422 with validation messages when the new password is
|
|
674
|
+
* rejected. The code is already consumed at that point.
|
|
675
|
+
*/
|
|
676
|
+
resetPasswordComplete(input: ResetPasswordInput, options?: RequestOptions): Promise<User>;
|
|
677
|
+
/**
|
|
678
|
+
* `POST /users/update_email_start` - emails TWO codes: one to the address
|
|
679
|
+
* currently on the account, one to the address it is moving to.
|
|
680
|
+
*
|
|
681
|
+
* Requires a live session. One HTTP request, two `EmailVerification` rows,
|
|
682
|
+
* two different reasons (`email_update_prev` and `email_update_new`), and
|
|
683
|
+
* {@link changeEmailComplete} needs both codes back. Proving control of the
|
|
684
|
+
* new mailbox alone is not enough: an attacker sitting on a hijacked session
|
|
685
|
+
* would otherwise move the account to an address they own and lock the real
|
|
686
|
+
* owner out permanently.
|
|
687
|
+
*
|
|
688
|
+
* The old address is read from the session, never from the arguments, so
|
|
689
|
+
* there is nothing to spoof: `email` here is the NEW address only.
|
|
690
|
+
*
|
|
691
|
+
* The two issues are not atomic. The previous-address code is issued first,
|
|
692
|
+
* and if the new address fails validation the call answers `422` with the old
|
|
693
|
+
* code already sent and live. Harmless, but expect users to report a code
|
|
694
|
+
* arriving for a change that "failed".
|
|
695
|
+
*
|
|
696
|
+
* Answers `200` with the bare string `"Email update instructions sent."`.
|
|
697
|
+
*
|
|
698
|
+
* Same shared `*_start` throttle: **4 a minute and 20 an hour per IP**. Note
|
|
699
|
+
* that this is one request even though it sends two emails, so it costs one
|
|
700
|
+
* unit of the budget, not two.
|
|
701
|
+
*
|
|
702
|
+
* @throws {OmsAuthError} 401 without a live session.
|
|
703
|
+
* @throws {OmsApiError} 422 with validation messages when either address is
|
|
704
|
+
* not a valid email.
|
|
705
|
+
*/
|
|
706
|
+
changeEmailStart(newEmail: string, options?: RequestOptions): Promise<string>;
|
|
707
|
+
/**
|
|
708
|
+
* `POST /users/update_email_end` - presents both codes and moves the address.
|
|
709
|
+
*
|
|
710
|
+
* Requires a live session; answers `200` with the updated {@link User}.
|
|
711
|
+
*
|
|
712
|
+
* Both codes are checked BEFORE either is consumed (the controller verifies
|
|
713
|
+
* twice with `destroy: false`, then verifies again to consume), so getting
|
|
714
|
+
* one right and one wrong burns neither. What it does still cost is an
|
|
715
|
+
* attempt against BOTH live codes: a wrong guess charges
|
|
716
|
+
* `register_failed_attempt` on the code for that reason, so a user typing the
|
|
717
|
+
* two codes into the wrong boxes spends one of the five guesses on each. With
|
|
718
|
+
* two codes in play the per-code budget is easier to exhaust than anywhere
|
|
719
|
+
* else in this family - validate with {@link isVerificationCode} first, and
|
|
720
|
+
* label the two inputs unmistakably.
|
|
721
|
+
*
|
|
722
|
+
* Consuming both stamps `email_verified_at`, since both mailboxes are proven.
|
|
723
|
+
*
|
|
724
|
+
* Throttled to **10 a minute per IP**, shared with the other `*_end`
|
|
725
|
+
* endpoints.
|
|
726
|
+
*
|
|
727
|
+
* @throws {OmsAuthError} 401 without a live session.
|
|
728
|
+
* @throws {OmsApiError} 404 `"Invalid Verification"` when either code is
|
|
729
|
+
* wrong, expired or burned. The message does not say which one.
|
|
730
|
+
* @throws {OmsApiError} 422 with validation messages when the new address is
|
|
731
|
+
* rejected at write time - already taken, malformed. Both codes are
|
|
732
|
+
* consumed by then and the flow restarts at {@link changeEmailStart}.
|
|
733
|
+
*/
|
|
734
|
+
changeEmailComplete(input: ChangeEmailInput, options?: RequestOptions): Promise<User>;
|
|
735
|
+
/**
|
|
736
|
+
* `POST /users/destroy_start` - emails a deletion code to the address on the
|
|
737
|
+
* account.
|
|
738
|
+
*
|
|
739
|
+
* Requires a live session. The address is taken from the session, so a user
|
|
740
|
+
* can only ever start deleting themselves; there is no argument and nothing
|
|
741
|
+
* to point at somebody else.
|
|
742
|
+
*
|
|
743
|
+
* Not in the same league as the other flows: {@link deleteAccountComplete}
|
|
744
|
+
* destroys the row and everything hanging off it. Make the confirmation
|
|
745
|
+
* unmistakable, and note that the code is the only thing standing between a
|
|
746
|
+
* hijacked session and a deleted account.
|
|
747
|
+
*
|
|
748
|
+
* Answers `200` with the bare string `"User deletion instructions sent."`.
|
|
749
|
+
* Shared `*_start` throttle: **4 a minute and 20 an hour per IP**.
|
|
750
|
+
*
|
|
751
|
+
* Included here even though it is the one pair of routes the sibling
|
|
752
|
+
* documentation does not list, because nothing else in the SDK covers it and
|
|
753
|
+
* a user who cannot delete their account has no exit.
|
|
754
|
+
*
|
|
755
|
+
* @throws {OmsAuthError} 401 without a live session.
|
|
756
|
+
*/
|
|
757
|
+
deleteAccountStart(options?: RequestOptions): Promise<string>;
|
|
758
|
+
/**
|
|
759
|
+
* `POST /users/destroy_end` - presents the code and DESTROYS THE ACCOUNT.
|
|
760
|
+
*
|
|
761
|
+
* Requires a live session. Irreversible: `user.destroy` runs, taking the
|
|
762
|
+
* user's sessions, files, music library and everything else that cascades
|
|
763
|
+
* from the row. There is no soft-delete on this path and no undo.
|
|
764
|
+
* {@link deactivateUser} is the reversible operation, and it is
|
|
765
|
+
* administrators only.
|
|
766
|
+
*
|
|
767
|
+
* Answers `200` with an empty body. The credential is dead the moment this
|
|
768
|
+
* resolves - the sessions went with the user - so discard the client and
|
|
769
|
+
* every stored token; do not try to {@link signOut} afterwards.
|
|
770
|
+
*
|
|
771
|
+
* Throttled to **10 a minute per IP**, shared with the other `*_end`
|
|
772
|
+
* endpoints, plus the per-code budget.
|
|
773
|
+
*
|
|
774
|
+
* @throws {OmsAuthError} 401 without a live session.
|
|
775
|
+
* @throws {OmsApiError} 404 `"Invalid Verification"` for a wrong, expired or
|
|
776
|
+
* burned code.
|
|
777
|
+
* @throws {OmsApiError} 500 with the model's error messages when the record
|
|
778
|
+
* could not be destroyed - a foreign key that refused to cascade. The
|
|
779
|
+
* account survives; the code does not.
|
|
780
|
+
*/
|
|
781
|
+
deleteAccountComplete(code: string, options?: RequestOptions): Promise<void>;
|
|
782
|
+
/**
|
|
783
|
+
* `GET /users` - the user roster.
|
|
784
|
+
*
|
|
785
|
+
* Requires a credential, and any authenticated account can enumerate the
|
|
786
|
+
* whole table: `User.viewable_by` is `->(user) { all }`. What an ordinary
|
|
787
|
+
* caller does NOT get is the privileged columns - `group`, `email`, `gender`,
|
|
788
|
+
* `last_seen_at`, `sessions_count`, `deactivated_at` and
|
|
789
|
+
* `allowed_to_use_spotify` are all rendered conditionally, so an absent key
|
|
790
|
+
* means "not visible to you", never "empty".
|
|
791
|
+
*
|
|
792
|
+
* This used to be anonymous and is not any more, precisely so that the roster
|
|
793
|
+
* could not be harvested. For a picker, prefer `oms.account.search()`: it is
|
|
794
|
+
* anonymous, capped at eight rows, and returns only id, handle and name.
|
|
795
|
+
*
|
|
796
|
+
* Only `name` and `handle` are filterable. Any other key is a `400 "Unknown
|
|
797
|
+
* search filter"` - this DSL fails closed rather than ignoring what it does
|
|
798
|
+
* not recognise.
|
|
799
|
+
*
|
|
800
|
+
* Deactivated accounts are NOT filtered out of this listing (the `active`
|
|
801
|
+
* scope is applied by `users#search`, not by the index), so a roster shows
|
|
802
|
+
* them; an administrator can tell by `deactivated_at`, and nobody else can.
|
|
803
|
+
*
|
|
804
|
+
* Index responses carry an `ETag`, so a repeat can answer `304` with no body.
|
|
805
|
+
* Counts against the general authenticated ceiling, 600 a minute.
|
|
806
|
+
*/
|
|
807
|
+
listUsers(params?: ListUsersParams, options?: RequestOptions): Promise<Paginated<User>>;
|
|
808
|
+
/**
|
|
809
|
+
* `POST /users/:id/deactivate` - administrators only. Suspends an account.
|
|
810
|
+
*
|
|
811
|
+
* ## This is also the only way to revoke somebody's sessions
|
|
812
|
+
*
|
|
813
|
+
* `User#deactivate!` stamps `deactivated_at` and runs `sessions.delete_all`
|
|
814
|
+
* in the same transaction, so every device the target is signed in on is
|
|
815
|
+
* logged out at once, and `Session`'s create validation then refuses to mint
|
|
816
|
+
* a new one. It is the single lever in this API that ends a session other
|
|
817
|
+
* than the caller's own - {@link signOut} cannot, and neither can anything in
|
|
818
|
+
* `oms.account.sessions`. If a token has leaked, this is the response.
|
|
819
|
+
*
|
|
820
|
+
* Reversible with {@link reactivateUser}, which clears the stamp. The old
|
|
821
|
+
* sessions do not come back; the user signs in again.
|
|
822
|
+
*
|
|
823
|
+
* Answers `200` with the target {@link User}.
|
|
824
|
+
*
|
|
825
|
+
* @throws {OmsAuthError} 401 `"Admins only."` for a non-administrator, and
|
|
826
|
+
* also for an anonymous caller.
|
|
827
|
+
* @throws {OmsApiError} 400 `"Cannot deactivate yourself."` - the guard that
|
|
828
|
+
* stops an administrator locking themselves out.
|
|
829
|
+
* @throws {OmsApiError} 404 `"User not found."`
|
|
830
|
+
*/
|
|
831
|
+
deactivateUser(id: Id, options?: RequestOptions): Promise<User>;
|
|
832
|
+
/**
|
|
833
|
+
* `POST /users/:id/reactivate` - administrators only. Clears
|
|
834
|
+
* `deactivated_at`.
|
|
835
|
+
*
|
|
836
|
+
* The account can sign in again from the next request. It does NOT restore
|
|
837
|
+
* the sessions {@link deactivateUser} deleted, so every device has to sign in
|
|
838
|
+
* fresh. Answers `200` with the target {@link User}.
|
|
839
|
+
*
|
|
840
|
+
* Unlike deactivation there is no self-guard, because reactivating yourself
|
|
841
|
+
* is not reachable: a deactivated administrator has no session to call with.
|
|
842
|
+
*
|
|
843
|
+
* @throws {OmsAuthError} 401 `"Admins only."`
|
|
844
|
+
* @throws {OmsApiError} 404 `"User not found."`
|
|
845
|
+
*/
|
|
846
|
+
reactivateUser(id: Id, options?: RequestOptions): Promise<User>;
|
|
847
|
+
}
|