@partylayer/adapter-bron 0.6.0 → 0.7.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 CHANGED
@@ -42,12 +42,55 @@ npm install @partylayer/adapter-bron
42
42
  ## Usage with SDK
43
43
 
44
44
  ```typescript
45
- import { createPartyLayer } from '@partylayer/sdk';
45
+ import { createPartyLayer, type BronAdapterConfig } from '@partylayer/sdk';
46
+
47
+ // Supply the OAuth endpoints, public client ID, registered redirect URI and
48
+ // signer API URL issued for your application by Bron.
49
+ export function createBronClient(bron: BronAdapterConfig) {
50
+ return createPartyLayer({
51
+ network: 'devnet',
52
+ app: { name: 'My Canton dApp' },
53
+ bron,
54
+ });
55
+ }
56
+ ```
57
+
58
+ Bron requires application-owned OAuth and signer API configuration. Without
59
+ `bron` configuration, Bron is omitted from the wallet picker. After constructing
60
+ the client with your approved configuration, find Bron in `await client.listWallets()`
61
+ and pass that entry's `walletId` to `client.connect()` from a user action in the browser.
62
+
63
+ Use PKCE for browser flows and never expose a client secret in frontend code.
64
+ Register the callback URL with Bron and follow their onboarding instructions;
65
+ this package does not provision a Bron account or credentials.
66
+
67
+ ### Registered callback page
46
68
 
47
- const client = createPartyLayer({ network: 'devnet' });
48
- await client.connect({ walletId: 'bron' });
69
+ For the explicitly configured legacy OAuth contract, the application's registered
70
+ same-origin redirect page relays the authorization response to the popup opener:
71
+
72
+ ```ts
73
+ import { completeBronAuthorization } from '@partylayer/sdk';
74
+
75
+ await completeBronAuthorization('https://your-app.example/oauth/bron/callback');
49
76
  ```
50
77
 
78
+ Use the exact registered URL. Load this helper only on that dedicated callback
79
+ page; avoid analytics and other third-party scripts there. The opener verifies
80
+ the popup source, origin, redirect path, single-use state and S256 PKCE, then
81
+ exchanges the code. Browser client secrets and non-PKCE browser configuration are
82
+ rejected. Authorization is bounded, tokens remain in memory, and disconnect
83
+ invalidates pending authorization. No new PartyLayer wallet package is required.
84
+
85
+ This compatibility configuration requires a vendor-approved OAuth/signer API
86
+ contract. Bron's current [public API authentication documentation](https://developer.bron.org/api-reference/authentication)
87
+ describes signed API-key JWTs; it does not establish a universal per-dApp OAuth
88
+ client or a delegated submission right on an application's participant. Do not
89
+ put those API private keys in browser configuration or infer a submission route
90
+ from a message-signing capability. The callback helper fixes the local OAuth
91
+ completion path; vendor credentials, endpoint compatibility and live acceptance
92
+ remain independently validated integration requirements.
93
+
51
94
  ---
52
95
 
53
96
  ## Links
package/dist/index.d.mts CHANGED
@@ -27,7 +27,7 @@ interface BronAuthConfig {
27
27
  redirectUri: string;
28
28
  /** Scopes */
29
29
  scopes?: string[];
30
- /** Use PKCE (recommended for browser) */
30
+ /** PKCE defaults to true and is required in browsers. */
31
31
  usePKCE?: boolean;
32
32
  }
33
33
 
