@gusnips/react 0.9.29 → 0.9.31

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/README.md CHANGED
@@ -696,10 +696,19 @@ your scripts signs in on a machine with no browser, it prints a short code, and
696
696
  it on your device page. `@gusnips/server/oauth` is the server half. These two hooks are the
697
697
  logic for the two pages. The pages themselves are yours.
698
698
 
699
+ ```ts
700
+ import { createOAuthConsoleApi } from "@gusnips/react/oauth";
701
+
702
+ export const oauthApi = createOAuthConsoleApi(api, "/auth/oauth");
703
+ ```
704
+
705
+ That is both pages' calls to the server's `oauthConsoleRoutes`, mounted at `/auth/oauth` behind
706
+ your sign-in. `api` is your `createApiClient` client, or any object with its `get` and `post`.
707
+
699
708
  ```tsx
700
709
  import { useConsentRequest } from "@gusnips/react/oauth";
701
710
 
702
- const consent = useConsentRequest(api.oauth, useLocation().search);
711
+ const consent = useConsentRequest(oauthApi.consent, useLocation().search);
703
712
 
704
713
  if (consent.state.status === "ready")
705
714
  return (
@@ -709,9 +718,11 @@ if (consent.state.status === "ready")
709
718
  );
710
719
  ```
711
720
 
712
- `api.oauth` is two calls to your own API: `describe(query)` and `approve(query, choice)`. They
713
- reach the server's `describe` and `approve` through routes behind your sign-in. `state.status` is
714
- `loading`, `refused`, `failed` or `ready`:
721
+ If your routes are your own, pass your own two calls instead: `describe(query)` and
722
+ `approve(query, choice)`. The choice is anything your page adds, such as a workspace, and it reaches
723
+ the server's approver. With no choice, call `approve()`. When your server adds fields to the view,
724
+ type them as the second parameter, `ConsentApi<Choice, { spaces: Space[] }>`, and
725
+ `state.view.spaces` is typed too. `state.status` is `loading`, `refused`, `failed` or `ready`:
715
726
 
716
727
  - **`refused`** means the request is not one to answer: an unknown app, or a return address it
717
728
  never registered. Show `refusal.message` (or your own words for `refusal.reason`) and a link
@@ -726,13 +737,23 @@ for a second press. Approve sends the browser on with `window.location.assign`,
726
737
  an app's own scheme such as `cursor://`.
727
738
 
728
739
  ```tsx
729
- import { useDeviceApproval } from "@gusnips/react/oauth";
740
+ import { useDeviceApproval, useDeviceCodeField } from "@gusnips/react/oauth";
730
741
 
731
- const device = useDeviceApproval(api.device, useLocation().search);
742
+ const device = useDeviceApproval(oauthApi.device, useLocation().search);
743
+ const field = useDeviceCodeField(device);
732
744
 
733
- <input value={device.code} onChange={(event) => device.setCode(event.target.value)} />;
745
+ <label htmlFor={field.inputProps.id}>Code</label>
746
+ <input {...field.inputProps} />
747
+ <p {...field.statusProps}>{device.invalidLetter ? "Codes have no vowels." : null}</p>
734
748
  ```
735
749
 
750
+ `useDeviceCodeField` is the field's wiring: the input's id, value and change handler, the
751
+ attributes a typed code needs (no autocomplete, capitals, no spell check), `aria-invalid`, and the
752
+ status line as the input's description, read out when it changes. It focuses the field only when it
753
+ starts empty. It also keeps the code in the address as `?code=`, so a reload or a shared link comes
754
+ back to the same code. It replaces the address and never pushes, so Back still leaves the page.
755
+ Once the person decides, the code leaves the address, because the server no longer knows it.
756
+
736
757
  The link the script prints carries `?code=`, so the field usually starts full. The field shows
737
758
  what was typed as `WDJB-MJHT`, however it was typed. `invalidLetter` turns on as soon as a letter
738
759
  no code uses (a vowel) is typed, so you can say so before all eight are in. `state.status` adds
