@mdbase-dev/connect 0.1.0-beta.82 → 0.1.0-beta.83

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.
@@ -1 +1 @@
1
- sha384-2NXwGD96iZvxt5iZ6vfV4ZTuoY3PG7RMPOn+wmXEq+K+i06SN/u0vdHtOUb+Mj2p
1
+ sha384-yqwzqr0WMJUcmiTnwJyLNuevsaXVJcM5YLftkDrVxhNXp+39bpKQFo/GFL7iXP8T
package/dist/connect.d.ts CHANGED
@@ -58,8 +58,5 @@ export declare class MdbaseConnectInternals<Frontmatter extends JsonObject> {
58
58
  defaultApplicationOrigin(): string;
59
59
  private pendingKey;
60
60
  private storagePrefix;
61
- private connectionsKey;
62
- private connectionIds;
63
- private addConnectionId;
64
61
  private emitConnections;
65
62
  }
package/dist/connect.js CHANGED
@@ -3,10 +3,12 @@ import { abortableDelay } from "./async.js";
3
3
  import { randomBase64Url } from "./base64.js";
4
4
  import { ApplicationIdentityStoreError, IndexedDbApplicationIdentityStore, MemoryApplicationIdentityStore, applicationIdentity, applicationInstallationId, signApplicationAuthorization } from "./application-identity.js";
5
5
  import { MdbaseConnection } from "./connection.js";
6
+ import { addConnectionId, connectionIds, removeConnectionId } from "./connection-index.js";
6
7
  import { authorizationAbort, declarationIdFromFamilyIdentity } from "./connect-authorization-helpers.js";
7
8
  import { IndexedDbGrantKeyStore, MemoryGrantKeyStore } from "./crypto.js";
8
9
  import { connectError, serverConnectError } from "./errors.js";
9
10
  import { DEFAULT_OPERATIONS } from "./internal-types.js";
11
+ import { prepareAuthorizationWindow, publishPopupAuthorizationCompletion, waitForPopupAuthorization } from "./popup-authorization.js";
10
12
  import { uniqueOperations } from "./operation-helpers.js";
11
13
  import { pendingMutationsUseKey, pendingRecoveryTransports, removePendingMutations } from "./pending-recovery-transports.js";
12
14
  import { resolveConnectTimeouts, withRequestBudget } from "./request-budget.js";
@@ -146,37 +148,28 @@ export class MdbaseConnectInternals {
146
148
  }
147
149
  }
148
150
  async authorize(options = {}) {
149
- return withRequestBudget(options, this.timeouts.requestMs, (budget) => this.authorizeWithinBudget({ ...options, signal: budget.signal, timeoutMs: null }));
151
+ const authorizationTimeout = options.presentation === "popup"
152
+ ? 10 * 60 * 1_000
153
+ : this.timeouts.requestMs;
154
+ return withRequestBudget(options, authorizationTimeout, (budget) => this.authorizeWithinBudget({ ...options, signal: budget.signal, timeoutMs: null }));
150
155
  }
151
156
  async authorizeWithinBudget(options) {
152
- if (typeof location === "undefined" && !this.navigate && !options.openVerification) {
153
- throw connectError("browser_required", "Authorization navigation requires a browser environment.");
154
- }
155
157
  const portableDeclared = typeof this.manifest !== "string"
156
158
  && this.manifest.distribution === "portable";
157
- const popup = portableDeclared
158
- && !options.openVerification
159
- && !this.navigate
160
- && typeof window !== "undefined"
161
- ? window.open("", "mdbase-connect-authorization", "popup,width=620,height=760")
162
- : null;
163
- let application;
164
- try {
165
- application = await this.registerWithinBudget(options.signal);
166
- }
167
- catch (error) {
168
- popup?.close();
169
- throw error;
170
- }
159
+ const { application, popup } = await prepareAuthorizationWindow({
160
+ options,
161
+ portableDeclared,
162
+ navigate: this.navigate,
163
+ register: (signal) => this.registerWithinBudget(signal)
164
+ });
171
165
  if (application.distribution === "portable") {
172
166
  if (options.openVerification)
173
167
  popup?.close();
174
168
  return this.authorizePortable(application, options, options.openVerification ? null : popup);
175
169
  }
176
- popup?.close();
177
- return this.authorizeWeb(application, options);
170
+ return this.authorizeWeb(application, options, popup);
178
171
  }
