@onlyworlds/sdk 4.30.0 → 4.31.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/AGENTS.md +4 -0
- package/CHANGELOG.md +29 -0
- package/README.md +13 -0
- package/dist/index.d.ts +74 -4
- package/dist/index.js +160 -3
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -57,6 +57,10 @@ against live data). The v1 surface (`OnlyWorldsClient`, the `ElementType` enum)
|
|
|
57
57
|
- Who am I (since 4.22.0, a world key): `client.me()` is `{ world, scope, kind, role,
|
|
58
58
|
membership, character }` (`membership` is what `created_by` holds for what you create; null
|
|
59
59
|
for the owner); `client.members()` is the roster's rows. An account token is a TypeError.
|
|
60
|
+
- Signing in (no key needed): `new OwV2Client().signInWithPassword({ email, password,
|
|
61
|
+
tokenName })` (or `username`) mints an `ow_a_` account token named for the device and ends
|
|
62
|
+
the sign-in session; only the token comes back. Refusals: `OwSignInError` with `reason`
|
|
63
|
+
(`invalid_credentials`, `email_unverified`, `rate_limited`, `offline`, `mint_failed`, `failed`).
|
|
60
64
|
- Invite links (since 4.12.0): `previewJoin(code)` (no credential sent) shows who invites, to
|
|
61
65
|
which world, in what role; `acceptJoin(code)` with an `ow_a_` account token joins. Bad code:
|
|
62
66
|
`err.isNotFound`; `err.isAlreadyMember`, `err.isPinRequired` for the two 409s.
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,35 @@ earlier history lives in git log only.
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [4.31.0] — 2026-10-18
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Signing in with a password** (keel #211): `signInWithPassword({ email | username, password, tokenName })`
|
|
12
|
+
turns an email address (or username) and password into a new account token (`ow_a_`) for this device,
|
|
13
|
+
named `tokenName`, the name the account's tokens page lists it under. It signs in at keel's sign-in service
|
|
14
|
+
(`{baseUrl}/auth/app/v1`), mints the token with that session at `POST /account/tokens`, then ends the
|
|
15
|
+
session, whether the mint worked or not. Only the token comes back (`OwSignedIn`: `token`, `id`, `name`,
|
|
16
|
+
`last4`); the password is sent once and held nowhere. Works on a client with no key, and sends no key
|
|
17
|
+
whatever the client holds. Exactly one of `email` and `username`, else a TypeError before sending.
|
|
18
|
+
- `OwSignInError`, with a `reason`: `invalid_credentials`, `email_unverified`, `rate_limited` (with
|
|
19
|
+
`retryAfter` when keel sends it), `offline`, `mint_failed` or `failed`, plus keel's `status` and `code`.
|
|
20
|
+
The password is never in its message or properties. For Atlas's sign-in (atlas #387) and Lantern, which
|
|
21
|
+
carried its own copy. The session's end is cut off after five seconds, so a hung network never holds a
|
|
22
|
+
sign-in whose token is already made. Checked live on production (a sign-in, the token reading the
|
|
23
|
+
account's worlds, revoked and read back as `key_revoked`).
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- `me()`'s doc names the player kind (keel #353).
|
|
27
|
+
|
|
28
|
+
## [4.30.1] — 2026-10-16
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
- **`changes()` and `changesAll()` hand back every cursor as a string**, as `OwChangesPage.cursor` has
|
|
32
|
+
always said. keel sends the cursor of a page at the head of the feed (an empty page, for any key but
|
|
33
|
+
a guest's) as a number, and it passed through as one: a walk of an up-to-date world returned
|
|
34
|
+
`{ cursor: 818 }`, so a cursor stored and compared as a string could miss. `changesHead()` already
|
|
35
|
+
did this. Found on production with a demo key.
|
|
36
|
+
|
|
8
37
|
## [4.30.0] — 2026-10-16
|
|
9
38
|
|
|
10
39
|
### Added
|
package/README.md
CHANGED
|
@@ -110,6 +110,19 @@ await writer.patch('character', id, { image_url: null });
|
|
|
110
110
|
|
|
111
111
|
The image's uploader or the world's owner can remove an uploaded image; the space goes back to the uploader's storage. Removal leaves elements as they are, so clear their `image_url` yourself (`createRemovalTicket(key).referenced` says how many still show it). A copy the network already holds can keep answering for a while.
|
|
112
112
|
|
|
113
|
+
## Signing in
|
|
114
|
+
|
|
115
|
+
An app where people sign in with their OnlyWorlds email (or username) and password turns that into an account token for the device. A client with no key can do it:
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
const { token } = await new OwV2Client().signInWithPassword({
|
|
119
|
+
email, password, tokenName: 'My app on iPhone', // the name the account's tokens page lists it under
|
|
120
|
+
});
|
|
121
|
+
const account = new OwV2Client({ apiKey: token }); // keep the token; the password is not kept anywhere
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The SDK signs in, mints the token, and ends the sign-in session. A refusal is an `OwSignInError` whose `reason` is `invalid_credentials`, `email_unverified`, `rate_limited` (see `retryAfter`), `offline`, `mint_failed` or `failed`.
|
|
125
|
+
|
|
113
126
|
## Invite links
|
|
114
127
|
|
|
115
128
|
A world's owner can invite a person with a link (`https://www.onlyworlds.com/join#ow_j_…`). The code is the part after `#`:
|
package/dist/index.d.ts
CHANGED
|
@@ -2317,6 +2317,17 @@ interface OwPlayerJoined {
|
|
|
2317
2317
|
}
|
|
2318
2318
|
/** POST /join's answer: tell the two apart by `kind`. */
|
|
2319
2319
|
type OwJoined = OwAgentJoined | OwPlayerJoined;
|
|
2320
|
+
/** signInWithPassword's answer: the new account token, shown once, and its row on the account's tokens page. */
|
|
2321
|
+
interface OwSignedIn {
|
|
2322
|
+
/** The account token (`ow_a_`): keep it, it is the only copy. */
|
|
2323
|
+
token: string;
|
|
2324
|
+
/** The token's id, for revoking it. */
|
|
2325
|
+
id: string;
|
|
2326
|
+
/** The name it was made under (`tokenName`). */
|
|
2327
|
+
name: string;
|
|
2328
|
+
/** Its last four characters, as the account's tokens page shows them. */
|
|
2329
|
+
last4: string;
|
|
2330
|
+
}
|
|
2320
2331
|
/** One live agent seat, as GET /account/worlds/{id}/seats lists it (keel #355, D106). No key or secret. */
|
|
2321
2332
|
interface OwSeat {
|
|
2322
2333
|
/** The seat's membership id: removeSeat takes it, and GET /members names the seat by it. */
|
|
@@ -2418,7 +2429,7 @@ interface OwClientConfig {
|
|
|
2418
2429
|
/**
|
|
2419
2430
|
* The key: an ow_w_ (write) or ow_r_ (read) world key, an ow_a_ account token (sent as a Bearer token, for the account routes), or a 10-digit legacy key.
|
|
2420
2431
|
* Leave it out for a client that holds none, such as a friend with only a link (since 4.30.0): it can call health,
|
|
2421
|
-
* previewJoin and
|
|
2432
|
+
* previewJoin, redeemJoin and signInWithPassword, and any other call is a TypeError before anything is sent.
|
|
2422
2433
|
*/
|
|
2423
2434
|
apiKey?: string;
|
|
2424
2435
|
/**
|
|
@@ -2573,6 +2584,37 @@ declare class OwNetworkError extends Error {
|
|
|
2573
2584
|
readonly cause2: unknown;
|
|
2574
2585
|
constructor(message: string, cause: unknown);
|
|
2575
2586
|
}
|
|
2587
|
+
/**
|
|
2588
|
+
* Why `signInWithPassword` refused:
|
|
2589
|
+
* - `invalid_credentials`: keel does not know that email (or username) with that password.
|
|
2590
|
+
* - `email_unverified`: the account's email address is not confirmed yet; the link is in the
|
|
2591
|
+
* mail onlyworlds.com sent when the account was made.
|
|
2592
|
+
* - `rate_limited`: too many sign-ins (a 429), or too many failed ones for this account
|
|
2593
|
+
* (`too_many_login_attempts`); wait `retryAfter` seconds when it is set.
|
|
2594
|
+
* - `offline`: the request never reached keel.
|
|
2595
|
+
* - `mint_failed`: the password was right, but keel made no token; the session was still ended.
|
|
2596
|
+
* - `failed`: any other refusal; the message is keel's when it gave one.
|
|
2597
|
+
*/
|
|
2598
|
+
type OwSignInReason = 'invalid_credentials' | 'email_unverified' | 'rate_limited' | 'offline' | 'mint_failed' | 'failed';
|
|
2599
|
+
/**
|
|
2600
|
+
* A refused `signInWithPassword`. `reason` is what to tell the user; `status` and `code` are
|
|
2601
|
+
* keel's answer when there was one (`code`: keel's error code, e.g. `email_password_mismatch`).
|
|
2602
|
+
* The password is never in the message or in any property.
|
|
2603
|
+
*/
|
|
2604
|
+
declare class OwSignInError extends Error {
|
|
2605
|
+
readonly reason: OwSignInReason;
|
|
2606
|
+
/** The HTTP status of the refusing answer; absent when nothing came back (`offline`). */
|
|
2607
|
+
readonly status?: number;
|
|
2608
|
+
/** keel's error code, when its answer carried one. */
|
|
2609
|
+
readonly code?: string;
|
|
2610
|
+
/** Seconds to wait before trying again, from `Retry-After` (on a 429). */
|
|
2611
|
+
readonly retryAfter?: number;
|
|
2612
|
+
constructor(reason: OwSignInReason, message: string, opts?: {
|
|
2613
|
+
status?: number;
|
|
2614
|
+
code?: string;
|
|
2615
|
+
retryAfter?: number;
|
|
2616
|
+
});
|
|
2617
|
+
}
|
|
2576
2618
|
/** Parse a wire envelope into OwApiError parts (exported for the error type-tests). */
|
|
2577
2619
|
/** Parse the platform ERROR envelope into an OwApiError. (Renamed from parseEnvelope in 4.0 —
|
|
2578
2620
|
* distinct from the world-export envelope, which is a different artifact entirely.) */
|
|
@@ -2596,7 +2638,7 @@ type OwKeyKind =
|
|
|
2596
2638
|
| 'account'
|
|
2597
2639
|
/** Grandfathered 10-digit key. Needs PIN to read private worlds. */
|
|
2598
2640
|
| 'legacy' | 'unknown'
|
|
2599
|
-
/** No key: a client for the routes that send none (health, previewJoin, redeemJoin), as a friend with only a link has. */
|
|
2641
|
+
/** No key: a client for the routes that send none (health, previewJoin, redeemJoin, signInWithPassword), as a friend with only a link has. */
|
|
2600
2642
|
| 'none';
|
|
2601
2643
|
/** Demo range 0000000000-0000000009: read-only aliases, safe as live read gates. */
|
|
2602
2644
|
declare function isDemoKey(key: string): boolean;
|
|
@@ -2645,7 +2687,7 @@ declare class OwV2Client {
|
|
|
2645
2687
|
patchWorld(partial: Record<string, unknown>): Promise<OwWorldMeta>;
|
|
2646
2688
|
/**
|
|
2647
2689
|
* GET /me -- who this world key is: the world, read or write scope, the kind (owner,
|
|
2648
|
-
* person, agent), the role, the membership id (what `created_by` holds for what this key
|
|
2690
|
+
* person, agent, player), the role, the membership id (what `created_by` holds for what this key
|
|
2649
2691
|
* creates; null for the owner) and the member's Character. A world key's call; an account
|
|
2650
2692
|
* token is a TypeError before sending.
|
|
2651
2693
|
*/
|
|
@@ -2735,6 +2777,8 @@ declare class OwV2Client {
|
|
|
2735
2777
|
* view changed (a guest's, or its role): drop the cursor, walk from zero, and
|
|
2736
2778
|
* replace the local copy rather than merging into it.
|
|
2737
2779
|
* Citizenship: heaviest route on the platform; default page size is polite.
|
|
2780
|
+
* keel sends the cursor of a page at the head as a number (an empty page, for anyone
|
|
2781
|
+
* but a guest); it is handed back as a string, like every cursor here.
|
|
2738
2782
|
*/
|
|
2739
2783
|
changes(opts?: {
|
|
2740
2784
|
since?: string;
|
|
@@ -2848,6 +2892,32 @@ declare class OwV2Client {
|
|
|
2848
2892
|
name?: string;
|
|
2849
2893
|
agentName?: string;
|
|
2850
2894
|
}): Promise<OwJoined>;
|
|
2895
|
+
/**
|
|
2896
|
+
* Sign in with an email address (or a username) and password, and get a new account token
|
|
2897
|
+
* (`ow_a_`) for this device, named `tokenName` (say, "Lantern on iPhone"): the name the
|
|
2898
|
+
* account's tokens page lists it under, where it can be revoked. Pass exactly one of
|
|
2899
|
+
* `email` and `username`; both or neither is a TypeError before anything is sent.
|
|
2900
|
+
*
|
|
2901
|
+
* Works on a client with no key, and sends no key whatever this client holds. Three
|
|
2902
|
+
* requests: a sign-in at keel's sign-in service (`{baseUrl}/auth/app/v1/auth/login`), which
|
|
2903
|
+
* answers with a short-lived session; `POST /account/tokens` with that session, which mints
|
|
2904
|
+
* the token; then the session is ended, whether the mint worked or not. Only the token is
|
|
2905
|
+
* kept: the password is sent once and held nowhere, and the session never leaves this call.
|
|
2906
|
+
* Store the token as you would any account token, and make a client with it
|
|
2907
|
+
* (`new OwV2Client({ apiKey: token })`).
|
|
2908
|
+
*
|
|
2909
|
+
* A refusal is an `OwSignInError` whose `reason` says why: `invalid_credentials` (wrong
|
|
2910
|
+
* email, username or password), `email_unverified` (the account's email address is not
|
|
2911
|
+
* confirmed yet), `rate_limited` (too many attempts; `retryAfter` when keel sent it),
|
|
2912
|
+
* `offline` (keel was not reached), `mint_failed` (signed in, but no token was made) or
|
|
2913
|
+
* `failed` (anything else, with keel's message). The password is never in an error.
|
|
2914
|
+
*/
|
|
2915
|
+
signInWithPassword(opts: {
|
|
2916
|
+
email?: string;
|
|
2917
|
+
username?: string;
|
|
2918
|
+
password: string;
|
|
2919
|
+
tokenName: string;
|
|
2920
|
+
}): Promise<OwSignedIn>;
|
|
2851
2921
|
/**
|
|
2852
2922
|
* POST /account/join -- accept an invite link's code as the account this client's
|
|
2853
2923
|
* `ow_a_` token belongs to: 201 with the new membership. Refusals: 404 `not_found`
|
|
@@ -3925,4 +3995,4 @@ declare class TokenResource {
|
|
|
3925
3995
|
getEncryptionInfo(): Promise<EncryptionInfo>;
|
|
3926
3996
|
}
|
|
3927
3997
|
|
|
3928
|
-
export { AGENT_DOORS, ASK_LIMITS, type AccessKeyResponse, CALENDAR_FIELD, CALENDAR_SUPERTYPE, type CalendarConfig, type CalendarEra, type CalendarIntercalary, type CalendarMonths, type CalendarWeek, ELEMENT_FAMILIES, ELEMENT_ICONS, ELEMENT_LABELS, ELEMENT_SECTIONS, ELEMENT_TYPES, type ElementFamily, type ElementType, type EncryptionInfo, type EraDomain, FAMILY_COLORS, FAMILY_ORDER, FIELD_SCHEMA, type FieldInfo, type FieldType, GameTier, type ListParams, ONLYWORLDS_VERSION, type OwAgentJoined, type OwAllowance, OwApiError, type OwAskAnswer, type OwAskContext, type OwAuthErrorCode, type OwBulkItem, type OwBulkItemResult, type OwBulkResponse, type OwCalendarElement, type OwChange, type OwChangeDelete, type OwChangeUpsert, type OwChangesPage, type OwClientConfig, type OwCreatedWorldLink, type OwElement, type OwElementBase, type OwErrorBody, type OwJoinPreview, type OwJoined, type OwJoinedMembership, type OwKeyKind, type OwLinkEdit, type OwLookupElement, type OwLookupHit, type OwLookupLink, type OwLookupOptions, type OwLookupReason, type OwMe, type OwMediaTicket, type OwMember, type OwMemberRole, OwNetworkError, type OwPage, type OwPhotoOptions, type OwPhotoPlan, type OwPlayerJoined, OwPreconditionFailedError, type OwPreparedPhoto, type OwReadWorldFile, type OwReadWorldFileOptions, type OwRemovalTicket, type OwRemovedImage, type OwRenderOptions, type OwSeat, type OwSeatCreated, type OwSortAddToRow, type OwSortAnswer, type OwSortAskRow, type OwSortLinkRow, type OwSortMakeRow, type OwSortNote, type OwSortRef, type OwSortRow, type OwUploadedImage, OwV2Client, type OwWorldFileEntry, OwWorldFileError, type OwWorldFileReason, type OwWorldIndex, type OwWorldLink, type OwWorldMeta, type OwWriteDoor, PHOTO_MAX_BYTES, PHOTO_TYPES, type ParseError, type ParseOptions, type ParseResult, type RevokeAllSessionsResponse, type RevokeSessionResponse, SPATIAL_TYPES, type SectionInfo, type StepBreakdown, type TokenConsumeParams, type TokenConsumeResponse, TokenResource, type TokenStatus, type TokenTransport, WORLD_FILE_MAX_BYTES, WORLD_FILE_MAX_ENTRIES, type WorldCalendar, absYearForEraYear, absoluteYear, approxYearLength, breakdownStep, buildAskContext, buildWorldIndex, detectKeyKind, displayYear, elementColor, eraForStep, errorFromResponse, familyOf, floorDiv, floorMod, formatDuration, formatMidLabel, formatStep, formatYearLabel, getElementIcon, getElementLabel, isAgentDoor, isDemoKey, isLeapYear, isParseError, kindCanWrite, lookupWorld, monthStartSteps, parseDate, parseErrorEnvelope, pinExpectation, planPhoto, preparePhoto, readCalendarConfig, readCalendarValue, readWorldFile, renderElement, resolveCalendarLens, resolveEraDomains, validateCalendarConfig, weekdayForStep, worldCalendars, writeWorldFile, yearLength, yearStartStep };
|
|
3998
|
+
export { AGENT_DOORS, ASK_LIMITS, type AccessKeyResponse, CALENDAR_FIELD, CALENDAR_SUPERTYPE, type CalendarConfig, type CalendarEra, type CalendarIntercalary, type CalendarMonths, type CalendarWeek, ELEMENT_FAMILIES, ELEMENT_ICONS, ELEMENT_LABELS, ELEMENT_SECTIONS, ELEMENT_TYPES, type ElementFamily, type ElementType, type EncryptionInfo, type EraDomain, FAMILY_COLORS, FAMILY_ORDER, FIELD_SCHEMA, type FieldInfo, type FieldType, GameTier, type ListParams, ONLYWORLDS_VERSION, type OwAgentJoined, type OwAllowance, OwApiError, type OwAskAnswer, type OwAskContext, type OwAuthErrorCode, type OwBulkItem, type OwBulkItemResult, type OwBulkResponse, type OwCalendarElement, type OwChange, type OwChangeDelete, type OwChangeUpsert, type OwChangesPage, type OwClientConfig, type OwCreatedWorldLink, type OwElement, type OwElementBase, type OwErrorBody, type OwJoinPreview, type OwJoined, type OwJoinedMembership, type OwKeyKind, type OwLinkEdit, type OwLookupElement, type OwLookupHit, type OwLookupLink, type OwLookupOptions, type OwLookupReason, type OwMe, type OwMediaTicket, type OwMember, type OwMemberRole, OwNetworkError, type OwPage, type OwPhotoOptions, type OwPhotoPlan, type OwPlayerJoined, OwPreconditionFailedError, type OwPreparedPhoto, type OwReadWorldFile, type OwReadWorldFileOptions, type OwRemovalTicket, type OwRemovedImage, type OwRenderOptions, type OwSeat, type OwSeatCreated, OwSignInError, type OwSignInReason, type OwSignedIn, type OwSortAddToRow, type OwSortAnswer, type OwSortAskRow, type OwSortLinkRow, type OwSortMakeRow, type OwSortNote, type OwSortRef, type OwSortRow, type OwUploadedImage, OwV2Client, type OwWorldFileEntry, OwWorldFileError, type OwWorldFileReason, type OwWorldIndex, type OwWorldLink, type OwWorldMeta, type OwWriteDoor, PHOTO_MAX_BYTES, PHOTO_TYPES, type ParseError, type ParseOptions, type ParseResult, type RevokeAllSessionsResponse, type RevokeSessionResponse, SPATIAL_TYPES, type SectionInfo, type StepBreakdown, type TokenConsumeParams, type TokenConsumeResponse, TokenResource, type TokenStatus, type TokenTransport, WORLD_FILE_MAX_BYTES, WORLD_FILE_MAX_ENTRIES, type WorldCalendar, absYearForEraYear, absoluteYear, approxYearLength, breakdownStep, buildAskContext, buildWorldIndex, detectKeyKind, displayYear, elementColor, eraForStep, errorFromResponse, familyOf, floorDiv, floorMod, formatDuration, formatMidLabel, formatStep, formatYearLabel, getElementIcon, getElementLabel, isAgentDoor, isDemoKey, isLeapYear, isParseError, kindCanWrite, lookupWorld, monthStartSteps, parseDate, parseErrorEnvelope, pinExpectation, planPhoto, preparePhoto, readCalendarConfig, readCalendarValue, readWorldFile, renderElement, resolveCalendarLens, resolveEraDomains, validateCalendarConfig, weekdayForStep, worldCalendars, writeWorldFile, yearLength, yearStartStep };
|
package/dist/index.js
CHANGED
|
@@ -182,6 +182,16 @@ var OwNetworkError = class extends Error {
|
|
|
182
182
|
this.cause2 = cause;
|
|
183
183
|
}
|
|
184
184
|
};
|
|
185
|
+
var OwSignInError = class extends Error {
|
|
186
|
+
constructor(reason, message, opts = {}) {
|
|
187
|
+
super(message);
|
|
188
|
+
this.name = "OwSignInError";
|
|
189
|
+
this.reason = reason;
|
|
190
|
+
if (opts.status !== void 0) this.status = opts.status;
|
|
191
|
+
if (opts.code !== void 0) this.code = opts.code;
|
|
192
|
+
if (opts.retryAfter !== void 0) this.retryAfter = opts.retryAfter;
|
|
193
|
+
}
|
|
194
|
+
};
|
|
185
195
|
function parseErrorEnvelope(status, body, retryAfter = null) {
|
|
186
196
|
const env = body && typeof body === "object" ? body : {};
|
|
187
197
|
const nested = typeof env.error === "object" && env.error !== null ? env.error : void 0;
|
|
@@ -282,7 +292,7 @@ var OwV2Client = class {
|
|
|
282
292
|
// -- Who am I, and who is here ---------------------------------------------
|
|
283
293
|
/**
|
|
284
294
|
* GET /me -- who this world key is: the world, read or write scope, the kind (owner,
|
|
285
|
-
* person, agent), the role, the membership id (what `created_by` holds for what this key
|
|
295
|
+
* person, agent, player), the role, the membership id (what `created_by` holds for what this key
|
|
286
296
|
* creates; null for the owner) and the member's Character. A world key's call; an account
|
|
287
297
|
* token is a TypeError before sending.
|
|
288
298
|
*/
|
|
@@ -431,10 +441,13 @@ var OwV2Client = class {
|
|
|
431
441
|
* view changed (a guest's, or its role): drop the cursor, walk from zero, and
|
|
432
442
|
* replace the local copy rather than merging into it.
|
|
433
443
|
* Citizenship: heaviest route on the platform; default page size is polite.
|
|
444
|
+
* keel sends the cursor of a page at the head as a number (an empty page, for anyone
|
|
445
|
+
* but a guest); it is handed back as a string, like every cursor here.
|
|
434
446
|
*/
|
|
435
447
|
async changes(opts = {}) {
|
|
436
448
|
const query = buildQuery({ since: opts.since, limit: opts.limit ?? this.changesPageSize });
|
|
437
|
-
|
|
449
|
+
const page = await this.request("GET", "/changes/", { query });
|
|
450
|
+
return { ...page, cursor: String(page.cursor) };
|
|
438
451
|
}
|
|
439
452
|
/**
|
|
440
453
|
* GET /changes?head=true -- where the feed is now, without a page: no element is read.
|
|
@@ -585,6 +598,131 @@ var OwV2Client = class {
|
|
|
585
598
|
if (opts.agentName !== void 0) body.agent_name = opts.agentName;
|
|
586
599
|
return this.request("POST", "/join", { auth: false, body });
|
|
587
600
|
}
|
|
601
|
+
// -- Signing in with a password (keel #211) --------------------------------
|
|
602
|
+
/**
|
|
603
|
+
* Sign in with an email address (or a username) and password, and get a new account token
|
|
604
|
+
* (`ow_a_`) for this device, named `tokenName` (say, "Lantern on iPhone"): the name the
|
|
605
|
+
* account's tokens page lists it under, where it can be revoked. Pass exactly one of
|
|
606
|
+
* `email` and `username`; both or neither is a TypeError before anything is sent.
|
|
607
|
+
*
|
|
608
|
+
* Works on a client with no key, and sends no key whatever this client holds. Three
|
|
609
|
+
* requests: a sign-in at keel's sign-in service (`{baseUrl}/auth/app/v1/auth/login`), which
|
|
610
|
+
* answers with a short-lived session; `POST /account/tokens` with that session, which mints
|
|
611
|
+
* the token; then the session is ended, whether the mint worked or not. Only the token is
|
|
612
|
+
* kept: the password is sent once and held nowhere, and the session never leaves this call.
|
|
613
|
+
* Store the token as you would any account token, and make a client with it
|
|
614
|
+
* (`new OwV2Client({ apiKey: token })`).
|
|
615
|
+
*
|
|
616
|
+
* A refusal is an `OwSignInError` whose `reason` says why: `invalid_credentials` (wrong
|
|
617
|
+
* email, username or password), `email_unverified` (the account's email address is not
|
|
618
|
+
* confirmed yet), `rate_limited` (too many attempts; `retryAfter` when keel sent it),
|
|
619
|
+
* `offline` (keel was not reached), `mint_failed` (signed in, but no token was made) or
|
|
620
|
+
* `failed` (anything else, with keel's message). The password is never in an error.
|
|
621
|
+
*/
|
|
622
|
+
async signInWithPassword(opts) {
|
|
623
|
+
const { email, username, password, tokenName } = opts ?? {};
|
|
624
|
+
const login = typeof email === "string" ? email.trim() : "";
|
|
625
|
+
const user = typeof username === "string" ? username.trim() : "";
|
|
626
|
+
if (Boolean(login) === Boolean(user)) {
|
|
627
|
+
throw new TypeError("signInWithPassword takes exactly one of email and username");
|
|
628
|
+
}
|
|
629
|
+
if (typeof password !== "string" || password === "") {
|
|
630
|
+
throw new TypeError("signInWithPassword needs a password");
|
|
631
|
+
}
|
|
632
|
+
if (typeof tokenName !== "string" || tokenName.trim() === "") {
|
|
633
|
+
throw new TypeError("signInWithPassword needs a tokenName: the name the new token is listed under");
|
|
634
|
+
}
|
|
635
|
+
const auth = `${this.baseUrl}/auth/app/v1`;
|
|
636
|
+
const safe = (text, fallback) => typeof text === "string" && text !== "" && !text.includes(password) ? text : fallback;
|
|
637
|
+
let res;
|
|
638
|
+
try {
|
|
639
|
+
res = await this.fetchImpl(`${auth}/auth/login`, {
|
|
640
|
+
method: "POST",
|
|
641
|
+
headers: { "Content-Type": "application/json" },
|
|
642
|
+
body: JSON.stringify(login ? { email: login, password } : { username: user, password })
|
|
643
|
+
});
|
|
644
|
+
} catch {
|
|
645
|
+
throw new OwSignInError("offline", "OnlyWorlds could not be reached: signing in needs a connection.");
|
|
646
|
+
}
|
|
647
|
+
const retryAfter = parseRetryAfter(res.headers?.get?.("Retry-After") ?? null) ?? void 0;
|
|
648
|
+
if (res.status === 429) {
|
|
649
|
+
throw new OwSignInError("rate_limited", "Too many sign-in attempts. Wait a while, then try again.", {
|
|
650
|
+
status: 429,
|
|
651
|
+
retryAfter
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
const body = await readJsonBody(res);
|
|
655
|
+
const sessionToken = body?.meta?.session_token;
|
|
656
|
+
const session = typeof sessionToken === "string" && sessionToken !== "" ? sessionToken : void 0;
|
|
657
|
+
if (!res.ok || !session) {
|
|
658
|
+
if (session) await endAuthSession(this.fetchImpl, auth, session);
|
|
659
|
+
const errors = Array.isArray(body?.errors) ? body.errors : [];
|
|
660
|
+
const codes = errors.map((e) => e?.code).filter((c) => typeof c === "string");
|
|
661
|
+
const first = errors.find((e) => typeof e?.message === "string")?.message;
|
|
662
|
+
const code = codes.find((c) => !c.includes(password));
|
|
663
|
+
const status = res.status;
|
|
664
|
+
const mismatch = codes.find((c) => c === "email_password_mismatch" || c === "username_password_mismatch");
|
|
665
|
+
if (mismatch) {
|
|
666
|
+
throw new OwSignInError(
|
|
667
|
+
"invalid_credentials",
|
|
668
|
+
safe(first, "The email address and/or password you specified are not correct."),
|
|
669
|
+
{ status, code: mismatch }
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
if (codes.includes("too_many_login_attempts")) {
|
|
673
|
+
throw new OwSignInError(
|
|
674
|
+
"rate_limited",
|
|
675
|
+
safe(first, "Too many failed sign-in attempts. Try again later."),
|
|
676
|
+
{ status, code: "too_many_login_attempts", retryAfter }
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
const flows = Array.isArray(body?.data?.flows) ? body.data.flows : [];
|
|
680
|
+
if (status === 401 && flows.some((f) => f?.id === "verify_email" && f?.is_pending === true)) {
|
|
681
|
+
throw new OwSignInError(
|
|
682
|
+
"email_unverified",
|
|
683
|
+
"Confirm the account's email address first: the link is in the mail onlyworlds.com sent.",
|
|
684
|
+
{ status, code }
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
throw new OwSignInError(
|
|
688
|
+
"failed",
|
|
689
|
+
safe(first, res.ok ? "Signing in did not work: onlyworlds.com sent no session." : `Signing in did not work (${status}).`),
|
|
690
|
+
{ status, code }
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
try {
|
|
694
|
+
let minted;
|
|
695
|
+
try {
|
|
696
|
+
minted = await this.fetchImpl(`${this.baseUrl}/account/tokens`, {
|
|
697
|
+
method: "POST",
|
|
698
|
+
headers: { "Content-Type": "application/json", "X-Session-Token": session },
|
|
699
|
+
body: JSON.stringify({ name: tokenName })
|
|
700
|
+
});
|
|
701
|
+
} catch {
|
|
702
|
+
throw new OwSignInError("offline", "The connection dropped after signing in: no token was made. Try again.");
|
|
703
|
+
}
|
|
704
|
+
const row = await readJsonBody(minted);
|
|
705
|
+
const token = row?.token;
|
|
706
|
+
if (minted.status !== 201 || typeof token !== "string" || token === "") {
|
|
707
|
+
const err = row && typeof row.error === "object" && row.error !== null ? row.error : void 0;
|
|
708
|
+
const said = err?.code;
|
|
709
|
+
const code = typeof said === "string" && !said.includes(password) ? said : void 0;
|
|
710
|
+
throw new OwSignInError(
|
|
711
|
+
"mint_failed",
|
|
712
|
+
safe(err?.message, `Signed in, but no token was made (${minted.status}). Try again.`),
|
|
713
|
+
{ status: minted.status, code }
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
return {
|
|
717
|
+
token,
|
|
718
|
+
id: typeof row.id === "string" ? row.id : "",
|
|
719
|
+
name: typeof row.name === "string" ? row.name : tokenName,
|
|
720
|
+
last4: typeof row.last4 === "string" ? row.last4 : token.slice(-4)
|
|
721
|
+
};
|
|
722
|
+
} finally {
|
|
723
|
+
await endAuthSession(this.fetchImpl, auth, session);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
588
726
|
/**
|
|
589
727
|
* POST /account/join -- accept an invite link's code as the account this client's
|
|
590
728
|
* `ow_a_` token belongs to: 201 with the new membership. Refusals: 404 `not_found`
|
|
@@ -764,7 +902,7 @@ var OwV2Client = class {
|
|
|
764
902
|
throw new TypeError(`ifMatch must be a whole number (an element's change_seq), got ${String(opts.ifMatch)}`);
|
|
765
903
|
}
|
|
766
904
|
if (opts.auth !== false && this.keyKind === "none") {
|
|
767
|
-
throw new TypeError(`${method} ${path} needs a key, and this client holds none: without one, only health, previewJoin and
|
|
905
|
+
throw new TypeError(`${method} ${path} needs a key, and this client holds none: without one, only health, previewJoin, redeemJoin and signInWithPassword work`);
|
|
768
906
|
}
|
|
769
907
|
const url = `${this.baseUrl}${path}${opts.query ? `?${opts.query}` : ""}`;
|
|
770
908
|
const headers = {};
|
|
@@ -809,6 +947,24 @@ function sanitizePayload(payload) {
|
|
|
809
947
|
for (const field of READ_ONLY_FIELDS) delete rest[field];
|
|
810
948
|
return rest;
|
|
811
949
|
}
|
|
950
|
+
async function endAuthSession(fetchImpl, auth, session) {
|
|
951
|
+
const abort = new AbortController();
|
|
952
|
+
const timer = setTimeout(() => abort.abort(), 5e3);
|
|
953
|
+
try {
|
|
954
|
+
await fetchImpl(`${auth}/auth/session`, { method: "DELETE", headers: { "X-Session-Token": session }, signal: abort.signal });
|
|
955
|
+
} catch {
|
|
956
|
+
} finally {
|
|
957
|
+
clearTimeout(timer);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
async function readJsonBody(res) {
|
|
961
|
+
try {
|
|
962
|
+
const text = await res.text();
|
|
963
|
+
return text ? JSON.parse(text) : null;
|
|
964
|
+
} catch {
|
|
965
|
+
return null;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
812
968
|
function readReplayHeader(headers) {
|
|
813
969
|
const v = headers.get("Idempotent-Replay");
|
|
814
970
|
return v != null && v.toLowerCase() === "true";
|
|
@@ -3033,6 +3189,7 @@ export {
|
|
|
3033
3189
|
OwApiError,
|
|
3034
3190
|
OwNetworkError,
|
|
3035
3191
|
OwPreconditionFailedError,
|
|
3192
|
+
OwSignInError,
|
|
3036
3193
|
OwV2Client,
|
|
3037
3194
|
OwWorldFileError,
|
|
3038
3195
|
PHOTO_MAX_BYTES,
|