@@ -743,6 +764,77 @@ and send a stranger the link, and that sentence is the only guard against it. On
743
764
  page stays decided: it never asks the server again, because the server no longer knows the code,
744
765
  and asking on the next focus would tell someone who just said yes that their code is wrong.
745
766
 
767
+ `approve(choice)` sends a choice with the yes, as on the consent page; `deny()` sends none.
768
+ `pending` is `"approve"` or `"deny"` from the press until the server answers, so the spinner goes
769
+ on the button that was pressed.
770
+
771
+ ## A new API key, shown once
772
+
773
+ ```ts
774
+ import { createKeyLifecycle } from "@gusnips/react/keys";
775
+
776
+ export const apiKeys = createKeyLifecycle<ApiKey>();
777
+ ```
778
+
779
+ Make it once, beside your query client, outside every route. It keeps each new key's secret in
780
+ memory until the person presses Done. A second key never replaces the first, and a trip to your
781
+ docs and back loses neither.
782
+
783
+ Tell it who is signed in, and where, from the code that already hears about sign-in and workspace
784
+ changes:
785
+
786
+ ```ts
787
+ apiKeys.setScope(user ? `${user.id}:${workspaceId}` : null);
788
+ ```
789
+
790
+ A new scope forgets every unread secret. A request started before the change rejects with
791
+ `KeyScopeChangedError` instead of showing its secret, even when the same account signs straight
792
+ back in. Build the scope from ids, never from an access token: a token refresh is not a new person.
793
+
794
+ Send your create request through it:
795
+
796
+ ```ts
797
+ const create = useMutation({
798
+ mutationFn: (name: string) =>
799
+ apiKeys.create(async () => {
800
+ const { key, ...record } = await api.post<NewApiKey>("/keys", { name });
801
+ return { record, key };
802
+ }),
803
+ onSuccess: () => queryClient.invalidateQueries({ queryKey: ["keys"] }),
804
+ });
805
+ ```
806
+
807
+ `create` keeps the secret and resolves only the record. In three products we read, `mutation.data`
808
+ held the whole answer, so the secret stayed in react-query's cache for as long as the cache kept
809
+ the mutation. A record that still contains the secret is refused, so that mistake shows on the
810
+ first try. The reveal lands even when the page that asked has unmounted.
811
+
812
+ Show what is unread, and let the person say they saved it:
813
+
814
+ ```tsx
815
+ const reveals = useKeyReveals(apiKeys);
816
+
817
+ return reveals.map(({ record, key }) => (
818
+ <KeyReveal key={record.id} secret={key} onDone={() => apiKeys.dismiss(record.id)} />
819
+ ));
820
+ ```
821
+
822
+ Copy with `useCopy`, and keep Done separate: a copy can fail, and the secret must stay on screen
823
+ when it does. Revoke goes through the same object:
824
+
825
+ ```ts
826
+ mutationFn: (id: string) => apiKeys.revoke(id, () => api.delete(`/keys/${id}`)),
827
+ ```
828
+
829
+ Only a successful revoke forgets that key's secret. A failed one leaves it on screen, because the
830
+ key still works.
831
+
832
+ Nothing is written to storage or the URL, so a reload loses an unread secret. While one is unread
833
+ the tab asks before it closes, and the listener goes away with the last one, because a page with
834
+ a `beforeunload` listener is kept out of some browsers' back-forward cache. Draw the list through
835
+ `queryView`, so a failed refresh keeps the rows instead of saying there are no keys. Forms,
836
+ permissions, plan limits and every word on the page stay yours.
837
+
746
838
  ## Also here
747
839
 
748
840
  `ErrorBoundary`, an SSE reader split into a platform-free parser and a stream wrapper,
@@ -824,7 +916,8 @@ lives behind a subpath, so you install a dependency only if you import the thing
824
916
  | `@gusnips/react/contract` | two prerender names | nothing |
825
917
  | `@gusnips/react/theme` | `createTheme`, `startTheme` | react |
826
918
  | `@gusnips/react/commands` | slash matching and `useSlashCommands` | react |
