@civitai/sdk 0.8.0 → 0.10.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/README.md +32 -1
- package/dist/core/handshake.d.ts +16 -1
- package/dist/core/transports/iframe-transport.js +24 -0
- package/dist/host/index.d.ts +10 -3
- package/dist/host/index.js +6 -1
- package/dist/host/protocol.d.ts +14 -0
- package/dist/index.d.ts +1 -1
- package/dist/sign-in/index.d.ts +14 -1
- package/dist/sign-in/index.js +85 -25
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -60,6 +60,14 @@ Both give you:
|
|
|
60
60
|
A block's `app` also has `host`, `viewer`, `context`, `settings`, `theme` and
|
|
61
61
|
`onChange(listener)`. Outside a block they do not exist, and the types say so.
|
|
62
62
|
|
|
63
|
+
`context` and `theme` are LIVE, not mount-time snapshots: the host pushes
|
|
64
|
+
`THEME_CHANGE` when the viewer toggles dark mode and `ROUTE_CHANGED` when a page
|
|
65
|
+
block's sub-path moves, and both are folded into the same snapshot — so
|
|
66
|
+
`app.context.subPath` tracks `app.host.navigate(path)` (and the viewer's own back
|
|
67
|
+
button) and `onChange` tells you when. Read them on each render rather than
|
|
68
|
+
copying them once; there is deliberately no `onRouteChange`, because one value
|
|
69
|
+
observed two ways is two values that can disagree.
|
|
70
|
+
|
|
63
71
|
A block opts in by declaring `auth: "oauth"` in its `block.manifest.json`. The
|
|
64
72
|
host then hands it a real OAuth access token that `/api/v1`, the orchestrator
|
|
65
73
|
and the MCP accept, and consent — including `requestGrants` — goes through the
|
|
@@ -131,6 +139,29 @@ and a flag that the viewer signed in before (`returning`). Signing in again is
|
|
|
131
139
|
a quick round trip, since Civitai remembers the consent. `requestGrants` goes
|
|
132
140
|
back to Civitai for anything not yet granted.
|
|
133
141
|
|
|
142
|
+
### Without leaving the page
|
|
143
|
+
|
|
144
|
+
A page that must not be navigated away from, such as one with a chat or an
|
|
145
|
+
editor embedded in it, signs in in a popup instead:
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
import { createSignIn, initialize } from '@civitai/sdk';
|
|
149
|
+
|
|
150
|
+
const auth = await createSignIn({ clientId, scopes: ['ai:write:budgeted'] });
|
|
151
|
+
button.onclick = async () => {
|
|
152
|
+
await auth.signInWithPopup(); // from the click itself: browsers only open a popup there
|
|
153
|
+
const app = await initialize(auth);
|
|
154
|
+
};
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Civitai returns the popup to `redirectUri`, the same page by default. That page
|
|
158
|
+
calls `createSignIn()` as on any visit; in the popup it hands the result back to
|
|
159
|
+
the page that opened it and closes. The result travels over a same-origin
|
|
160
|
+
`BroadcastChannel`, not `window.opener`, because signing in through Google cuts
|
|
161
|
+
the opener. A blocked popup rejects with `SignInError` `code: 'popup-blocked'`;
|
|
162
|
+
pass a `signal` to give up, which rejects with `code: 'canceled'`.
|
|
163
|
+
`requestGrants` still leaves the page.
|
|
164
|
+
|
|
134
165
|
Only scopes with an OAuth equivalent can be asked for: `user:read:self`,
|
|
135
166
|
`models:read:self`, `ai:write:budgeted`, `buzz:read:self`, `posts:write:self`
|
|
136
167
|
and `social:tip:self`. The storage and collection scopes belong to blocks.
|
|
@@ -315,7 +346,7 @@ wider scope — and refuses without one.
|
|
|
315
346
|
| `resize(height)` | Resizes the frame, clamped to the manifest |
|
|
316
347
|
| `autoResize(element?)` | Keeps the frame as tall as the body (or `element`); returns a stop function |
|
|
317
348
|
| `reportError(message, { fatal })` | `fatal` swaps the block for the host's fallback |
|
|
318
|
-
| `navigate(path, { target })` | Deep-links within this app's own sub-paths |
|
|
349
|
+
| `navigate(path, { scope, target })` | Deep-links within this app's own sub-paths; `scope: 'site'` asks to leave the app for a civitai.com page instead. `scope` defaults to `'app'` |
|
|
319
350
|
| `onVisibilityChange(handler)` | Reports the page hiding and returning |
|
|
320
351
|
| `requestSignIn({ returnUrl })` | Starts sign-in; the block re-initialises signed in |
|
|
321
352
|
| `download({ url, filename })` | Saves to the viewer's device — a sandboxed frame cannot |
|
package/dist/core/handshake.d.ts
CHANGED
|
@@ -99,7 +99,17 @@ export interface BlockInitPayload {
|
|
|
99
99
|
/** The domain ceiling narrowed to this viewer. Always a subset of the above. */
|
|
100
100
|
effectiveBrowsingLevel?: number;
|
|
101
101
|
}
|
|
102
|
-
/**
|
|
102
|
+
/**
|
|
103
|
+
* Host messages the transport itself acts on. Domains own everything else.
|
|
104
|
+
*
|
|
105
|
+
* `ROUTE_CHANGED` is here rather than in `HostPushes` for the same reason
|
|
106
|
+
* `THEME_CHANGE` is: it carries a value the snapshot already holds, so the
|
|
107
|
+
* transport folds it in and `client.context` / `client.onChange` are how a
|
|
108
|
+
* consumer observes it. A `HostPushes` row plus an `onRouteChange` method would
|
|
109
|
+
* publish a SECOND way to read one value, which is how `client.context.subPath`
|
|
110
|
+
* and a callback end up disagreeing. `SUSPEND`/`RESUME` are in `HostPushes`
|
|
111
|
+
* because they carry no state for the snapshot to hold.
|
|
112
|
+
*/
|
|
103
113
|
export type HostMessage = {
|
|
104
114
|
type: 'BLOCK_INIT';
|
|
105
115
|
payload: BlockInitPayload;
|
|
@@ -119,6 +129,11 @@ export type HostMessage = {
|
|
|
119
129
|
payload: {
|
|
120
130
|
theme: Theme;
|
|
121
131
|
};
|
|
132
|
+
} | {
|
|
133
|
+
type: 'ROUTE_CHANGED';
|
|
134
|
+
payload: {
|
|
135
|
+
subPath: string;
|
|
136
|
+
};
|
|
122
137
|
};
|
|
123
138
|
/** Discriminator only — the payload is still unvalidated wire data. */
|
|
124
139
|
export declare function isHostMessage<K extends HostMessage['type']>(data: unknown, type: K): data is Extract<HostMessage, {
|
|
@@ -287,6 +287,18 @@ export class IframeTransport {
|
|
|
287
287
|
this.#applyThemeChange(data.payload.theme);
|
|
288
288
|
return;
|
|
289
289
|
}
|
|
290
|
+
// The page's sub-path moved under the app root. Folded into the snapshot so
|
|
291
|
+
// `client.context.subPath` is live and `onChange` reports it — the same
|
|
292
|
+
// treatment as the theme, and the reason there is no `onRouteChange`.
|
|
293
|
+
//
|
|
294
|
+
// Unlike the theme this IS effectively init-gated, because the sub-path
|
|
295
|
+
// lives only in `context` and the pre-init context has no such key. That
|
|
296
|
+
// costs nothing: the host sends this only after init, and `BLOCK_INIT`
|
|
297
|
+
// replaces the snapshot with its own current value.
|
|
298
|
+
if (isHostMessage(data, 'ROUTE_CHANGED')) {
|
|
299
|
+
this.#applyRouteChange(data.payload.subPath);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
290
302
|
// Matching the TYPE too, not just the id: `IMAGE_SCAN_RESOLVED` is a push
|
|
291
303
|
// that deliberately reuses its `OPEN_IMAGE_UPLOAD`'s requestId, so an
|
|
292
304
|
// id-only match could resolve the upload with the scan verdict's payload.
|
|
@@ -337,6 +349,18 @@ export class IframeTransport {
|
|
|
337
349
|
this.#snapshot = next;
|
|
338
350
|
this.#emit();
|
|
339
351
|
}
|
|
352
|
+
// Only ever UPDATES a context that already carries the key; never INTRODUCES
|
|
353
|
+
// it. A model slot has no route, so synthesising `subPath` would assert a page
|
|
354
|
+
// context the host never sent. Emits only when the value moved.
|
|
355
|
+
#applyRouteChange(subPath) {
|
|
356
|
+
const context = this.#snapshot.context;
|
|
357
|
+
if (!('subPath' in context))
|
|
358
|
+
return;
|
|
359
|
+
if (context.subPath === subPath)
|
|
360
|
+
return;
|
|
361
|
+
this.#snapshot = { ...this.#snapshot, context: { ...context, subPath } };
|
|
362
|
+
this.#emit();
|
|
363
|
+
}
|
|
340
364
|
#emit() {
|
|
341
365
|
for (const listener of this.#listeners)
|
|
342
366
|
listener();
|
package/dist/host/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type BlockTransport } from '../core/transport.js';
|
|
2
2
|
import type { Session } from '../session/index.js';
|
|
3
|
-
import type { DownloadRequest, ImageScanResult, PickedResource, ResourcePickerType, SourceImage } from './protocol.js';
|
|
4
|
-
export type { ConsentRefusal, DownloadRequest, ImageScanResult, PickedResource, ResourcePickerType, SourceImage, UploadedImage, } from './protocol.js';
|
|
3
|
+
import type { DownloadRequest, ImageScanResult, NavigateScope, PickedResource, ResourcePickerType, SourceImage } from './protocol.js';
|
|
4
|
+
export type { ConsentRefusal, DownloadRequest, ImageScanResult, NavigateScope, PickedResource, ResourcePickerType, SourceImage, UploadedImage, } from './protocol.js';
|
|
5
5
|
export interface HostCallOptions {
|
|
6
6
|
signal?: AbortSignal;
|
|
7
7
|
}
|
|
@@ -33,8 +33,15 @@ export interface Host {
|
|
|
33
33
|
reportError(message: string, args?: {
|
|
34
34
|
fatal?: boolean;
|
|
35
35
|
}): void;
|
|
36
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* Asks the host to navigate. `scope` picks the space `path` is resolved in and
|
|
38
|
+
* DEFAULTS to `'app'` — this app's own sub-paths, which is all this method
|
|
39
|
+
* could reach before the field existed. Pass `scope: 'site'` to leave the app
|
|
40
|
+
* for a civitai.com page; the host grants that per-surface and refuses it
|
|
41
|
+
* elsewhere. Fire-and-forget either way: a refusal is silent.
|
|
42
|
+
*/
|
|
37
43
|
navigate(path: string, args?: {
|
|
44
|
+
scope?: NavigateScope;
|
|
38
45
|
target?: 'current' | 'new_tab';
|
|
39
46
|
}): void;
|
|
40
47
|
/** Fires `false` when the page hides and `true` when it returns. */
|
package/dist/host/index.js
CHANGED
|
@@ -25,7 +25,12 @@ export function createHost(transport) {
|
|
|
25
25
|
return () => observer.disconnect();
|
|
26
26
|
},
|
|
27
27
|
reportError: (message, args = {}) => notify('BLOCK_ERROR', { message, fatal: args.fatal ?? false }, { transport }),
|
|
28
|
-
|
|
28
|
+
// `scope` is OMITTED rather than sent as `undefined` when the caller did not
|
|
29
|
+
// choose one. Absent and `'app'` mean the same thing to the host, so this is
|
|
30
|
+
// not a behaviour difference — it keeps the wire payload byte-identical to
|
|
31
|
+
// what every pre-`scope` build sends, which is the property the back-compat
|
|
32
|
+
// claim rests on and the one a host-side diff can actually check.
|
|
33
|
+
navigate: (path, args = {}) => notify('NAVIGATE', { path, ...(args.scope ? { scope: args.scope } : {}), target: args.target ?? 'current' }, { transport }),
|
|
29
34
|
onVisibilityChange(handler) {
|
|
30
35
|
const offSuspend = on(transport, 'SUSPEND', () => handler(false));
|
|
31
36
|
const offResume = on(transport, 'RESUME', () => handler(true));
|
package/dist/host/protocol.d.ts
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
import type { WrappedToken } from '../core/handshake.js';
|
|
2
2
|
export type ResourcePickerType = 'Checkpoint' | 'LORA';
|
|
3
|
+
/**
|
|
4
|
+
* Which SPACE a `navigate()` path is resolved in.
|
|
5
|
+
*
|
|
6
|
+
* - `'app'` — under this app's own route, as a sub-path of it. The DEFAULT.
|
|
7
|
+
* - `'site'` — at the site root, leaving the app. The host grants this
|
|
8
|
+
* per-surface and refuses it where the surface does not hold the capability.
|
|
9
|
+
*
|
|
10
|
+
* Omitting it means `'app'`, which is what keeps this field additive: a block
|
|
11
|
+
* that never sets it behaves as it did before the field existed. A leading slash
|
|
12
|
+
* carries no meaning — the host normalises it away in both scopes, so
|
|
13
|
+
* `'/settings'` and `'settings'` are one request within the scope you chose.
|
|
14
|
+
*/
|
|
15
|
+
export type NavigateScope = 'app' | 'site';
|
|
3
16
|
/** The one resource the viewer picked. Discovery only: nothing here is an entitlement. */
|
|
4
17
|
export interface PickedResource {
|
|
5
18
|
versionId: number;
|
|
@@ -153,6 +166,7 @@ export type HostNotifications = {
|
|
|
153
166
|
};
|
|
154
167
|
NAVIGATE: {
|
|
155
168
|
path: string;
|
|
169
|
+
scope?: NavigateScope;
|
|
156
170
|
target: 'current' | 'new_tab';
|
|
157
171
|
};
|
|
158
172
|
REQUEST_SIGN_IN: {
|
package/dist/index.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export type { StorageCallOptions, StorageClient, StorageKeyEntry, StorageListQue
|
|
|
14
14
|
export { isTerminal, isTerminalStatus } from './orchestration/index.js';
|
|
15
15
|
export type { OrchestrationClient, Step, StepTemplate, WaitOptions, Workflow, WorkflowPage, WorkflowQuery, WorkflowStatus, WorkflowTemplate, } from './orchestration/index.js';
|
|
16
16
|
export { createHost } from './host/index.js';
|
|
17
|
-
export type { ConsentRefusal, DownloadRequest, Host, HostCallOptions, ImageScanResult, PendingImage, PickedResource, ResourcePickerType, SourceImage, UploadedImage, } from './host/index.js';
|
|
17
|
+
export type { ConsentRefusal, DownloadRequest, Host, HostCallOptions, ImageScanResult, NavigateScope, PendingImage, PickedResource, ResourcePickerType, SourceImage, UploadedImage, } from './host/index.js';
|
|
18
18
|
export { ApiError } from './http/index.js';
|
|
19
19
|
export type { Query, QueryValue, RequestOptions } from './http/index.js';
|
|
20
20
|
export { BridgeError, CivitaiError } from './core/errors.js';
|
package/dist/sign-in/index.d.ts
CHANGED
|
@@ -3,7 +3,11 @@ import type { GrantOptions, Scope, TokenSessionOptions } from '../session/index.
|
|
|
3
3
|
export declare const DEFAULT_AUTH_URL = "https://auth.civitai.com";
|
|
4
4
|
export declare class SignInError extends CivitaiError {
|
|
5
5
|
readonly status?: number;
|
|
6
|
-
|
|
6
|
+
/** `canceled`: the viewer gave up on a popup sign-in. `popup-blocked`: the browser refused to open it. */
|
|
7
|
+
readonly code?: 'canceled' | 'popup-blocked';
|
|
8
|
+
constructor(message: string, status?: number, options?: ErrorOptions & {
|
|
9
|
+
code?: 'canceled' | 'popup-blocked';
|
|
10
|
+
});
|
|
7
11
|
}
|
|
8
12
|
type SignInStorage = Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>;
|
|
9
13
|
export interface SignInOptions {
|
|
@@ -34,6 +38,15 @@ export interface SignIn extends TokenSessionOptions {
|
|
|
34
38
|
signIn(opts?: {
|
|
35
39
|
scopes?: readonly Scope[];
|
|
36
40
|
}): Promise<never>;
|
|
41
|
+
/**
|
|
42
|
+
* Signs in in a popup, so the page stays where it is. Call it straight from a click or key press:
|
|
43
|
+
* browsers only open a popup there. Civitai returns the popup to `redirectUri`, whose page must call
|
|
44
|
+
* `createSignIn()` with the same client, which hands the result back here and closes the popup.
|
|
45
|
+
*/
|
|
46
|
+
signInWithPopup(opts?: {
|
|
47
|
+
scopes?: readonly Scope[];
|
|
48
|
+
signal?: AbortSignal;
|
|
49
|
+
}): Promise<void>;
|
|
37
50
|
signOut(): Promise<void>;
|
|
38
51
|
}
|
|
39
52
|
export declare function scopeBitmask(scopes: readonly Scope[]): number;
|
package/dist/sign-in/index.js
CHANGED
|
@@ -13,10 +13,13 @@ const OAUTH_BITS = {
|
|
|
13
13
|
const EXPIRY_MARGIN_MS = 60_000;
|
|
14
14
|
export class SignInError extends CivitaiError {
|
|
15
15
|
status;
|
|
16
|
+
/** `canceled`: the viewer gave up on a popup sign-in. `popup-blocked`: the browser refused to open it. */
|
|
17
|
+
code;
|
|
16
18
|
constructor(message, status, options) {
|
|
17
19
|
super(message, options);
|
|
18
20
|
this.name = 'SignInError';
|
|
19
21
|
this.status = status;
|
|
22
|
+
this.code = options?.code;
|
|
20
23
|
}
|
|
21
24
|
}
|
|
22
25
|
export function scopeBitmask(scopes) {
|
|
@@ -62,6 +65,14 @@ function writeJson(storage, key, value) {
|
|
|
62
65
|
// Private windows can refuse storage; the viewer then signs in again next visit.
|
|
63
66
|
}
|
|
64
67
|
}
|
|
68
|
+
const canceled = () => new SignInError('sign-in was canceled', undefined, { code: 'canceled' });
|
|
69
|
+
function popupFeatures(win) {
|
|
70
|
+
const width = 520;
|
|
71
|
+
const height = 720;
|
|
72
|
+
const left = Math.max(0, Math.round(win.screenX + (win.outerWidth - width) / 2));
|
|
73
|
+
const top = Math.max(0, Math.round(win.screenY + (win.outerHeight - height) / 2));
|
|
74
|
+
return `popup,width=${width},height=${height},left=${left},top=${top}`;
|
|
75
|
+
}
|
|
65
76
|
/**
|
|
66
77
|
* Sign-in with Civitai for an app outside civitai.com. Completes the return
|
|
67
78
|
* from Civitai when the page has just come back from it.
|
|
@@ -72,6 +83,9 @@ export async function createSignIn(options) {
|
|
|
72
83
|
const doFetch = options.fetch ?? fetch.bind(globalThis);
|
|
73
84
|
const authUrl = options.authUrl ?? DEFAULT_AUTH_URL;
|
|
74
85
|
const key = `civitai.sign-in.${options.clientId}`;
|
|
86
|
+
// A same-origin channel, not `popup.opener`: Civitai may pass the popup through Google, whose
|
|
87
|
+
// opener policy cuts that link for good.
|
|
88
|
+
const channelName = key;
|
|
75
89
|
const pendingKey = `${key}.pending`;
|
|
76
90
|
const returningKey = `${key}.returning`;
|
|
77
91
|
const requested = scopeBitmask(options.scopes);
|
|
@@ -123,15 +137,14 @@ export async function createSignIn(options) {
|
|
|
123
137
|
});
|
|
124
138
|
return refreshing;
|
|
125
139
|
};
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
writeJson(storage, pendingKey, pending);
|
|
140
|
+
const newPending = (scopes, popup) => ({
|
|
141
|
+
state: randomString(),
|
|
142
|
+
verifier: randomString(),
|
|
143
|
+
redirectUri: options.redirectUri ?? `${win.location.origin}${win.location.pathname}`,
|
|
144
|
+
scope: requested | (scopes ? scopeBitmask(scopes) : 0) | (tokens?.scope ?? 0),
|
|
145
|
+
...(popup ? { popup: true } : {}),
|
|
146
|
+
});
|
|
147
|
+
const authorizeUrl = async (pending) => {
|
|
135
148
|
const url = new URL('/api/auth/oauth/authorize', authUrl);
|
|
136
149
|
url.search = new URLSearchParams({
|
|
137
150
|
client_id: options.clientId,
|
|
@@ -139,33 +152,79 @@ export async function createSignIn(options) {
|
|
|
139
152
|
response_type: 'code',
|
|
140
153
|
state: pending.state,
|
|
141
154
|
scope: String(pending.scope),
|
|
142
|
-
code_challenge: await challengeFor(verifier),
|
|
155
|
+
code_challenge: await challengeFor(pending.verifier),
|
|
143
156
|
code_challenge_method: 'S256',
|
|
144
157
|
}).toString();
|
|
145
|
-
|
|
158
|
+
return url.toString();
|
|
159
|
+
};
|
|
160
|
+
const complete = async (pending, params) => {
|
|
161
|
+
const code = params.get('code');
|
|
162
|
+
if (!code)
|
|
163
|
+
throw new SignInError(params.get('error_description') ?? params.get('error') ?? 'sign-in was declined');
|
|
164
|
+
save(await tokenRequest({ grant_type: 'authorization_code', code, redirect_uri: pending.redirectUri, code_verifier: pending.verifier }, pending.scope));
|
|
165
|
+
};
|
|
166
|
+
const signIn = async ({ scopes } = {}) => {
|
|
167
|
+
const pending = newPending(scopes, false);
|
|
168
|
+
writeJson(storage, pendingKey, pending);
|
|
169
|
+
win.location.assign(await authorizeUrl(pending));
|
|
146
170
|
return new Promise(() => { });
|
|
147
171
|
};
|
|
172
|
+
const signInWithPopup = async ({ scopes, signal } = {}) => {
|
|
173
|
+
// Written before the popup opens: a popup starts with a copy of this page's sessionStorage.
|
|
174
|
+
const pending = newPending(scopes, true);
|
|
175
|
+
writeJson(storage, pendingKey, pending);
|
|
176
|
+
const popup = win.open('about:blank', key, popupFeatures(win));
|
|
177
|
+
if (!popup) {
|
|
178
|
+
writeJson(storage, pendingKey, null);
|
|
179
|
+
throw new SignInError('the browser blocked the sign-in popup', undefined, { code: 'popup-blocked' });
|
|
180
|
+
}
|
|
181
|
+
const channel = new BroadcastChannel(channelName);
|
|
182
|
+
try {
|
|
183
|
+
const returned = new Promise((resolve, reject) => {
|
|
184
|
+
channel.onmessage = (event) => {
|
|
185
|
+
const params = new URLSearchParams(typeof event.data?.search === 'string' ? event.data.search : '');
|
|
186
|
+
if (params.get('state') === pending.state)
|
|
187
|
+
resolve(params);
|
|
188
|
+
};
|
|
189
|
+
if (signal?.aborted)
|
|
190
|
+
reject(canceled());
|
|
191
|
+
signal?.addEventListener('abort', () => reject(canceled()), { once: true });
|
|
192
|
+
});
|
|
193
|
+
popup.location.replace(await authorizeUrl(pending));
|
|
194
|
+
await complete(pending, await returned);
|
|
195
|
+
error = null;
|
|
196
|
+
}
|
|
197
|
+
finally {
|
|
198
|
+
channel.close();
|
|
199
|
+
if (readJson(storage, pendingKey)?.state === pending.state)
|
|
200
|
+
writeJson(storage, pendingKey, null);
|
|
201
|
+
if (!popup.closed)
|
|
202
|
+
popup.close();
|
|
203
|
+
}
|
|
204
|
+
};
|
|
148
205
|
const params = new URLSearchParams(win.location.search);
|
|
149
206
|
const pending = readJson(storage, pendingKey);
|
|
150
207
|
if (pending && params.get('state') === pending.state) {
|
|
151
|
-
writeJson(storage, pendingKey, null);
|
|
152
|
-
const code = params.get('code');
|
|
153
208
|
const clean = new URL(win.location.href);
|
|
154
209
|
for (const key of ['code', 'state', 'error', 'error_description'])
|
|
155
210
|
clean.searchParams.delete(key);
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
redirect_uri: pending.redirectUri,
|
|
164
|
-
code_verifier: pending.verifier,
|
|
165
|
-
}, pending.scope));
|
|
211
|
+
if (pending.popup) {
|
|
212
|
+
// This is the popup: the page that opened it holds the verifier and does the exchange.
|
|
213
|
+
const channel = new BroadcastChannel(channelName);
|
|
214
|
+
channel.postMessage({ search: win.location.search });
|
|
215
|
+
channel.close();
|
|
216
|
+
win.history.replaceState(win.history.state, '', clean.toString());
|
|
217
|
+
win.close();
|
|
166
218
|
}
|
|
167
|
-
|
|
168
|
-
|
|
219
|
+
else {
|
|
220
|
+
writeJson(storage, pendingKey, null);
|
|
221
|
+
win.history.replaceState(win.history.state, '', clean.toString());
|
|
222
|
+
try {
|
|
223
|
+
await complete(pending, params);
|
|
224
|
+
}
|
|
225
|
+
catch (err) {
|
|
226
|
+
error = err instanceof SignInError ? err : new SignInError('sign-in failed', undefined, { cause: err });
|
|
227
|
+
}
|
|
169
228
|
}
|
|
170
229
|
}
|
|
171
230
|
return {
|
|
@@ -194,6 +253,7 @@ export async function createSignIn(options) {
|
|
|
194
253
|
return signIn({ scopes });
|
|
195
254
|
},
|
|
196
255
|
signIn,
|
|
256
|
+
signInWithPopup,
|
|
197
257
|
signOut: async () => {
|
|
198
258
|
const held = tokens;
|
|
199
259
|
save(null);
|
package/package.json
CHANGED