@onlyworlds/sdk 4.44.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 CHANGED
@@ -5,6 +5,17 @@ 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
+
13
+ ## [4.45.0] - 2026-10-22
14
+
15
+ ### Added
16
+ - `hide(type, id)` and `unhide(type, id)`: the hidden mark's writes, `PUT` and `DELETE /{type}/{id}/hidden` (keel #421), for the parlor (Temper, h66 #9720). 204; the element's body and `change_seq` do not move.
17
+ - `listAccountWorlds()` (`GET /account/worlds`, `OwAccountWorld[]`: owned and shared worlds with their keys by name and last four) and `mintWorldKey(worldId, { scope, name })` (`POST /account/worlds/{id}/keys`, `OwMintedKey` with the full key once), the two account routes Atlas and the parlor hand-rolled. Account token only.
18
+
8
19
  ## [4.44.0] - 2026-10-22
9
20
 
10
21
  ### Added
package/dist/index.d.ts CHANGED
@@ -2426,6 +2426,39 @@ interface OwSignedIn {
2426
2426
  last4: string;
2427
2427
  }
2428
2428
  /** One live agent seat, as GET /account/worlds/{id}/seats lists it (keel #355, D106). No key or secret. */
2429
+ /**
2430
+ * One world of the account, from GET /account/worlds: owned (`role: 'owner'`) or shared
2431
+ * (`'collaborator'`, with `member_role` the membership's role). Keys by name and last four only,
2432
+ * never the key; on a shared world only the member's own keys (keel H8).
2433
+ */
2434
+ interface OwAccountWorld {
2435
+ id: string;
2436
+ name: string;
2437
+ role: 'owner' | 'collaborator';
2438
+ member_role?: OwMemberRole;
2439
+ public_read: boolean;
2440
+ keys: Array<{
2441
+ name: string;
2442
+ last4: string;
2443
+ scope: 'READ' | 'WRITE';
2444
+ revoked: boolean;
2445
+ }>;
2446
+ }
2447
+ /**
2448
+ * A world key as minted by POST /account/worlds/{id}/keys: the row the account's key list shows,
2449
+ * plus the full key once in `key` (never shown again). A member's key carries its `member` label
2450
+ * (keel D72); an owner's row has no such field.
2451
+ */
2452
+ interface OwMintedKey {
2453
+ id: string;
2454
+ name: string;
2455
+ last4: string;
2456
+ scope: 'READ' | 'WRITE';
2457
+ last_used_at: string | null;
2458
+ revoked: boolean;
2459
+ key: string;
2460
+ member?: Record<string, unknown>;
2461
+ }
2429
2462
  interface OwSeat {
2430
2463
  /** The seat's membership id: removeSeat takes it, and GET /members names the seat by it. */
2431
2464
  id: string;
@@ -2865,7 +2898,7 @@ declare class OwNetworkError extends Error {
2865
2898
  * - `mint_failed`: the password was right, but keel made no token; the session was still ended.
2866
2899
  * - `failed`: any other refusal; the message is keel's when it gave one.
2867
2900
  */
2868
- 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';
2869
2902
  /**
2870
2903
  * A refused `signInWithPassword`. `reason` is what to tell the user; `status` and `code` are
2871
2904
  * keel's answer when there was one (`code`: keel's error code, e.g. `email_password_mismatch`).
@@ -2925,6 +2958,65 @@ declare function kindCanWrite(kind: OwKeyKind): boolean;
2925
2958
  */
2926
2959
  declare function pinExpectation(kind: OwKeyKind): 'never' | 'optional' | 'required-for-private-reads';
2927
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
+
2928
3020
  /**
2929
3021
  * keel v2 engine absorbed from an earlier v2 client (ow-v2-client v0.9.0),
2930
3022
  * wire-corrected against live staging fixtures 2026-07-18.
@@ -3427,6 +3519,52 @@ declare class OwV2Client {
3427
3519
  * goes with it, any other stays. Needs the client's `ow_a_` token.
3428
3520
  */
3429
3521
  removeSeat(worldId: string, seatId: string): Promise<void>;
3522
+ /**
3523
+ * PUT /{type}/{id}/hidden -- hide an element (keel #421). The owner's key or a co-builder's
3524
+ * (a person's or a seat's) with its PIN; hiding a hidden element again changes nothing, and
3525
+ * the element's body and `change_seq` do not move. Hidden, it is seen only by the owner, the
3526
+ * co-builders and the member who created it; to every other key it is a 404, left out of
3527
+ * lists, search, counts and the link arrays that name it. 204, no body. No Idempotency-Key.
3528
+ */
3529
+ hide(type: ElementType | string, id: string): Promise<void>;
3530
+ /** DELETE /{type}/{id}/hidden -- clear the mark; an element that is not hidden stays as it is. 204. */
3531
+ unhide(type: ElementType | string, id: string): Promise<void>;
3532
+ /**
3533
+ * GET /account/worlds -- every world the account owns or is a member of, with its keys by
3534
+ * name and last four (a member sees only their own keys on a shared world). Account token only.
3535
+ */
3536
+ listAccountWorlds(): Promise<OwAccountWorld[]>;
3537
+ /**
3538
+ * POST /account/worlds/{id}/keys -- mint a world key, `scope` `'write'` (the default) or
3539
+ * `'read'`, `name` as the key list shows it (at most 255 characters). The full key comes back
3540
+ * once in `key`. The owner mints a world key; a member mints their own member key, 409
3541
+ * `pin_required` (`err.isPinRequired`) for a write key while the account has no PIN. Account
3542
+ * token only; no Idempotency-Key.
3543
+ */
3544
+ mintWorldKey(worldId: string, opts?: {
3545
+ scope?: 'read' | 'write';
3546
+ name?: string;
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>;
3430
3568
  /**
3431
3569
  * POST /account/ask -- a question about a world the account owns or is in, answered by
3432
3570
  * keel's AI proxy from `context` only (build it with buildAskContext from the elements
@@ -4469,4 +4607,4 @@ declare class TokenResource {
4469
4607
  getEncryptionInfo(): Promise<EncryptionInfo>;
4470
4608
  }
4471
4609
 
4472
- 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 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, 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;
@@ -1134,6 +1287,74 @@ var OwV2Client = class {
1134
1287
  }
1135
1288
  await this.request("DELETE", `/account/worlds/${encodeURIComponent(worldId)}/seats/${encodeURIComponent(seatId)}`);
1136
1289
  }
1290
+ // -- The hidden mark (keel #421) ------------------------------------------
1291
+ /**
1292
+ * PUT /{type}/{id}/hidden -- hide an element (keel #421). The owner's key or a co-builder's
1293
+ * (a person's or a seat's) with its PIN; hiding a hidden element again changes nothing, and
1294
+ * the element's body and `change_seq` do not move. Hidden, it is seen only by the owner, the
1295
+ * co-builders and the member who created it; to every other key it is a 404, left out of
1296
+ * lists, search, counts and the link arrays that name it. 204, no body. No Idempotency-Key.
1297
+ */
1298
+ async hide(type, id) {
1299
+ await this.request("PUT", `/${type}/${id}/hidden`, { write: true, body: {}, allowEmpty: true });
1300
+ }
1301
+ /** DELETE /{type}/{id}/hidden -- clear the mark; an element that is not hidden stays as it is. 204. */
1302
+ async unhide(type, id) {
1303
+ await this.request("DELETE", `/${type}/${id}/hidden`, { write: true, allowEmpty: true });
1304
+ }
1305
+ // -- The account's worlds and keys (account token) -----------------------
1306
+ /**
1307
+ * GET /account/worlds -- every world the account owns or is a member of, with its keys by
1308
+ * name and last four (a member sees only their own keys on a shared world). Account token only.
1309
+ */
1310
+ async listAccountWorlds() {
1311
+ if (this.keyKind !== "account") {
1312
+ throw new TypeError("listAccountWorlds needs an account token (ow_a_)");
1313
+ }
1314
+ const page = await this.request("GET", "/account/worlds");
1315
+ return page.data;
1316
+ }
1317
+ /**
1318
+ * POST /account/worlds/{id}/keys -- mint a world key, `scope` `'write'` (the default) or
1319
+ * `'read'`, `name` as the key list shows it (at most 255 characters). The full key comes back
1320
+ * once in `key`. The owner mints a world key; a member mints their own member key, 409
1321
+ * `pin_required` (`err.isPinRequired`) for a write key while the account has no PIN. Account
1322
+ * token only; no Idempotency-Key.
1323
+ */
1324
+ async mintWorldKey(worldId, opts = {}) {
1325
+ if (this.keyKind !== "account") {
1326
+ throw new TypeError("mintWorldKey needs an account token (ow_a_)");
1327
+ }
1328
+ const body = {};
1329
+ if (opts.scope !== void 0) body.scope = opts.scope;
1330
+ if (opts.name !== void 0) body.name = opts.name;
1331
+ return this.request("POST", `/account/worlds/${encodeURIComponent(worldId)}/keys`, { write: true, body });
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
+ }
1137
1358
  // -- Ask your world (keel #159) -------------------------------------------
1138
1359
  /**
1139
1360
  * POST /account/ask -- a question about a world the account owns or is in, answered by
@@ -3633,6 +3854,7 @@ export {
3633
3854
  absYearForEraYear,
3634
3855
  absoluteYear,
3635
3856
  approxYearLength,
3857
+ base64url,
3636
3858
  breakdownStep,
3637
3859
  buildAskContext,
3638
3860
  buildWorldIndex,
@@ -3663,6 +3885,7 @@ export {
3663
3885
  parseDate,
3664
3886
  parseErrorEnvelope,
3665
3887
  pinExpectation,
3888
+ pkceChallenge,
3666
3889
  planPhoto,
3667
3890
  preparePhoto,
3668
3891
  readCalendarConfig,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlyworlds/sdk",
3
- "version": "4.44.0",
3
+ "version": "4.46.0",
4
4
  "description": "TypeScript SDK for the OnlyWorlds API - build world-building applications with type safety",
5
5
  "type": "module",
6
6
  "exports": {