827
- | `@gusnips/react/oauth` | `useConsentRequest`, `useDeviceApproval` | @tanstack/react-query |
919
+ | `@gusnips/react/oauth` | the consent and device page hooks | @tanstack/react-query |
920
+ | `@gusnips/react/keys` | `createKeyLifecycle`, `useKeyReveals` | react |
828
921
 
829
922
  The rule behind that table: **a peer marked optional must not be reachable from the main entry
830
923
  point.** An optional peer the barrel imports anyway is not optional: it is a required one whose
@@ -834,4 +927,4 @@ about it.
834
927
  `@tanstack/react-query`, `i18next` and `react-i18next` are optional and stay in the main entry,
835
928
  because only their types are used and types erase.
836
929
 
837
- MIT · part of [frontkit](https://github.com/gusnips/frontkit)
930
+ MIT · part of [kit](https://github.com/gusnips/kit)
package/dist/keys.d.ts ADDED
@@ -0,0 +1,58 @@
1
+ /**
2
+ * A new API key, the one time the server sends its secret. Three products wrote this flow, and
3
+ * each knew one part the others missed:
4
+ *
5
+ * - One kept unread secrets in a list outside the page, so a second key did not overwrite the
6
+ * first and a trip to the docs did not lose either. The tab asked before closing only while one
7
+ * was unread, and signing out forgot them all.
8
+ * - One kept the secret out of the list cache, folding only the record into it.
9
+ * - None kept it out of react-query's MUTATION cache. Each `mutationFn` resolved the whole answer,
10
+ * secret included, so `mutation.data` held it, one of them under a comment promising it did not.
11
+ *
12
+ * So `create` takes the secret here and resolves only the record, and the request is the caller's.
13
+ * Forms, permissions, list queries, invalidation and copy feedback stay in the product.
14
+ */
15
+ /** What your create request resolves: the record your list shows, and the secret beside it. */
16
+ export interface RevealedKey<Record> {
17
+ readonly record: Record;
18
+ readonly key: string;
19
+ }
20
+ /** The account or workspace changed while a create or revoke was in flight. */
21
+ export declare class KeyScopeChangedError extends Error {
22
+ readonly operation: "create" | "revoke";
23
+ constructor(operation: "create" | "revoke");
24
+ }
25
+ export interface KeyLifecycle<Record> {
26
+ /**
27
+ * Who and where keys are made for: the signed-in account and the workspace, joined into one
28
+ * string, or `null` once signed out. Call it from your auth and workspace subscription, never
29
+ * from the keys page, which may not be mounted. A new scope forgets every unread secret and
30
+ * rejects requests started under the old one, including when the same account signs back in.
31
+ */
32
+ setScope(scope: string | null): void;
33
+ /**
34
+ * Run your create request, keep its secret here, and resolve only the record. Use it as a
35
+ * `mutationFn`: neither the result nor the variables carry the secret, and the reveal still
36
+ * lands when the page that asked has unmounted.
37
+ */
38
+ create(request: () => Promise<RevealedKey<Record>>): Promise<Record>;
39
+ /** Run your revoke request. Only a success, in the same scope, forgets that key's secret. */
40
+ revoke(id: string, request: () => Promise<unknown>): Promise<void>;
41
+ /** The person saved it. Wire this to "Done", never to a copy: a copy can fail. */
42
+ dismiss(id: string): void;
43
+ /** Unread secrets, newest first. */
44
+ getSnapshot(): readonly RevealedKey<Record>[];
45
+ subscribe(listener: () => void): () => void;
46
+ }
47
+ /**
48
+ * One per app, created at module scope, outside every route. Memory only: never storage, the URL
49
+ * or a query cache, so a reload loses an unread secret, and the tab asks before closing while one
50
+ * is unread. The listener comes and goes with the secrets because a page with a `beforeunload`
51
+ * listener is kept out of some browsers' back-forward cache.
52
+ */
53
+ export declare function createKeyLifecycle<Record extends {
54
+ readonly id: string;
55
+ }>(): KeyLifecycle<Record>;
56
+ /** Unread secrets, newest first. Unmounting the component forgets nothing. */
57
+ export declare function useKeyReveals<Record>(lifecycle: KeyLifecycle<Record>): readonly RevealedKey<Record>[];
58
+ //# sourceMappingURL=keys.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;GAaG;AAEH,+FAA+F;AAC/F,MAAM,WAAW,WAAW,CAAC,MAAM;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED,+EAA+E;AAC/E,qBAAa,oBAAqB,SAAQ,KAAK;IAC7C,QAAQ,CAAC,SAAS,EAAE,QAAQ,GAAG,QAAQ,CAAC;gBAE5B,SAAS,EAAE,QAAQ,GAAG,QAAQ;CAS3C;AAED,MAAM,WAAW,YAAY,CAAC,MAAM;IAClC;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAAC;IACrC;;;;OAIG;IACH,MAAM,CAAC,OAAO,EAAE,MAAM,OAAO,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACrE,6FAA6F;IAC7F,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnE,kFAAkF;IAClF,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,oCAAoC;IACpC,WAAW,IAAI,SAAS,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC;IAC9C,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;CAC7C;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,SAAS;IAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CAAE,KAAK,YAAY,CAAC,MAAM,CAAC,CAkEjG;AAKD,8EAA8E;AAC9E,wBAAgB,aAAa,CAAC,MAAM,EAClC,SAAS,EAAE,YAAY,CAAC,MAAM,CAAC,GAC9B,SAAS,WAAW,CAAC,MAAM,CAAC,EAAE,CAEhC"}
package/dist/keys.js ADDED
@@ -0,0 +1,94 @@
1
+ import { useSyncExternalStore } from "react";
2
+ /** The account or workspace changed while a create or revoke was in flight. */
3
+ export class KeyScopeChangedError extends Error {
4
+ operation;
5
+ constructor(operation) {
6
+ super(operation === "create"
7
+ ? "The account or workspace changed before the new key could be shown. If nobody saved it, revoke it from that workspace's keys."
8
+ : "The account or workspace changed while the key was being revoked. Check that workspace's keys to see whether it worked.");
9
+ this.name = "KeyScopeChangedError";
10
+ this.operation = operation;
11
+ }
12
+ }
13
+ /**
14
+ * One per app, created at module scope, outside every route. Memory only: never storage, the URL
15
+ * or a query cache, so a reload loses an unread secret, and the tab asks before closing while one
16
+ * is unread. The listener comes and goes with the secrets because a page with a `beforeunload`
17
+ * listener is kept out of some browsers' back-forward cache.
18
+ */
19
+ export function createKeyLifecycle() {
20
+ let revealed = [];
21
+ let scope = null;
22
+ let generation = 0;
23
+ const listeners = new Set();
24
+ // Per instance, so two lifecycles never remove each other's prompt.
25
+ function askBeforeLeaving(event) {
26
+ event.preventDefault();
27
+ // Older Chrome and Safari prompt only when this is set too. The browser picks the words.
28
+ event.returnValue = "";
29
+ }
30
+ function publish(next) {
31
+ const wasUnread = revealed.length > 0;
32
+ revealed = next;
33
+ if (wasUnread !== next.length > 0 && typeof window !== "undefined") {
34
+ if (next.length > 0)
35
+ window.addEventListener("beforeunload", askBeforeLeaving);
36
+ else
37
+ window.removeEventListener("beforeunload", askBeforeLeaving);
38
+ }
39
+ for (const listener of listeners)
40
+ listener();
41
+ }
42
+ /** Runs `request` under the current scope and refuses an answer that outlived it. */
43
+ async function scoped(operation, request) {
44
+ if (scope === null)
45
+ throw new Error("Call setScope with the signed-in account and workspace first.");
46
+ const started = generation;
47
+ const answer = await request();
48
+ if (started !== generation)
49
+ throw new KeyScopeChangedError(operation);
50
+ return answer;
51
+ }
52
+ function dismiss(id) {
53
+ const next = revealed.filter((one) => one.record.id !== id);
54
+ if (next.length !== revealed.length)
55
+ publish(next);
56
+ }
57
+ return {
58
+ setScope(next) {
59
+ if (scope === next)
60
+ return;
61
+ scope = next;
62
+ generation++;
63
+ if (revealed.length > 0)
64
+ publish([]);
65
+ },
66
+ async create(request) {
67
+ const { record, key } = await scoped("create", request);
68
+ // The record goes on to react-query and often into the list cache. A request that passed
69
+ // the whole answer as the record would put the secret there, which is the bug this exists
70
+ // to end, so say so on the first try instead.
71
+ if (JSON.stringify(record).includes(key))
72
+ throw new Error("The record still holds the secret. Resolve it without the key field.");
73
+ publish([{ record, key }, ...revealed]);
74
+ return record;
75
+ },
76
+ async revoke(id, request) {
77
+ await scoped("revoke", request);
78
+ dismiss(id);
79
+ },
80
+ dismiss,
81
+ getSnapshot: () => revealed,
82
+ subscribe(listener) {
83
+ listeners.add(listener);
84
+ return () => listeners.delete(listener);
85
+ },
86
+ };
87
+ }
88
+ // The server has no unread secrets, and a prerender must never print one.
89
+ const none = [];
90
+ /** Unread secrets, newest first. Unmounting the component forgets nothing. */
91
+ export function useKeyReveals(lifecycle) {
92
+ return useSyncExternalStore(lifecycle.subscribe, lifecycle.getSnapshot, () => none);
93
+ }
94
+ //# sourceMappingURL=keys.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keys.js","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"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"}
package/dist/oauth.d.ts CHANGED
@@ -1,10 +1,13 @@
1
+ import { type ChangeEvent } from "react";
2
+ import type { ApiClient } from "./api-client.ts";
1
3
  /**
2
4
  * The two pages behind an agent's sign-in: the consent page, where Claude or ChatGPT sends a
3
5
  * person to connect, and the device page, where a script's short code is typed and approved.
4
6
  * `@gusnips/server/oauth` is the server half. Logic only: the pages are the product's.
5
7
  *
6
8
  * Each hook takes your two API calls, so your client, your session and your envelope stay yours,
7
- * and returns one state to draw. What they settle once:
9
+ * and returns one state to draw. `createOAuthConsoleApi` writes those calls for the server's
10
+ * `oauthConsoleRoutes`. What they settle once:
8
11
  *
9
12
  * - A refusal (an unknown app, a return address it never registered) is a STATE, never a
10
13
  * redirect: sending the browser anywhere on a bad request is how an open redirect is built.
@@ -22,7 +25,8 @@ export interface OAuthRefusal {
22
25
  /** An English fallback a person can read. */
23
26
  message: string;
24
27
  }
25
- export interface ConsentView {
28
+ /** `Extra` is anything your server adds to the view, such as the workspaces a person may pick. */
29
+ export type ConsentView<Extra extends object = object> = Extra & {
26
30
  ok: true;
27
31
  /** What the app calls itself. Anyone may pick any name, so show `redirectHost` beside it. */
28
32
  clientName: string;
@@ -32,8 +36,8 @@ export interface ConsentView {
32
36
  keyName: string;
33
37
  /** Where "Cancel" goes: back to the app, which is told the person said no. */
34
38
  cancelUrl: string;
35
- }
36
- export interface DeviceView {
39
+ };
40
+ export type DeviceView<Extra extends object = object> = Extra & {
37
41
  ok: true;
38
42
  clientName: string;
39
43
  /** As the script printed it: `WDJB-MJHT`. */
@@ -41,17 +45,17 @@ export interface DeviceView {
41
45
  keyName: string;
42
46
  /** ISO time: JSON carries the server's `Date` as a string. */
43
47
  expiresAt: string;
44
- }
45
- export interface ConsentApi<Choice> {
48
+ };
49
+ export interface ConsentApi<Choice = void, Extra extends object = object> {
46
50
  /** Your API's call to the server's `describe`, with the query string as it came. */
47
- describe(query: string): Promise<ConsentView | OAuthRefusal>;
51
+ describe(query: string): Promise<ConsentView<Extra> | OAuthRefusal>;
48
52
  /** Your API's call to `approve`. `choice` is anything your page adds, such as a workspace. */
49
53
  approve(query: string, choice: Choice): Promise<{
50
54
  ok: true;
51
55
  redirectTo: string;
52
56
  } | OAuthRefusal>;
53
57
  }
54
- export type ConsentState = {
58
+ export type ConsentState<Extra extends object = object> = {
55
59
  status: "loading";
56
60
  }
57
61
  /** Show `refusal` and a way home. Never a link back to the app. */
@@ -66,10 +70,10 @@ export type ConsentState = {
66
70
  retry: () => void;
67
71
  } | {
68
72
  status: "ready";
69
- view: ConsentView;
73
+ view: ConsentView<Extra>;
70
74
  };
71
- export interface ConsentRequest<Choice> {
72
- state: ConsentState;
75
+ export interface ConsentRequest<Choice = void, Extra extends object = object> {
76
+ state: ConsentState<Extra>;
73
77
  approve(choice: Choice): void;
74
78
  /** From the press until the browser has left. Keep the button busy the whole time. */
75
79
  leaving: boolean;
@@ -80,16 +84,16 @@ export interface ConsentRequest<Choice> {
80
84
  * const consent = useConsentRequest(api.oauth, useLocation().search);
81
85
  * if (consent.state.status === "ready") ... consent.approve(workspaceId)
82
86
  */
83
- export declare function useConsentRequest<Choice = void>(api: ConsentApi<Choice>, search: string): ConsentRequest<Choice>;
84
- export interface DeviceApi {
87
+ export declare function useConsentRequest<Choice = void, Extra extends object = object>(api: ConsentApi<Choice, Extra>, search: string): ConsentRequest<Choice, Extra>;
88
+ export interface DeviceApi<Choice = void, Extra extends object = object> {
85
89
  /** Your API's call to the server's `describeDevice`. */
86
- describe(code: string): Promise<DeviceView | OAuthRefusal>;
87
- /** Your API's call to `decideDevice`. */
88
- decide(code: string, decision: "approve" | "deny"): Promise<{
90
+ describe(code: string): Promise<DeviceView<Extra> | OAuthRefusal>;
91
+ /** Your API's call to `decideDevice`. `choice` comes with a yes only, as on the consent page. */
92
+ decide(code: string, decision: "approve" | "deny", choice?: Choice): Promise<{
89
93
  ok: true;
90
94
  } | OAuthRefusal>;
91
95
  }
92
- export type DeviceState =
96
+ export type DeviceState<Extra extends object = object> =
93
97
  /** Fewer than 8 letters so far, or one a code never uses (see `invalidLetter`). */
94
98
  {
95
99
  status: "typing";
@@ -112,7 +116,7 @@ export type DeviceState =
112
116
  */
113
117
  | {
114
118
  status: "ready";
115
- view: DeviceView;
119
+ view: DeviceView<Extra>;
116
120
  }
117
121
  /** Done. Tell them to go back to the app, which carries on by itself. */
118
122
  | {
@@ -120,16 +124,18 @@ export type DeviceState =
120
124
  } | {
121
125
  status: "denied";
122
126
  };
123
- export interface DeviceApproval {
124
- state: DeviceState;
127
+ export interface DeviceApproval<Choice = void, Extra extends object = object> {
128
+ state: DeviceState<Extra>;
125
129
  /** What the field shows, formatted as they type: `WDJB-MJ`. */
126
130
  code: string;
127
131
  setCode(input: string): void;
128
132
  /** A typed letter no code uses: a vowel. Say so now, not after eight letters. */
129
133
  invalidLetter: boolean;
130
- approve(): void;
134
+ approve(choice: Choice): void;
131
135
  deny(): void;
132
136
  deciding: boolean;
137
+ /** Which button was pressed, until the server answers: put the spinner on that one. */
138
+ pending: "approve" | "deny" | null;
133
139
  /** The decision did not answer. Show it beside the buttons; pressing again retries. */
134
140
  decideError: unknown;
135
141
  }
@@ -141,5 +147,53 @@ export declare function formatUserCode(input: string): string;
141
147
  *
142
148
  * The link the script prints carries `?code=`, so the field usually starts full.
143
149
  */
144
- export declare function useDeviceApproval(api: DeviceApi, search: string): DeviceApproval;
150
+ export declare function useDeviceApproval<Choice = void, Extra extends object = object>(api: DeviceApi<Choice, Extra>, search: string): DeviceApproval<Choice, Extra>;
151
+ export interface DeviceCodeField {
152
+ /** Spread on the code's `<input>`. Point your `<label htmlFor>` at its `id`. */
153
+ inputProps: {
154
+ id: string;
155
+ value: string;
156
+ onChange(event: ChangeEvent<HTMLInputElement>): void;
157
+ placeholder: string;
158
+ autoComplete: "off";
159
+ autoCapitalize: "characters";
160
+ spellCheck: false;
161
+ /** Only when the field starts empty: a code from the link is there to be read first. */
162
+ autoFocus: boolean;
163
+ disabled: boolean;
164
+ "aria-invalid": boolean;
165
+ "aria-describedby": string;
166
+ };
167
+ /** Spread on the line under the field that says what is happening: a vowel, checking, refused. */
168
+ statusProps: {
169
+ id: string;
170
+ "aria-live": "polite";
171
+ };
172
+ }
173
+ /**
174
+ * const field = useDeviceCodeField(device);
175
+ * <label htmlFor={field.inputProps.id}>Code</label>
176
+ * <input {...field.inputProps} />
177
+ * <p {...field.statusProps}>{device.invalidLetter ? "Codes have no vowels." : null}</p>
178
+ *
179
+ * The field's wiring, and the code kept in the address as `?code=`, so a reload or a shared link
180
+ * comes back to the same code. The address is replaced, never pushed, so Back still leaves the
181
+ * page, and the router's own state rides along. Once decided the code leaves the address: the
182
+ * server no longer knows it, and a reload would only tell the person it is wrong.
183
+ */
184
+ export declare function useDeviceCodeField<Choice, Extra extends object>(device: DeviceApproval<Choice, Extra>): DeviceCodeField;
185
+ /** The calls the console API makes. `createApiClient`'s client fits as it is. */
186
+ export type OAuthConsoleHttp = Pick<ApiClient, "get" | "post">;
187
+ /**
188
+ * export const oauthApi = createOAuthConsoleApi(api, "/auth/oauth");
189
+ * useConsentRequest(oauthApi.consent, search);
190
+ * useDeviceApproval(oauthApi.device, search);
191
+ *
192
+ * Both pages' calls to the server's `oauthConsoleRoutes`, mounted at `basePath` behind your
193
+ * sign-in. A choice travels as `choice`, which is where those routes hand it to your approver.
194
+ */
195
+ export declare function createOAuthConsoleApi<Choice = void>(http: OAuthConsoleHttp, basePath: string): {
196
+ consent: ConsentApi<Choice>;
197
+ device: DeviceApi<Choice>;
198
+ };
145
199
  //# sourceMappingURL=oauth.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../src/oauth.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../src/oauth.ts"],"names":[],"mappings":"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"}
package/dist/oauth.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { useMutation, useQuery } from "@tanstack/react-query";
2
- import { useState } from "react";
2
+ import { useEffect, useId, useState } from "react";
3
3
  /**
4
4
  * const consent = useConsentRequest(api.oauth, useLocation().search);
5
5
  * if (consent.state.status === "ready") ... consent.approve(workspaceId)
@@ -59,9 +59,9 @@ export function useDeviceApproval(api, search) {
59
59
  const invalidLetter = [...letters].some((letter) => !ALPHABET.includes(letter));
60
60
  const complete = letters.length === LENGTH && !invalidLetter;
61
61
  const decision = useMutation({
62
- mutationFn: (choice) => api.decide(letters, choice),
62
+ mutationFn: (pressed) => api.decide(letters, pressed.decision, pressed.choice),
63
63
  });
64
- const decided = decision.data?.ok === true ? decision.variables : undefined;
64
+ const decided = decision.isSuccess && decision.data.ok ? decision.variables.decision : undefined;
65
65
  const request = useQuery({
66
66
  queryKey: ["@gusnips/react/oauth", "device", letters],
67
67
  queryFn: () => api.describe(letters),
@@ -90,10 +90,73 @@ export function useDeviceApproval(api, search) {
90
90
  setTyped(formatUserCode(input));
91
91
  },
92
92
  invalidLetter,
93
- approve: () => decision.mutate("approve"),
94
- deny: () => decision.mutate("deny"),
93
+ approve: (choice) => decision.mutate({ decision: "approve", choice }),
94
+ deny: () => decision.mutate({ decision: "deny" }),
95
95
  deciding: decision.isPending,
96
+ pending: decision.isPending ? decision.variables.decision : null,
96
97
  decideError: decision.error,
97
98
  };
98
99
  }
100
+ /**
101
+ * const field = useDeviceCodeField(device);
102
+ * <label htmlFor={field.inputProps.id}>Code</label>
103
+ * <input {...field.inputProps} />
104
+ * <p {...field.statusProps}>{device.invalidLetter ? "Codes have no vowels." : null}</p>
105
+ *
106
+ * The field's wiring, and the code kept in the address as `?code=`, so a reload or a shared link
107
+ * comes back to the same code. The address is replaced, never pushed, so Back still leaves the
108
+ * page, and the router's own state rides along. Once decided the code leaves the address: the
109
+ * server no longer knows it, and a reload would only tell the person it is wrong.
110
+ */
111
+ export function useDeviceCodeField(device) {
112
+ const id = useId();
113
+ const statusId = `${id}-status`;
114
+ const { status } = device.state;
115
+ const inAddress = status === "approved" || status === "denied" ? "" : device.code;
116
+ useEffect(() => {
117
+ const url = new URL(window.location.href);
118
+ if (inAddress)
119
+ url.searchParams.set("code", inAddress);
120
+ else
121
+ url.searchParams.delete("code");
122
+ if (url.href !== window.location.href)
123
+ window.history.replaceState(window.history.state, "", url);
124
+ }, [inAddress]);
125
+ return {
126
+ inputProps: {
127
+ id,
128
+ value: device.code,
129
+ onChange: (event) => device.setCode(event.target.value),
130
+ placeholder: "WDJB-MJHT",
131
+ autoComplete: "off",
132
+ autoCapitalize: "characters",
133
+ spellCheck: false,
134
+ autoFocus: device.code === "",
135
+ disabled: device.deciding,
136
+ "aria-invalid": device.invalidLetter || status === "refused",
137
+ "aria-describedby": statusId,
138
+ },
139
+ statusProps: { id: statusId, "aria-live": "polite" },
140
+ };
141
+ }
142
+ /**
143
+ * export const oauthApi = createOAuthConsoleApi(api, "/auth/oauth");
144
+ * useConsentRequest(oauthApi.consent, search);
145
+ * useDeviceApproval(oauthApi.device, search);
146
+ *
147
+ * Both pages' calls to the server's `oauthConsoleRoutes`, mounted at `basePath` behind your
148
+ * sign-in. A choice travels as `choice`, which is where those routes hand it to your approver.
149
+ */
150
+ export function createOAuthConsoleApi(http, basePath) {
151
+ return {
152
+ consent: {
153
+ describe: (query) => http.get(`${basePath}/request?${query}`),
154
+ approve: (query, choice) => http.post(`${basePath}/approve`, { query, choice }),
155
+ },
156
+ device: {
157
+ describe: (code) => http.get(`${basePath}/device?code=${encodeURIComponent(code)}`),
158
+ decide: (code, decision, choice) => http.post(`${basePath}/device`, { code, decision, choice }),
159
+ },
160
+ };
161
+ }
99
162
  //# sourceMappingURL=oauth.js.map
package/dist/oauth.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"oauth.js","sourceRoot":"","sources":["../src/oauth.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"oauth.js","sourceRoot":"","sources":["../src/oauth.ts"],"names":[],"mappings":"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"}