@@ -77,10 +77,12 @@ declare class BronAdapter implements WalletAdapter {
77
77
  readonly name = "Bron";
78
78
  private authClient;
79
79
  private apiClient;
80
+ private redirectUri;
81
+ private connecting;
80
82
  constructor(config: BronAdapterConfig);
81
83
  getCapabilities(): CapabilityKey[];
82
84
  detectInstalled(): Promise<AdapterDetectResult>;
83
- connect(ctx: AdapterContext, _opts?: {
85
+ connect(ctx: AdapterContext, opts?: {
84
86
  timeoutMs?: number;
85
87
  requiredCapabilities?: CapabilityKey[];
86
88
  }): Promise<AdapterConnectResult>;
@@ -97,4 +99,11 @@ declare class BronAdapter implements WalletAdapter {
97
99
  ledgerApi(ctx: AdapterContext, session: _partylayer_core.Session, params: LedgerApiParams): Promise<LedgerApiResult>;
98
100
  }
99
101
 
100
- export { BronAdapter, type BronAdapterApiConfig, type BronAdapterConfig, type BronApiConfig, type BronAuthConfig };
102
+ /**
103
+ * Call from the application's registered redirect page. The popup relays only
104
+ * to its own origin; the opener verifies the source, path, state and PKCE.
105
+ * This helper does not register a Bron OAuth client or define vendor endpoints.
106
+ */
107
+ declare function completeBronAuthorization(redirectUri: string): void;
108
+
109
+ export { BronAdapter, type BronAdapterApiConfig, type BronAdapterConfig, type BronApiConfig, type BronAuthConfig, completeBronAuthorization };
package/dist/index.d.ts CHANGED
@@ -27,7 +27,7 @@ interface BronAuthConfig {
27
27
  redirectUri: string;
28
28
  /** Scopes */
29
29
  scopes?: string[];
30
- /** Use PKCE (recommended for browser) */
30
+ /** PKCE defaults to true and is required in browsers. */
31
31
  usePKCE?: boolean;
32
32
  }
33
33
 
@@ -77,10 +77,12 @@ declare class BronAdapter implements WalletAdapter {
77
77
  readonly name = "Bron";
78
78
  private authClient;
79
79
  private apiClient;
80
+ private redirectUri;
81
+ private connecting;
80
82
  constructor(config: BronAdapterConfig);
81
83
  getCapabilities(): CapabilityKey[];
82
84
  detectInstalled(): Promise<AdapterDetectResult>;
83
- connect(ctx: AdapterContext, _opts?: {
85
+ connect(ctx: AdapterContext, opts?: {
84
86
  timeoutMs?: number;
85
87
  requiredCapabilities?: CapabilityKey[];
86
88
  }): Promise<AdapterConnectResult>;
@@ -97,4 +99,11 @@ declare class BronAdapter implements WalletAdapter {
97
99
  ledgerApi(ctx: AdapterContext, session: _partylayer_core.Session, params: LedgerApiParams): Promise<LedgerApiResult>;
98
100
  }
99
101
 
100
- export { BronAdapter, type BronAdapterApiConfig, type BronAdapterConfig, type BronApiConfig, type BronAuthConfig };
102
+ /**
103
+ * Call from the application's registered redirect page. The popup relays only
104
+ * to its own origin; the opener verifies the source, path, state and PKCE.
105
+ * This helper does not register a Bron OAuth client or define vendor endpoints.
106
+ */
107
+ declare function completeBronAuthorization(redirectUri: string): void;
108
+
109
+ export { BronAdapter, type BronAdapterApiConfig, type BronAdapterConfig, type BronApiConfig, type BronAuthConfig, completeBronAuthorization };
package/dist/index.js CHANGED
@@ -10,15 +10,32 @@ var BronAuthClient = class {
10
10
  storage;
11
11
  tokens = null;
12
12
  // In-memory by default
13
+ pending = null;
14
+ exchange = null;
15
+ authorization = new AbortController();
16
+ epoch = 0;
17
+ storageWrites = Promise.resolve();
18
+ persist(action) {
19
+ const write = this.storageWrites.catch(() => {
20
+ }).then(action);
21
+ this.storageWrites = write;
22
+ return write;
23
+ }
13
24
  constructor(config, storage) {
14
- this.config = config;
25
+ for (const input of [config.authorizationUrl, config.tokenUrl, config.redirectUri]) {
26
+ const url = new URL(input);
27
+ if (url.username || url.password || url.hash || url.protocol !== "https:" && !(url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname))) throw new Error("OAuth requires trusted HTTPS URLs");
28
+ }
29
+ if (!config.clientId?.trim()) throw new Error("OAuth clientId is required");
30
+ if (typeof window !== "undefined" && (config.clientSecret || config.usePKCE === false)) throw new Error("Browser OAuth requires PKCE and must not contain a client secret");
31
+ this.config = { ...config, usePKCE: config.usePKCE ?? true };
15
32
  this.storage = storage;
16
33
  }
17
34
  /**
18
35
  * Generate PKCE code verifier and challenge
19
36
  */
20
37
  async generatePKCE() {
21
- const verifier = this.generateRandomString(128);
38
+ const verifier = this.generateRandomString(32);
22
39
  const encoder = new TextEncoder();
23
40
  const data = encoder.encode(verifier);
24
41
  const digest = await crypto.subtle.digest("SHA-256", data);
@@ -37,6 +54,9 @@ var BronAuthClient = class {
37
54
  * Start authorization flow
38
55
  */
39
56
  async startAuth() {
57
+ this.cancelAuthorization();
58
+ this.authorization = new AbortController();
59
+ const epoch = this.epoch;
40
60
  const state = this.generateRandomString(32);
41
61
  const params = new URLSearchParams({
42
62
  response_type: "code",
@@ -49,94 +69,108 @@ var BronAuthClient = class {
49
69
  const pkce = await this.generatePKCE();
50
70
  params.set("code_challenge", pkce.challenge);
51
71
  params.set("code_challenge_method", "S256");
52
- if (typeof window !== "undefined") {
53
- sessionStorage.setItem("bron_pkce_verifier", pkce.verifier);
54
- sessionStorage.setItem("bron_auth_state", state);
55
- }
72
+ if (epoch !== this.epoch) throw new Error("OAuth authorization cancelled");
73
+ this.pending = { state, verifier: pkce.verifier, expiresAt: Date.now() + 3e5 };
56
74
  } else {
57
- if (typeof window !== "undefined") {
58
- sessionStorage.setItem("bron_auth_state", state);
59
- }
75
+ this.pending = { state, expiresAt: Date.now() + 3e5 };
60
76
  }
61
- const authUrl = `${this.config.authorizationUrl}?${params.toString()}`;
62
- return authUrl;
77
+ const url = new URL(this.config.authorizationUrl);
78
+ for (const [key, value] of params) url.searchParams.set(key, value);
79
+ return url.href;
63
80
  }
64
81
  /**
65
82
  * Finish authorization flow with callback URL
66
83
  */
67
84
  async finishAuth(callbackUrl) {
68
85
  const url = new URL(callbackUrl);
86
+ const redirect = new URL(this.config.redirectUri);
87
+ const pending = this.pending;
88
+ if (url.origin !== redirect.origin || url.pathname !== redirect.pathname || url.hash || url.username || url.password || [...redirect.searchParams].some(([key, value]) => url.searchParams.getAll(key).length !== 1 || url.searchParams.get(key) !== value) || !pending || pending.expiresAt <= Date.now() || url.searchParams.getAll("state").length !== 1 || url.searchParams.get("state") !== pending.state) throw new Error("Invalid or expired OAuth callback state");
89
+ this.pending = null;
90
+ if (url.searchParams.has("error")) throw new Error("OAuth authorization was declined or failed");
69
91
  const code = url.searchParams.get("code");
70
- const state = url.searchParams.get("state");
71
- const error = url.searchParams.get("error");
72
- if (error) {
73
- throw new Error(`OAuth error: ${error}`);
74
- }
75
- if (!code) {
76
- throw new Error("No authorization code in callback");
77
- }
78
- if (typeof window !== "undefined") {
79
- const storedState = sessionStorage.getItem("bron_auth_state");
80
- if (state !== storedState) {
81
- throw new Error("State mismatch");
82
- }
83
- }
92
+ if (!code || url.searchParams.getAll("code").length !== 1) throw new Error("No unique authorization code in callback");
84
93
  const tokenParams = new URLSearchParams({
85
94
  grant_type: "authorization_code",
86
95
  code,
87
96
  redirect_uri: this.config.redirectUri,
88
97
  client_id: this.config.clientId
89
98
  });
90
- if (this.config.usePKCE && typeof window !== "undefined") {
91
- const verifier = sessionStorage.getItem("bron_pkce_verifier");
92
- if (verifier) {
93
- tokenParams.set("code_verifier", verifier);
94
- sessionStorage.removeItem("bron_pkce_verifier");
95
- }
96
- }
97
- if (this.config.clientSecret) {
98
- tokenParams.set("client_secret", this.config.clientSecret);
99
- }
100
- const response = await fetch(this.config.tokenUrl, {
101
- method: "POST",
102
- headers: {
103
- "Content-Type": "application/x-www-form-urlencoded"
104
- },
105
- body: tokenParams.toString()
106
- });
107
- if (!response.ok) {
108
- const error2 = await response.text();
109
- throw new Error(`Token exchange failed: ${error2}`);
110
- }
111
- const tokenData = await response.json();
112
- const tokens = {
113
- accessToken: tokenData.access_token,
114
- refreshToken: tokenData.refresh_token,
115
- expiresAt: Date.now() + tokenData.expires_in * 1e3,
116
- tokenType: tokenData.token_type || "Bearer"
117
- };
118
- this.tokens = tokens;
119
- if (this.storage) {
120
- await this.storage.set("bron_tokens", JSON.stringify(tokens));
99
+ if (this.config.usePKCE) {
100
+ if (!pending.verifier) throw new Error("Missing PKCE verifier");
101
+ tokenParams.set("code_verifier", pending.verifier);
121
102
  }
122
- if (typeof window !== "undefined") {
123
- sessionStorage.removeItem("bron_auth_state");
103
+ if (this.config.clientSecret) tokenParams.set("client_secret", this.config.clientSecret);
104
+ const epoch = this.epoch;
105
+ const controller = new AbortController();
106
+ this.exchange = controller;
107
+ const timer = setTimeout(() => controller.abort(), 15e3);
108
+ try {
109
+ const response = await fetch(this.config.tokenUrl, {
110
+ method: "POST",
111
+ headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
112
+ body: tokenParams.toString(),
113
+ credentials: "omit",
114
+ cache: "no-store",
115
+ redirect: "error",
116
+ referrerPolicy: "no-referrer",
117
+ signal: controller.signal
118
+ });
119
+ if (!response.ok) throw new Error("OAuth token exchange failed");
120
+ const raw = await response.text();
121
+ if (raw.length > 256e3) throw new Error("OAuth token response is too large");
122
+ const data = JSON.parse(raw);
123
+ if (!data || typeof data.access_token !== "string" || !data.access_token || data.access_token.length > 64e3 || /[\s\p{Cc}]/u.test(data.access_token) || typeof data.token_type !== "string" || data.token_type.toLowerCase() !== "bearer" || typeof data.expires_in !== "number" || !Number.isFinite(data.expires_in) || data.expires_in <= 0 || data.expires_in > 31536e3 || data.refresh_token !== void 0 && (typeof data.refresh_token !== "string" || !data.refresh_token || /\p{Cc}/u.test(data.refresh_token))) {
124
+ throw new Error("Invalid OAuth token response");
125
+ }
126
+ if (controller.signal.aborted || epoch !== this.epoch) throw new Error("OAuth authorization cancelled");
127
+ const tokens = {
128
+ accessToken: data.access_token,
129
+ expiresAt: Date.now() + data.expires_in * 1e3,
130
+ tokenType: "Bearer",
131
+ ...data.refresh_token ? { refreshToken: data.refresh_token } : {}
132
+ };
133
+ this.tokens = tokens;
134
+ if (this.storage) await this.persist(async () => {
135
+ if (epoch === this.epoch) await this.storage.set("bron_tokens", JSON.stringify(tokens));
136
+ });
137
+ if (epoch !== this.epoch) throw new Error("OAuth authorization cancelled");
138
+ return tokens;
139
+ } finally {
140
+ clearTimeout(timer);
141
+ if (this.exchange === controller) this.exchange = null;
124
142
  }
125
- return tokens;
143
+ }
144
+ /** Clear authorization state and abort only the in-flight token exchange. */
145
+ cancelAuthorization() {
146
+ this.authorization.abort();
147
+ this.epoch++;
148
+ this.pending = null;
149
+ this.exchange?.abort();
150
+ this.exchange = null;
151
+ }
152
+ getAuthorizationSignal() {
153
+ return this.authorization.signal;
126
154
  }
127
155
  /**
128
156
  * Get current access token
129
157
  */
130
158
  async getAccessToken() {
159
+ const epoch = this.epoch;
131
160
  if (this.tokens && Date.now() < this.tokens.expiresAt) {
132
161
  return this.tokens.accessToken;
133
162
  }
134
163
  if (this.storage) {
135
164
  const stored = await this.storage.get("bron_tokens");
165
+ if (epoch !== this.epoch) return null;
136
166
  if (stored) {
137
- this.tokens = JSON.parse(stored);
138
- if (Date.now() < this.tokens.expiresAt) {
139
- return this.tokens.accessToken;
167
+ try {
168
+ const tokens = JSON.parse(stored);
169
+ if (typeof tokens.accessToken === "string" && tokens.accessToken && !/[\s\p{Cc}]/u.test(tokens.accessToken) && tokens.tokenType === "Bearer" && Number.isFinite(tokens.expiresAt) && Date.now() < tokens.expiresAt) {
170
+ this.tokens = tokens;
171
+ return tokens.accessToken;
172
+ }
173
+ } catch {
140
174
  }
141
175
  }
142
176
  }
@@ -146,9 +180,10 @@ var BronAuthClient = class {
146
180
  * Logout
147
181
  */
148
182
  async logout() {
183
+ this.cancelAuthorization();
149
184
  this.tokens = null;
150
185
  if (this.storage) {
151
- await this.storage.remove("bron_tokens");
186
+ await this.persist(() => this.storage.remove("bron_tokens"));
152
187
  }
153
188
  }
154
189
  };
@@ -270,6 +305,61 @@ var BronApiClient = class {
270
305
  throw new Error("Polling timeout");
271
306
  }
272
307
  };
308
+ var CALLBACK = "partylayer:bron:authorization-callback";
309
+ function completeBronAuthorization(redirectUri) {
310
+ if (typeof window === "undefined" || !window.opener) throw new Error("Open this OAuth callback from the connecting application");
311
+ const expected = new URL(redirectUri);
312
+ if (window.location.origin !== expected.origin || window.location.pathname !== expected.pathname) throw new Error("Unexpected OAuth redirect page");
313
+ const callbackUrl = window.location.href;
314
+ window.opener.postMessage({ type: CALLBACK, callbackUrl }, expected.origin);
315
+ window.history.replaceState(null, "", expected.pathname);
316
+ window.close();
317
+ }
318
+ async function authorizeBronPopup(auth, redirectUri, timeoutMs) {
319
+ if (typeof window === "undefined") throw new Error("OAuth flow requires a browser environment");
320
+ if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 3e5) throw new Error("Invalid OAuth timeout");
321
+ const expected = new URL(redirectUri);
322
+ if (expected.origin !== window.location.origin) throw new Error("Register a same-origin OAuth callback page");
323
+ const url = await auth.startAuth();
324
+ const popup = window.open(url, "partylayer-bron-authorization", "width=500,height=650");
325
+ if (!popup) {
326
+ auth.cancelAuthorization();
327
+ throw new Error("Allow the wallet authorization popup and try again");
328
+ }
329
+ return new Promise((resolve, reject) => {
330
+ const signal = auth.getAuthorizationSignal();
331
+ let callbackReceived = false;
332
+ let finished = false;
333
+ const finish = (error) => {
334
+ if (finished) return;
335
+ finished = true;
336
+ clearTimeout(timer);
337
+ clearInterval(closed);
338
+ window.removeEventListener("message", receive);
339
+ signal.removeEventListener("abort", cancel);
340
+ if (error) auth.cancelAuthorization();
341
+ try {
342
+ popup.close();
343
+ } catch {
344
+ }
345
+ if (error) reject(error);
346
+ else resolve();
347
+ };
348
+ const receive = (event) => {
349
+ if (finished || callbackReceived || event.source !== popup || event.origin !== expected.origin || !event.data || event.data.type !== CALLBACK || typeof event.data.callbackUrl !== "string") return;
350
+ callbackReceived = true;
351
+ void auth.finishAuth(event.data.callbackUrl).then(() => finish(), (error) => finish(error));
352
+ };
353
+ const cancel = () => finish(new Error("Wallet authorization cancelled"));
354
+ const timer = setTimeout(() => finish(new Error("Wallet authorization timed out")), timeoutMs);
355
+ const closed = setInterval(() => {
356
+ if (!callbackReceived && popup.closed) finish(new core.UserRejectedError("Wallet authorization was closed"));
357
+ }, 250);
358
+ window.addEventListener("message", receive);
359
+ signal.addEventListener("abort", cancel, { once: true });
360
+ if (signal.aborted) cancel();
361
+ });
362
+ }
273
363
 
274
364
  // src/bron-adapter.ts
275
365
  var BronAdapter = class {
@@ -277,8 +367,11 @@ var BronAdapter = class {
277
367
  name = "Bron";
278
368
  authClient;
279
369
  apiClient;
370
+ redirectUri;
371
+ connecting = false;
280
372
  constructor(config) {
281
373
  this.authClient = new BronAuthClient(config.auth);
374
+ this.redirectUri = config.auth.redirectUri;
282
375
  this.apiClient = new BronApiClient({
283
376
  baseUrl: config.api.baseUrl,
284
377
  getAccessToken: async () => {
@@ -308,23 +401,13 @@ var BronAdapter = class {
308
401
  reason: "Bron is a remote signer service"
309
402
  });
310
403
  }
311
- async connect(ctx, _opts) {
404
+ async connect(ctx, opts) {
405
+ if (this.connecting) throw new Error("A Bron connection is already pending");
406
+ this.connecting = true;
312
407
  try {
313
408
  const accessToken = await this.authClient.getAccessToken();
314
409
  if (!accessToken) {
315
- if (typeof window === "undefined") {
316
- throw new Error("OAuth flow requires browser environment");
317
- }
318
- const authUrl = await this.authClient.startAuth();
319
- const popup = window.open(
320
- authUrl,
321
- "Bron Auth",
322
- "width=500,height=600"
323
- );
324
- if (!popup) {
325
- throw new Error("Failed to open auth popup");
326
- }
327
- throw new Error("OAuth callback not implemented in adapter - handle in app");
410
+ await authorizeBronPopup(this.authClient, this.redirectUri, opts?.timeoutMs ?? 12e4);
328
411
  }
329
412
  const session = await this.apiClient.createSession();
330
413
  return {
@@ -350,6 +433,8 @@ var BronAdapter = class {
350
433
  phase: "connect",
351
434
  transport: "remote"
352
435
  });
436
+ } finally {
437
+ this.connecting = false;
353
438
  }
354
439
  }
355
440
  async disconnect(_ctx, _session) {
@@ -497,5 +582,6 @@ var BronAdapter = class {
497
582
  };
498
583
 
499
584
  exports.BronAdapter = BronAdapter;
585
+ exports.completeBronAuthorization = completeBronAuthorization;
500
586
  //# sourceMappingURL=index.js.map
501
587
  //# sourceMappingURL=index.js.map