179
- async authorizeWeb(application, options) {
172
+ async authorizeWeb(application, options, popup) {
180
173
  const { verifier, challenge } = await createPkce();
181
174
  const state = randomBase64Url(24);
182
175
  const authorizationId = crypto.randomUUID();
@@ -231,7 +224,8 @@ export class MdbaseConnectInternals {
231
224
  keyHandle,
232
225
  authorizationId,
233
226
  applicationAgreementPublicKey: grantKey.agreementPublicKey,
234
- applicationSigningPublicKey: grantKey.signingPublicKey
227
+ applicationSigningPublicKey: grantKey.signingPublicKey,
228
+ ...(options.presentation === "popup" && popup ? { presentation: "popup" } : {})
235
229
  };
236
230
  this.storage.setItem(this.pendingKey(state), JSON.stringify(pending));
237
231
  try {
@@ -260,6 +254,20 @@ export class MdbaseConnectInternals {
260
254
  || body.expires_in <= 0) {
261
255
  throw connectError("invalid_device_authorization_response", "The authorization service returned an invalid authorization response.");
262
256
  }
257
+ if (options.presentation === "popup" && popup) {
258
+ popup.location.href = body.authorization_uri;
259
+ return await waitForPopupAuthorization({
260
+ storage: this.storage,
261
+ storagePrefix: this.storagePrefix(),
262
+ state,
263
+ pending,
264
+ popup,
265
+ signal: options.signal,
266
+ keyStore: this.keyStore,
267
+ connection: (collectionId) => this.connection(collectionId)
268
+ });
269
+ }
270
+ popup?.close();
263
271
  if (this.navigate)
264
272
  await this.navigate(body.authorization_uri);
265
273
  else if (typeof location !== "undefined")
@@ -270,6 +278,7 @@ export class MdbaseConnectInternals {
270
278
  return { kind: "redirecting" };
271
279
  }
272
280
  catch (error) {
281
+ popup?.close();
273
282
  this.storage.removeItem(this.pendingKey(state));
274
283
  await this.keyStore.delete(keyHandle);
275
284
  if (options.signal?.aborted) {
@@ -477,10 +486,19 @@ export class MdbaseConnectInternals {
477
486
  if (!state) {
478
487
  return Promise.reject(connectError("invalid_callback", "Authorization callback is missing its state."));
479
488
  }
489
+ const pending = parseStored(this.storage.getItem(this.pendingKey(state)));
490
+ const popupAttempt = pending?.presentation === "popup";
480
491
  const existing = this.completionPromises.get(state);
481
492
  if (existing)
482
493
  return existing;
483
- const completion = this.performAuthorizationCompletion(callbackUrl, state, signal);
494
+ const completion = publishPopupAuthorizationCompletion({
495
+ storage: this.storage,
496
+ storagePrefix: this.storagePrefix(),
497
+ state,
498
+ pending,
499
+ popupAttempt,
500
+ completion: this.performAuthorizationCompletion(callbackUrl, state, signal)
501
+ });
484
502
  const shared = completion.finally(() => {
485
503
  if (this.completionPromises.get(state) === shared)
486
504
  this.completionPromises.delete(state);
@@ -556,7 +574,7 @@ export class MdbaseConnectInternals {
556
574
  }
557
575
  connections() {
558
576
  const connections = [];
559
- for (const collectionId of this.connectionIds()) {
577
+ for (const collectionId of connectionIds(this.storage, this.storagePrefix())) {
560
578
  const info = this.connection(collectionId)?.info();
561
579
  if (info)
562
580
  connections.push(info);
@@ -599,7 +617,7 @@ export class MdbaseConnectInternals {
599
617
  void this.keyStore.delete(previous.keyHandle).catch(() => undefined);
600
618
  }
601
619
  this.storage.setItem(this.tokenKey(collectionId), JSON.stringify(token));
602
- this.addConnectionId(collectionId);
620
+ addConnectionId(this.storage, this.storagePrefix(), collectionId);
603
621
  this.invalidatedConnections.delete(collectionId);
604
622
  this.connectionCache.delete(collectionId);
605
623
  this.emitConnections();
@@ -643,10 +661,7 @@ export class MdbaseConnectInternals {
643
661
  for (const transport of ["web_push", "fcm"]) {
644
662
  this.storage.removeItem(this.notificationKey(collectionId, transport));
645
663
  }
646
- this.storage.setItem(this.connectionsKey(), JSON.stringify({
647
- version: 1,
648
- collectionIds: this.connectionIds().filter((id) => id !== collectionId)
649
- }));
664
+ removeConnectionId(this.storage, this.storagePrefix(), collectionId);
650
665
  this.connectionCache.delete(collectionId);
651
666
  this.emitConnections();
652
667
  }
@@ -671,22 +686,6 @@ export class MdbaseConnectInternals {
671
686
  storagePrefix() {
672
687
  return `mdbase-connect:${this.serverUrl}:${this.manifestSource}`;
673
688
  }
674
- connectionsKey() {
675
- return `${this.storagePrefix()}:connections`;
676
- }
677
- connectionIds() {
678
- const index = parseStored(this.storage.getItem(this.connectionsKey()));
679
- return index?.version === 1 && Array.isArray(index.collectionIds)
680
- && index.collectionIds.every((collectionId) => typeof collectionId === "string")
681
- ? index.collectionIds
682
- : [];
683
- }
684
- addConnectionId(collectionId) {
685
- this.storage.setItem(this.connectionsKey(), JSON.stringify({
686
- version: 1,
687
- collectionIds: [...new Set([...this.connectionIds(), collectionId])]
688
- }));
689
- }
690
689
  emitConnections() {
691
690
  const connections = this.connections();
692
691
  for (const listener of this.listeners)
@@ -0,0 +1,3 @@
1
+ export declare function connectionIds(storage: Storage, storagePrefix: string): string[];
2
+ export declare function addConnectionId(storage: Storage, storagePrefix: string, collectionId: string): void;
3
+ export declare function removeConnectionId(storage: Storage, storagePrefix: string, collectionId: string): void;
@@ -0,0 +1,20 @@
1
+ import { parseStored } from "./runtime-utils.js";
2
+ export function connectionIds(storage, storagePrefix) {
3
+ const index = parseStored(storage.getItem(`${storagePrefix}:connections`));
4
+ return index?.version === 1 && Array.isArray(index.collectionIds)
5
+ && index.collectionIds.every((collectionId) => typeof collectionId === "string")
6
+ ? index.collectionIds
7
+ : [];
8
+ }
9
+ export function addConnectionId(storage, storagePrefix, collectionId) {
10
+ writeConnectionIds(storage, storagePrefix, [...connectionIds(storage, storagePrefix), collectionId]);
11
+ }
12
+ export function removeConnectionId(storage, storagePrefix, collectionId) {
13
+ writeConnectionIds(storage, storagePrefix, connectionIds(storage, storagePrefix).filter((id) => id !== collectionId));
14
+ }
15
+ function writeConnectionIds(storage, storagePrefix, ids) {
16
+ storage.setItem(`${storagePrefix}:connections`, JSON.stringify({
17
+ version: 1,
18
+ collectionIds: [...new Set(ids)]
19
+ }));
20
+ }
@@ -17,6 +17,8 @@ export type MdbaseAuthorizationTarget = {
17
17
  };
18
18
  export interface MdbaseAuthorizeOptions extends ConnectRequestOptions {
19
19
  operations?: CollectionOperation[];
20
+ /** Keep a web application loaded while Connect owns the approval ceremony. */
21
+ presentation?: "redirect" | "popup";
20
22
  /** Choose any compatible collection, or require one exact collection. */
21
23
  target?: MdbaseAuthorizationTarget;
22
24
  /** App-local location to restore after the authorization callback. */
@@ -28,6 +30,7 @@ export interface MdbaseAuthorizeOptions extends ConnectRequestOptions {
28
30
  }
29
31
  export interface MdbaseConnectionAuthorizeOptions extends ConnectRequestOptions {
30
32
  operations?: CollectionOperation[];
33
+ presentation?: "redirect" | "popup";
31
34
  returnTo?: string;
32
35
  onDeviceCode?: (authorization: MdbaseDeviceAuthorization) => void;
33
36
  openVerification?: (authorization: MdbaseDeviceAuthorization) => void | Promise<void>;
@@ -24,7 +24,20 @@ export interface StoredAuthorization {
24
24
  authorizationId?: string;
25
25
  applicationAgreementPublicKey?: string;
26
26
  applicationSigningPublicKey?: string;
27
+ presentation?: "popup";
27
28
  }
29
+ export type StoredAuthorizationCompletion = {
30
+ version: 1;
31
+ status: "connected";
32
+ collectionId: string;
33
+ returnTo?: string;
34
+ } | {
35
+ version: 1;
36
+ status: "failed";
37
+ code: string;
38
+ message: string;
39
+ returnTo?: string;
40
+ };
28
41
  export interface StoredToken {
29
42
  version: 1;
30
43
  accessToken: string;
@@ -0,0 +1,31 @@
1
+ import type { JsonObject } from "@mdbase-dev/connect-protocol";
2
+ import type { MdbaseAuthorizeOptions, MdbaseAuthorizationOutcome, MdbaseAuthorizationResult, MdbaseConnection } from "./connection.js";
3
+ import type { GrantKeyStore } from "./crypto.js";
4
+ import type { Application, StoredAuthorization } from "./internal-types.js";
5
+ export declare function prepareAuthorizationWindow(input: {
6
+ options: MdbaseAuthorizeOptions;
7
+ portableDeclared: boolean;
8
+ navigate?: (url: string) => void | Promise<void>;
9
+ register(signal: AbortSignal): Promise<Application>;
10
+ }): Promise<{
11
+ application: Application;
12
+ popup: Window | null;
13
+ }>;
14
+ export declare function waitForPopupAuthorization<Frontmatter extends JsonObject>(input: {
15
+ storage: Storage;
16
+ storagePrefix: string;
17
+ state: string;
18
+ pending: StoredAuthorization;
19
+ popup: Window;
20
+ signal: AbortSignal;
21
+ keyStore: GrantKeyStore;
22
+ connection(collectionId: string): MdbaseConnection<Frontmatter> | null;
23
+ }): Promise<MdbaseAuthorizationOutcome<Frontmatter>>;
24
+ export declare function publishPopupAuthorizationCompletion<Frontmatter extends JsonObject>(input: {
25
+ storage: Storage;
26
+ storagePrefix: string;
27
+ state: string;
28
+ pending: StoredAuthorization | null;
29
+ popupAttempt: boolean;
30
+ completion: Promise<MdbaseAuthorizationResult<Frontmatter>>;
31
+ }): Promise<MdbaseAuthorizationResult<Frontmatter>>;
@@ -0,0 +1,100 @@
1
+ import { abortableDelay } from "./async.js";
2
+ import { MdbaseConnectError, connectError, serverConnectError } from "./errors.js";
3
+ import { parseStored } from "./runtime-utils.js";
4
+ export async function prepareAuthorizationWindow(input) {
5
+ if (typeof location === "undefined" && !input.navigate && !input.options.openVerification) {
6
+ throw connectError("browser_required", "Authorization navigation requires a browser environment.");
7
+ }
8
+ const popupRequested = input.options.presentation === "popup" || input.portableDeclared;
9
+ const popup = popupRequested
10
+ && !input.options.openVerification
11
+ && !input.navigate
12
+ && typeof window !== "undefined"
13
+ ? window.open("", "mdbase-connect-authorization", "popup,width=620,height=760")
14
+ : null;
15
+ try {
16
+ return {
17
+ application: await input.register(input.options.signal),
18
+ popup
19
+ };
20
+ }
21
+ catch (error) {
22
+ popup?.close();
23
+ throw error;
24
+ }
25
+ }
26
+ export async function waitForPopupAuthorization(input) {
27
+ const completionKey = authorizationCompletionKey(input.storagePrefix, input.state);
28
+ try {
29
+ while (true) {
30
+ const completion = parseStored(input.storage.getItem(completionKey));
31
+ if (completion?.version === 1) {
32
+ input.storage.removeItem(completionKey);
33
+ if (completion.status === "failed") {
34
+ throw serverConnectError(completion.code, completion.message, {
35
+ details: completion.returnTo ? { return_to: completion.returnTo } : undefined
36
+ });
37
+ }
38
+ const connection = input.connection(completion.collectionId);
39
+ if (!connection) {
40
+ throw connectError("invalid_callback", "Authorization completed without a usable saved connection.");
41
+ }
42
+ return {
43
+ kind: "connected",
44
+ connection,
45
+ ...(completion.returnTo ? { returnTo: completion.returnTo } : {})
46
+ };
47
+ }
48
+ // Cross-origin opener isolation may report a live Connect window as
49
+ // closed. Shared callback state, abort, or the request budget owns exit.
50
+ await abortableDelay(200, input.signal);
51
+ }
52
+ }
53
+ finally {
54
+ input.popup.close();
55
+ input.storage.removeItem(completionKey);
56
+ const pendingKey = `${input.storagePrefix}:pending:${input.state}`;
57
+ const outstanding = parseStored(input.storage.getItem(pendingKey));
58
+ if (outstanding?.state === input.state) {
59
+ input.storage.removeItem(pendingKey);
60
+ if (input.pending.keyHandle)
61
+ await input.keyStore.delete(input.pending.keyHandle);
62
+ }
63
+ }
64
+ }
65
+ export function publishPopupAuthorizationCompletion(input) {
66
+ return input.completion.then((result) => {
67
+ if (input.popupAttempt)
68
+ writeCompletion(input, {
69
+ version: 1,
70
+ status: "connected",
71
+ collectionId: result.connection.collectionId,
72
+ ...(result.returnTo ? { returnTo: result.returnTo } : {})
73
+ });
74
+ if (input.popupAttempt)
75
+ closeCallbackWindow();
76
+ return result;
77
+ }).catch((error) => {
78
+ if (input.popupAttempt)
79
+ writeCompletion(input, {
80
+ version: 1,
81
+ status: "failed",
82
+ code: error instanceof MdbaseConnectError ? error.code : "invalid_callback",
83
+ message: error instanceof Error ? error.message : "Authorization could not be completed.",
84
+ ...(input.pending?.returnTo ? { returnTo: input.pending.returnTo } : {})
85
+ });
86
+ if (input.popupAttempt)
87
+ closeCallbackWindow();
88
+ throw error;
89
+ });
90
+ }
91
+ function writeCompletion(input, completion) {
92
+ input.storage.setItem(authorizationCompletionKey(input.storagePrefix, input.state), JSON.stringify(completion));
93
+ }
94
+ function authorizationCompletionKey(storagePrefix, state) {
95
+ return `${storagePrefix}:completion:${state}`;
96
+ }
97
+ function closeCallbackWindow() {
98
+ if (typeof window !== "undefined" && typeof window.close === "function")
99
+ window.close();
100
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mdbase-dev/connect",
3
- "version": "0.1.0-beta.82",
3
+ "version": "0.1.0-beta.83",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -33,7 +33,7 @@
33
33
  "README.md"
34
34
  ],
35
35
  "dependencies": {
36
- "@mdbase-dev/connect-protocol": "0.1.0-beta.82"
36
+ "@mdbase-dev/connect-protocol": "0.1.0-beta.83"
37
37
  },
38
38
  "devDependencies": {
39
39
  "esbuild": "^0.28.1",