@onlyworlds/sdk 4.45.0 → 4.46.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/CHANGELOG.md +5 -0
- package/dist/index.d.ts +81 -2
- package/dist/index.js +180 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,11 @@ earlier history lives in git log only.
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [4.46.0] - 2026-10-22
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Sign in with OnlyWorlds** (keel PR 154, live 2026-10-22 01:50; the handoff sheet §3): `signInWithOnlyWorlds({ clientId, redirectUri, resume, storage, navigate })` mints `state` and a PKCE S256 verifier, keeps them under the client id in `storage` (sessionStorage by default; required in Node), refuses a `redirectUri` that is not an `https:` string before sending, and sends the browser to `/signin/authorize`. `completeSignIn({ clientId, storage, url })` on the callback page checks `state` and the page, takes `code` and `state` out of the address bar, trades the code once at `POST /api/v2/auth/token` with no credential, and returns `OwSignInResult` (`token`, `account`, `resume`); the record is cleared either way and the SDK keeps no token. `OwSignInError` gains `invalid_grant` and `state_mismatch`. `pkceChallenge` and `base64url` exported.
|
|
12
|
+
|
|
8
13
|
## [4.45.0] - 2026-10-22
|
|
9
14
|
|
|
10
15
|
### Added
|
package/dist/index.d.ts
CHANGED
|
@@ -2898,7 +2898,7 @@ declare class OwNetworkError extends Error {
|
|
|
2898
2898
|
* - `mint_failed`: the password was right, but keel made no token; the session was still ended.
|
|
2899
2899
|
* - `failed`: any other refusal; the message is keel's when it gave one.
|
|
2900
2900
|
*/
|
|
2901
|
-
type OwSignInReason = 'invalid_credentials' | 'email_unverified' | 'rate_limited' | 'offline' | 'mint_failed' | 'failed';
|
|
2901
|
+
type OwSignInReason = 'invalid_credentials' | 'invalid_grant' | 'state_mismatch' | 'email_unverified' | 'rate_limited' | 'offline' | 'mint_failed' | 'failed';
|
|
2902
2902
|
/**
|
|
2903
2903
|
* A refused `signInWithPassword`. `reason` is what to tell the user; `status` and `code` are
|
|
2904
2904
|
* keel's answer when there was one (`code`: keel's error code, e.g. `email_password_mismatch`).
|
|
@@ -2958,6 +2958,65 @@ declare function kindCanWrite(kind: OwKeyKind): boolean;
|
|
|
2958
2958
|
*/
|
|
2959
2959
|
declare function pinExpectation(kind: OwKeyKind): 'never' | 'optional' | 'required-for-private-reads';
|
|
2960
2960
|
|
|
2961
|
+
/**
|
|
2962
|
+
* Sign in with OnlyWorlds: OAuth 2.1's authorization code with PKCE (S256) for public clients
|
|
2963
|
+
* (keel PR 154, live 2026-10-22; the handoff sheet, Orrery `keel/signin-handoff.md` §3).
|
|
2964
|
+
*
|
|
2965
|
+
* Two calls. `beginSignIn` mints `state` and a PKCE verifier, keeps them under the client id in a
|
|
2966
|
+
* storage the page shares with its callback (sessionStorage by default), and hands back the
|
|
2967
|
+
* authorize URL (sending the browser there where a `location` exists). `completeSignIn` runs on the
|
|
2968
|
+
* callback page: it checks `state` and that the page is the registered return address, takes the
|
|
2969
|
+
* code out of the address bar, trades it at `POST /api/v2/auth/token` with no credential, and
|
|
2970
|
+
* returns the token with the account and the `resume` the tool parked before the redirect. The
|
|
2971
|
+
* SDK keeps nothing: the record is cleared on success and on refusal, and the token is the tool's
|
|
2972
|
+
* to store.
|
|
2973
|
+
*/
|
|
2974
|
+
/** Where the pending sign-in's record lives between the two pages: string in, string out. */
|
|
2975
|
+
interface OwSignInStorage {
|
|
2976
|
+
get(key: string): string | null | undefined;
|
|
2977
|
+
set(key: string, value: string): void;
|
|
2978
|
+
remove(key: string): void;
|
|
2979
|
+
}
|
|
2980
|
+
interface OwSignInBegin {
|
|
2981
|
+
/** The `client_id` keel's registry lists the tool under (`GET /api/v2/auth/clients`). */
|
|
2982
|
+
clientId: string;
|
|
2983
|
+
/** The registered return address, the exact string, `https:` only; never built from `location.origin`. */
|
|
2984
|
+
redirectUri: string;
|
|
2985
|
+
/** Small JSON the tool wants back after the redirect (an open link, a pending join code). */
|
|
2986
|
+
resume?: unknown;
|
|
2987
|
+
/** Defaults to `globalThis.sessionStorage`; without one (Node) the option is required. */
|
|
2988
|
+
storage?: OwSignInStorage;
|
|
2989
|
+
/** How the browser is sent: a function taking the URL, or `false` to only return it. Default: `location.assign` where there is one. */
|
|
2990
|
+
navigate?: ((url: string) => void) | false;
|
|
2991
|
+
}
|
|
2992
|
+
interface OwSignInBegun {
|
|
2993
|
+
/** The authorize URL: `https://www.onlyworlds.com/signin/authorize?...`. */
|
|
2994
|
+
url: string;
|
|
2995
|
+
state: string;
|
|
2996
|
+
}
|
|
2997
|
+
interface OwSignInComplete {
|
|
2998
|
+
clientId: string;
|
|
2999
|
+
storage?: OwSignInStorage;
|
|
3000
|
+
/** The callback page's address; defaults to `location.href`. */
|
|
3001
|
+
url?: string;
|
|
3002
|
+
}
|
|
3003
|
+
/** What `completeSignIn` returns: the account token, shown only here, and what the tool parked. */
|
|
3004
|
+
interface OwSignInResult {
|
|
3005
|
+
/** An `ow_a_` account token: keep it, it is the only copy (the SDK stores none). */
|
|
3006
|
+
token: string;
|
|
3007
|
+
tokenType: 'bearer';
|
|
3008
|
+
account: {
|
|
3009
|
+
id: string;
|
|
3010
|
+
username: string;
|
|
3011
|
+
};
|
|
3012
|
+
/** The `resume` given to `beginSignIn`, or null. */
|
|
3013
|
+
resume: unknown;
|
|
3014
|
+
}
|
|
3015
|
+
/** Base64url without padding (RFC 4648 §5), as PKCE wants it. */
|
|
3016
|
+
declare function base64url(bytes: Uint8Array): string;
|
|
3017
|
+
/** The S256 code challenge of a verifier (RFC 7636 §4.2). */
|
|
3018
|
+
declare function pkceChallenge(verifier: string): Promise<string>;
|
|
3019
|
+
|
|
2961
3020
|
/**
|
|
2962
3021
|
* keel v2 engine absorbed from an earlier v2 client (ow-v2-client v0.9.0),
|
|
2963
3022
|
* wire-corrected against live staging fixtures 2026-07-18.
|
|
@@ -3486,6 +3545,26 @@ declare class OwV2Client {
|
|
|
3486
3545
|
scope?: 'read' | 'write';
|
|
3487
3546
|
name?: string;
|
|
3488
3547
|
}): Promise<OwMintedKey>;
|
|
3548
|
+
/**
|
|
3549
|
+
* Start Sign in with OnlyWorlds from a tool's page: mint `state` and a PKCE verifier, keep them
|
|
3550
|
+
* under `clientId` in `storage` (sessionStorage by default; in Node pass one), and send the
|
|
3551
|
+
* browser to `https://www.onlyworlds.com/signin/authorize` (or only return the URL with
|
|
3552
|
+
* `navigate: false`). `redirectUri` is the registered return address as a string, `https:`
|
|
3553
|
+
* only, never built from `location.origin`: a tool's registration is read at
|
|
3554
|
+
* `GET /api/v2/auth/clients`. `resume` is small JSON handed back by `completeSignIn`. No request
|
|
3555
|
+
* is sent; any client, keyless included. The handoff sheet: Orrery `keel/signin-handoff.md` §3.
|
|
3556
|
+
*/
|
|
3557
|
+
signInWithOnlyWorlds(opts: OwSignInBegin): Promise<OwSignInBegun>;
|
|
3558
|
+
/**
|
|
3559
|
+
* Finish Sign in with OnlyWorlds on the callback page: check `state` against the record and the
|
|
3560
|
+
* page against the registered address, take `code` and `state` out of the address bar, trade
|
|
3561
|
+
* the code once at `POST /auth/token` with the PKCE verifier and no credential, and return the
|
|
3562
|
+
* `ow_a_` token with the account and `resume`. The record is cleared on success and on refusal;
|
|
3563
|
+
* the SDK keeps no token: the tool does. Refusals are `OwSignInError`: `state_mismatch` (no
|
|
3564
|
+
* pending sign-in, a different state, or the wrong page; nothing sent), `invalid_grant` (keel's
|
|
3565
|
+
* 400: used, expired, or another client's code), `rate_limited`, `offline`, `failed`.
|
|
3566
|
+
*/
|
|
3567
|
+
completeSignIn(opts: OwSignInComplete): Promise<OwSignInResult>;
|
|
3489
3568
|
/**
|
|
3490
3569
|
* POST /account/ask -- a question about a world the account owns or is in, answered by
|
|
3491
3570
|
* keel's AI proxy from `context` only (build it with buildAskContext from the elements
|
|
@@ -4528,4 +4607,4 @@ declare class TokenResource {
|
|
|
4528
4607
|
getEncryptionInfo(): Promise<EncryptionInfo>;
|
|
4529
4608
|
}
|
|
4530
4609
|
|
|
4531
|
-
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_ACRONYMS, FIELD_SCHEMA, FILE_EXTENSIONS, type FieldInfo, type FieldType, GameTier, type ListParams, ONLYWORLDS_VERSION, type OwAccountWorld, 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 OwDeleteReason, type OwElement, type OwElementBase, type OwErrorBody, type OwFileAddedBy, type OwFileDownload, type OwFileKind, type OwFileRow, type OwFileTicket, 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, type OwMintedKey, OwNetworkError, type OwPage, type OwPhotoOptions, type OwPhotoPlan, type OwPlayerJoined, OwPreconditionFailedError, type OwPreparedPhoto, type OwReadWorldFile, type OwReadWorldFileOptions, type OwRemovalTicket, type OwRemovedImage, type OwRenderOptions, type OwResyncReason, 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 OwUploadedFile, type OwUploadedImage, OwV2Client, type OwWorldFileEntry, OwWorldFileError, type OwWorldFileReason, type OwWorldFileRow, type OwWorldFiles, type OwWorldIndex, type OwWorldLink, type OwWorldMeta, type OwWriteDoor, PHOTO_MAX_BYTES, PHOTO_TYPES, PUBLIC_FIELDS, 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, fieldLabel, fileKindOf, floorDiv, floorMod, formatDuration, formatMidLabel, formatStep, formatYearLabel, getElementIcon, getElementLabel, isAgentDoor, isDemoKey, isKnownOf, isLeapYear, isParseError, kindCanWrite, lookupWorld, monthStartSteps, parseDate, parseErrorEnvelope, pinExpectation, planPhoto, preparePhoto, readCalendarConfig, readCalendarValue, readWorldFile, renderElement, resolveCalendarLens, resolveEraDomains, validateCalendarConfig, weekdayForStep, worldCalendars, writeWorldFile, yearLength, yearStartStep };
|
|
4610
|
+
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_ACRONYMS, FIELD_SCHEMA, FILE_EXTENSIONS, type FieldInfo, type FieldType, GameTier, type ListParams, ONLYWORLDS_VERSION, type OwAccountWorld, 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 OwDeleteReason, type OwElement, type OwElementBase, type OwErrorBody, type OwFileAddedBy, type OwFileDownload, type OwFileKind, type OwFileRow, type OwFileTicket, 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, type OwMintedKey, OwNetworkError, type OwPage, type OwPhotoOptions, type OwPhotoPlan, type OwPlayerJoined, OwPreconditionFailedError, type OwPreparedPhoto, type OwReadWorldFile, type OwReadWorldFileOptions, type OwRemovalTicket, type OwRemovedImage, type OwRenderOptions, type OwResyncReason, type OwSeat, type OwSeatCreated, type OwSignInBegin, type OwSignInBegun, type OwSignInComplete, OwSignInError, type OwSignInReason, type OwSignInResult, type OwSignInStorage, type OwSignedIn, type OwSortAddToRow, type OwSortAnswer, type OwSortAskRow, type OwSortLinkRow, type OwSortMakeRow, type OwSortNote, type OwSortRef, type OwSortRow, type OwUploadedFile, type OwUploadedImage, OwV2Client, type OwWorldFileEntry, OwWorldFileError, type OwWorldFileReason, type OwWorldFileRow, type OwWorldFiles, type OwWorldIndex, type OwWorldLink, type OwWorldMeta, type OwWriteDoor, PHOTO_MAX_BYTES, PHOTO_TYPES, PUBLIC_FIELDS, 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, base64url, breakdownStep, buildAskContext, buildWorldIndex, detectKeyKind, displayYear, elementColor, eraForStep, errorFromResponse, familyOf, fieldLabel, fileKindOf, floorDiv, floorMod, formatDuration, formatMidLabel, formatStep, formatYearLabel, getElementIcon, getElementLabel, isAgentDoor, isDemoKey, isKnownOf, isLeapYear, isParseError, kindCanWrite, lookupWorld, monthStartSteps, parseDate, parseErrorEnvelope, pinExpectation, pkceChallenge, planPhoto, preparePhoto, readCalendarConfig, readCalendarValue, readWorldFile, renderElement, resolveCalendarLens, resolveEraDomains, validateCalendarConfig, weekdayForStep, worldCalendars, writeWorldFile, yearLength, yearStartStep };
|
package/dist/index.js
CHANGED
|
@@ -338,6 +338,159 @@ function pinExpectation(kind) {
|
|
|
338
338
|
}
|
|
339
339
|
}
|
|
340
340
|
|
|
341
|
+
// src/v2/oauth.ts
|
|
342
|
+
var RECORD_PREFIX = "ow_signin:";
|
|
343
|
+
function defaultStorage(name) {
|
|
344
|
+
const ss = globalThis.sessionStorage;
|
|
345
|
+
if (!ss || typeof ss.getItem !== "function") {
|
|
346
|
+
throw new TypeError(`${name}: no sessionStorage here; pass storage ({get, set, remove}) to hold the pending sign-in`);
|
|
347
|
+
}
|
|
348
|
+
return { get: (k) => ss.getItem(k), set: (k, v) => ss.setItem(k, v), remove: (k) => ss.removeItem(k) };
|
|
349
|
+
}
|
|
350
|
+
function webCrypto() {
|
|
351
|
+
const c = globalThis.crypto;
|
|
352
|
+
if (!c || typeof c.getRandomValues !== "function" || !c.subtle) {
|
|
353
|
+
throw new TypeError("Sign in with OnlyWorlds needs Web Crypto (crypto.getRandomValues and crypto.subtle)");
|
|
354
|
+
}
|
|
355
|
+
return c;
|
|
356
|
+
}
|
|
357
|
+
function base64url(bytes) {
|
|
358
|
+
const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
359
|
+
let out = "";
|
|
360
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
361
|
+
const b0 = bytes[i], b1 = bytes[i + 1], b2 = bytes[i + 2];
|
|
362
|
+
const n = b0 << 16 | (b1 ?? 0) << 8 | (b2 ?? 0);
|
|
363
|
+
out += ALPHABET[n >> 18 & 63] + ALPHABET[n >> 12 & 63];
|
|
364
|
+
if (b1 !== void 0) out += ALPHABET[n >> 6 & 63];
|
|
365
|
+
if (b2 !== void 0) out += ALPHABET[n & 63];
|
|
366
|
+
}
|
|
367
|
+
return out;
|
|
368
|
+
}
|
|
369
|
+
async function pkceChallenge(verifier) {
|
|
370
|
+
const digest = await webCrypto().subtle.digest("SHA-256", new TextEncoder().encode(verifier));
|
|
371
|
+
return base64url(new Uint8Array(digest));
|
|
372
|
+
}
|
|
373
|
+
function randomToken(bytes) {
|
|
374
|
+
const buf = new Uint8Array(bytes);
|
|
375
|
+
webCrypto().getRandomValues(buf);
|
|
376
|
+
return base64url(buf);
|
|
377
|
+
}
|
|
378
|
+
function sitePage(baseUrl, path) {
|
|
379
|
+
return `${new URL(baseUrl).origin}${path}`;
|
|
380
|
+
}
|
|
381
|
+
async function beginSignIn(baseUrl, opts) {
|
|
382
|
+
const { clientId, redirectUri, resume = null } = opts ?? {};
|
|
383
|
+
if (typeof clientId !== "string" || clientId === "") throw new TypeError("signInWithOnlyWorlds needs clientId");
|
|
384
|
+
if (typeof redirectUri !== "string" || !redirectUri.startsWith("https://")) {
|
|
385
|
+
throw new TypeError("signInWithOnlyWorlds: redirectUri must be the registered https: address, as a string");
|
|
386
|
+
}
|
|
387
|
+
const storage = opts.storage ?? defaultStorage("signInWithOnlyWorlds");
|
|
388
|
+
const state = randomToken(32);
|
|
389
|
+
const verifier = randomToken(32);
|
|
390
|
+
const challenge = await pkceChallenge(verifier);
|
|
391
|
+
storage.set(RECORD_PREFIX + clientId, JSON.stringify({ state, verifier, redirectUri, resume }));
|
|
392
|
+
const url = new URL(sitePage(baseUrl, "/signin/authorize"));
|
|
393
|
+
url.searchParams.set("client_id", clientId);
|
|
394
|
+
url.searchParams.set("redirect_uri", redirectUri);
|
|
395
|
+
url.searchParams.set("state", state);
|
|
396
|
+
url.searchParams.set("code_challenge", challenge);
|
|
397
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
398
|
+
const href = url.toString();
|
|
399
|
+
if (opts.navigate !== false) {
|
|
400
|
+
const go = opts.navigate ?? globalThis.location?.assign?.bind(globalThis.location);
|
|
401
|
+
if (go) go(href);
|
|
402
|
+
}
|
|
403
|
+
return { url: href, state };
|
|
404
|
+
}
|
|
405
|
+
function samePage(a, b) {
|
|
406
|
+
try {
|
|
407
|
+
const x = new URL(a), y = new URL(b);
|
|
408
|
+
return x.origin === y.origin && x.pathname.replace(/\/$/, "") === y.pathname.replace(/\/$/, "");
|
|
409
|
+
} catch {
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
async function completeSignIn(baseUrl, fetchImpl, opts) {
|
|
414
|
+
const { clientId } = opts ?? {};
|
|
415
|
+
if (typeof clientId !== "string" || clientId === "") throw new TypeError("completeSignIn needs clientId");
|
|
416
|
+
const storage = opts.storage ?? defaultStorage("completeSignIn");
|
|
417
|
+
const here = opts.url ?? globalThis.location?.href;
|
|
418
|
+
if (typeof here !== "string") throw new TypeError("completeSignIn: no location here; pass url (the callback page's address)");
|
|
419
|
+
const page = new URL(here);
|
|
420
|
+
const code = page.searchParams.get("code");
|
|
421
|
+
const state = page.searchParams.get("state");
|
|
422
|
+
const hist = globalThis.history;
|
|
423
|
+
if (hist && typeof hist.replaceState === "function" && (page.searchParams.has("code") || page.searchParams.has("state"))) {
|
|
424
|
+
const clean = new URL(here);
|
|
425
|
+
clean.searchParams.delete("code");
|
|
426
|
+
clean.searchParams.delete("state");
|
|
427
|
+
try {
|
|
428
|
+
hist.replaceState(hist.state, "", clean.toString());
|
|
429
|
+
} catch {
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
const key = RECORD_PREFIX + clientId;
|
|
433
|
+
const raw = storage.get(key);
|
|
434
|
+
storage.remove(key);
|
|
435
|
+
let record = null;
|
|
436
|
+
try {
|
|
437
|
+
record = raw ? JSON.parse(raw) : null;
|
|
438
|
+
} catch {
|
|
439
|
+
record = null;
|
|
440
|
+
}
|
|
441
|
+
if (!record || typeof record.state !== "string" || typeof record.verifier !== "string" || typeof record.redirectUri !== "string") {
|
|
442
|
+
throw new OwSignInError("state_mismatch", "No sign-in is pending for this tool here: start again from the sign-in button.");
|
|
443
|
+
}
|
|
444
|
+
if (!code || !state || state !== record.state) {
|
|
445
|
+
throw new OwSignInError("state_mismatch", "This page was not reached from the sign-in that was started here: start again.");
|
|
446
|
+
}
|
|
447
|
+
if (!samePage(here, record.redirectUri)) {
|
|
448
|
+
throw new OwSignInError("state_mismatch", "This page is not the return address the sign-in was started with.");
|
|
449
|
+
}
|
|
450
|
+
let res;
|
|
451
|
+
try {
|
|
452
|
+
res = await fetchImpl(`${baseUrl}/auth/token`, {
|
|
453
|
+
method: "POST",
|
|
454
|
+
headers: { "Content-Type": "application/json" },
|
|
455
|
+
body: JSON.stringify({
|
|
456
|
+
grant_type: "authorization_code",
|
|
457
|
+
code,
|
|
458
|
+
code_verifier: record.verifier,
|
|
459
|
+
client_id: clientId,
|
|
460
|
+
redirect_uri: record.redirectUri
|
|
461
|
+
})
|
|
462
|
+
});
|
|
463
|
+
} catch {
|
|
464
|
+
throw new OwSignInError("offline", "OnlyWorlds could not be reached: signing in needs a connection.");
|
|
465
|
+
}
|
|
466
|
+
const retryAfter = parseRetryAfter(res.headers?.get?.("Retry-After") ?? null) ?? void 0;
|
|
467
|
+
let body = null;
|
|
468
|
+
try {
|
|
469
|
+
body = await res.json();
|
|
470
|
+
} catch {
|
|
471
|
+
body = null;
|
|
472
|
+
}
|
|
473
|
+
const err = body?.error;
|
|
474
|
+
const errCode = typeof err?.code === "string" ? err.code : void 0;
|
|
475
|
+
const errMessage = typeof err?.message === "string" ? err.message : void 0;
|
|
476
|
+
if (res.status === 429) {
|
|
477
|
+
throw new OwSignInError("rate_limited", errMessage ?? "Too many sign-in attempts. Wait a while, then try again.", { status: 429, code: errCode, retryAfter });
|
|
478
|
+
}
|
|
479
|
+
if (res.status === 400 && errCode === "invalid_grant") {
|
|
480
|
+
throw new OwSignInError("invalid_grant", errMessage ?? "The sign-in code could not be exchanged: it was used, expired, or made for another tool. Start again.", { status: 400, code: errCode });
|
|
481
|
+
}
|
|
482
|
+
const ok = body;
|
|
483
|
+
if (!res.ok || typeof ok?.token !== "string" || ok.token === "" || typeof ok.account?.id !== "string") {
|
|
484
|
+
throw new OwSignInError("failed", errMessage ?? `Signing in did not work (${res.status}).`, { status: res.status, code: errCode });
|
|
485
|
+
}
|
|
486
|
+
return {
|
|
487
|
+
token: ok.token,
|
|
488
|
+
tokenType: "bearer",
|
|
489
|
+
account: { id: ok.account.id, username: typeof ok.account.username === "string" ? ok.account.username : "" },
|
|
490
|
+
resume: record.resume ?? null
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
341
494
|
// src/v2/client.ts
|
|
342
495
|
var DEFAULT_BASE_URL = "https://www.onlyworlds.com/api/v2";
|
|
343
496
|
var DEFAULT_PAGE_SIZE = 100;
|
|
@@ -1177,6 +1330,31 @@ var OwV2Client = class {
|
|
|
1177
1330
|
if (opts.name !== void 0) body.name = opts.name;
|
|
1178
1331
|
return this.request("POST", `/account/worlds/${encodeURIComponent(worldId)}/keys`, { write: true, body });
|
|
1179
1332
|
}
|
|
1333
|
+
// -- Sign in with OnlyWorlds (OAuth 2.1 code + PKCE, keel PR 154) ---------
|
|
1334
|
+
/**
|
|
1335
|
+
* Start Sign in with OnlyWorlds from a tool's page: mint `state` and a PKCE verifier, keep them
|
|
1336
|
+
* under `clientId` in `storage` (sessionStorage by default; in Node pass one), and send the
|
|
1337
|
+
* browser to `https://www.onlyworlds.com/signin/authorize` (or only return the URL with
|
|
1338
|
+
* `navigate: false`). `redirectUri` is the registered return address as a string, `https:`
|
|
1339
|
+
* only, never built from `location.origin`: a tool's registration is read at
|
|
1340
|
+
* `GET /api/v2/auth/clients`. `resume` is small JSON handed back by `completeSignIn`. No request
|
|
1341
|
+
* is sent; any client, keyless included. The handoff sheet: Orrery `keel/signin-handoff.md` §3.
|
|
1342
|
+
*/
|
|
1343
|
+
async signInWithOnlyWorlds(opts) {
|
|
1344
|
+
return beginSignIn(this.baseUrl, opts);
|
|
1345
|
+
}
|
|
1346
|
+
/**
|
|
1347
|
+
* Finish Sign in with OnlyWorlds on the callback page: check `state` against the record and the
|
|
1348
|
+
* page against the registered address, take `code` and `state` out of the address bar, trade
|
|
1349
|
+
* the code once at `POST /auth/token` with the PKCE verifier and no credential, and return the
|
|
1350
|
+
* `ow_a_` token with the account and `resume`. The record is cleared on success and on refusal;
|
|
1351
|
+
* the SDK keeps no token: the tool does. Refusals are `OwSignInError`: `state_mismatch` (no
|
|
1352
|
+
* pending sign-in, a different state, or the wrong page; nothing sent), `invalid_grant` (keel's
|
|
1353
|
+
* 400: used, expired, or another client's code), `rate_limited`, `offline`, `failed`.
|
|
1354
|
+
*/
|
|
1355
|
+
async completeSignIn(opts) {
|
|
1356
|
+
return completeSignIn(this.baseUrl, this.fetchImpl, opts);
|
|
1357
|
+
}
|
|
1180
1358
|
// -- Ask your world (keel #159) -------------------------------------------
|
|
1181
1359
|
/**
|
|
1182
1360
|
* POST /account/ask -- a question about a world the account owns or is in, answered by
|
|
@@ -3676,6 +3854,7 @@ export {
|
|
|
3676
3854
|
absYearForEraYear,
|
|
3677
3855
|
absoluteYear,
|
|
3678
3856
|
approxYearLength,
|
|
3857
|
+
base64url,
|
|
3679
3858
|
breakdownStep,
|
|
3680
3859
|
buildAskContext,
|
|
3681
3860
|
buildWorldIndex,
|
|
@@ -3706,6 +3885,7 @@ export {
|
|
|
3706
3885
|
parseDate,
|
|
3707
3886
|
parseErrorEnvelope,
|
|
3708
3887
|
pinExpectation,
|
|
3888
|
+
pkceChallenge,
|
|
3709
3889
|
planPhoto,
|
|
3710
3890
|
preparePhoto,
|
|
3711
3891
|
readCalendarConfig,
|