@andco/sdk 0.0.3 → 0.0.4

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.
Files changed (82) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +93 -49
  3. package/dist/auth.d.ts +5 -3
  4. package/dist/auth.d.ts.map +1 -1
  5. package/dist/auth.js +10 -17
  6. package/dist/browser/controller.d.ts +46 -0
  7. package/dist/browser/controller.d.ts.map +1 -1
  8. package/dist/browser/controller.js +102 -12
  9. package/dist/browser/frame.d.ts.map +1 -1
  10. package/dist/browser/frame.js +6 -15
  11. package/dist/browser/index.d.ts +8 -2
  12. package/dist/browser/index.d.ts.map +1 -1
  13. package/dist/browser/index.js +48 -33
  14. package/dist/browser/popup.d.ts +4 -0
  15. package/dist/browser/popup.d.ts.map +1 -1
  16. package/dist/browser/popup.js +23 -7
  17. package/dist/cli/index.d.ts +4 -6
  18. package/dist/cli/index.d.ts.map +1 -1
  19. package/dist/cli/index.js +7 -13
  20. package/dist/cli/server.d.ts +5 -0
  21. package/dist/cli/server.d.ts.map +1 -1
  22. package/dist/cli/server.js +5 -0
  23. package/dist/client.d.ts +66 -19
  24. package/dist/client.d.ts.map +1 -1
  25. package/dist/client.js +117 -78
  26. package/dist/config.d.ts +2 -1
  27. package/dist/config.d.ts.map +1 -1
  28. package/dist/config.js +1 -0
  29. package/dist/credentials.d.ts +58 -4
  30. package/dist/credentials.d.ts.map +1 -1
  31. package/dist/credentials.js +0 -0
  32. package/dist/errors.d.ts +23 -21
  33. package/dist/errors.d.ts.map +1 -1
  34. package/dist/errors.js +18 -20
  35. package/dist/globals.d.ts +25 -0
  36. package/dist/globals.d.ts.map +1 -0
  37. package/dist/globals.js +15 -0
  38. package/dist/grants-api.d.ts +34 -0
  39. package/dist/grants-api.d.ts.map +1 -0
  40. package/dist/grants-api.js +48 -0
  41. package/dist/grants.d.ts +16 -0
  42. package/dist/grants.d.ts.map +1 -0
  43. package/dist/grants.js +13 -0
  44. package/dist/index.d.ts +12 -6
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +8 -3
  47. package/dist/inflight.d.ts +31 -0
  48. package/dist/inflight.d.ts.map +1 -0
  49. package/dist/inflight.js +26 -0
  50. package/dist/intents.d.ts +346 -70
  51. package/dist/intents.d.ts.map +1 -1
  52. package/dist/intents.js +629 -112
  53. package/dist/oauth.d.ts +55 -6
  54. package/dist/oauth.d.ts.map +1 -1
  55. package/dist/oauth.js +86 -57
  56. package/dist/presenter.d.ts +25 -11
  57. package/dist/presenter.d.ts.map +1 -1
  58. package/dist/presenter.js +39 -6
  59. package/dist/resource.d.ts +109 -0
  60. package/dist/resource.d.ts.map +1 -0
  61. package/dist/resource.js +151 -0
  62. package/dist/rest.d.ts +23 -4
  63. package/dist/rest.d.ts.map +1 -1
  64. package/dist/rest.js +46 -5
  65. package/dist/server/index.d.ts +3 -0
  66. package/dist/server/index.d.ts.map +1 -1
  67. package/dist/server/index.js +1 -0
  68. package/dist/server-metadata.generated.d.ts.map +1 -1
  69. package/dist/server-metadata.generated.js +8 -4
  70. package/dist/service.d.ts +109 -0
  71. package/dist/service.d.ts.map +1 -0
  72. package/dist/service.js +241 -0
  73. package/dist/session-store.d.ts +12 -4
  74. package/dist/session-store.d.ts.map +1 -1
  75. package/dist/session-store.js +64 -14
  76. package/dist/storage.d.ts +16 -23
  77. package/dist/storage.d.ts.map +1 -1
  78. package/dist/storage.js +27 -25
  79. package/dist/tokens.d.ts +46 -0
  80. package/dist/tokens.d.ts.map +1 -0
  81. package/dist/tokens.js +148 -0
  82. package/package.json +13 -3
@@ -1,6 +1,34 @@
1
1
  import { ANDCO_ERROR_CODES as ANDCO_PROTOCOL_ERROR_CODES, ANDCO_RELEASE, callbackStateCreate } from "@andco/protocol";
2
2
  import { ANDCO_ERROR_CODES, AndcoError } from "../errors.js";
3
3
  import { AndcoButtonBridge } from "./frame.js";
4
+ /**
5
+ * Sandbox restrictions the hosted iframe grants: scripts to run, same-origin to reach its own
6
+ * storage, and a popup that may escape the sandbox because it is a separate top-level browsing
7
+ * context the sandbox does not otherwise constrain.
8
+ *
9
+ * This is a containment boundary, not a per-binding style choice, so it is declared exactly once
10
+ * here rather than copied into every framework binding.
11
+ */
12
+ const IFRAME_SANDBOX = "allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox";
13
+ /** Keeps the Project's own URL out of the request the hosted iframe makes. */
14
+ const IFRAME_REFERRER_POLICY = "no-referrer";
15
+ /** The part of the default iframe style every binding shares regardless of size. */
16
+ const IFRAME_BASE_STYLE = { border: "0", display: "block" };
17
+ /**
18
+ * The hosted control's default box.
19
+ *
20
+ * A bare iframe is 300x150 with a border, and the hosted control fills whatever box it is given, so
21
+ * without a default an application sees a bordered grey slab. React, Vue, and Svelte apply this
22
+ * directly to the `<iframe>` they render.
23
+ */
24
+ const DEFAULT_SIZE = { height: "3rem", width: "18rem" };
25
+ const IFRAME_DEFAULT_STYLE = { ...IFRAME_BASE_STYLE, ...DEFAULT_SIZE };
26
+ /** Renders a style record — property names only, no camelCase — as inline CSS text. */
27
+ function frameStyleText(style) {
28
+ return Object.entries(style)
29
+ .map(([property, value]) => `${property}:${value}`)
30
+ .join(";");
31
+ }
4
32
  /**
5
33
  * Coordinates one hosted Andco iframe without owning how a framework renders it.
6
34
  *
@@ -11,6 +39,15 @@ import { AndcoButtonBridge } from "./frame.js";
11
39
  * Render `snapshot.iframeURL` in your platform's own iframe element, then hand that element to
12
40
  * `connect()` once it mounts.
13
41
  *
42
+ * The frame's fixed attributes and default sizing are published as static data —
43
+ * {@link AndcoButtonController.IFRAME_SANDBOX}, {@link AndcoButtonController.IFRAME_REFERRER_POLICY},
44
+ * {@link AndcoButtonController.IFRAME_DEFAULT_STYLE}, and {@link AndcoButtonController.frameProps} for
45
+ * the snapshot-derived `src`/`hidden` pair — so every binding applies the same contract to its own
46
+ * element instead of redeclaring it. `sdk-script`'s custom element is the one exception on sizing: it
47
+ * sizes itself through a stylesheet layer (using {@link AndcoButtonController.DEFAULT_SIZE}, so the
48
+ * visible default stays the same) and stretches its internal iframe to fill it, so that an author's
49
+ * own CSS on `andco-button` wins without touching the iframe's inline style.
50
+ *
14
51
  * @example
15
52
  * ```ts
16
53
  * const controller = new AndcoButtonController({ client: andco, onComplete: reload });
@@ -19,6 +56,31 @@ import { AndcoButtonBridge } from "./frame.js";
19
56
  * ```
20
57
  */
21
58
  export class AndcoButtonController {
59
+ /** @see {@link IFRAME_SANDBOX} */
60
+ static IFRAME_SANDBOX = IFRAME_SANDBOX;
61
+ /** @see {@link IFRAME_REFERRER_POLICY} */
62
+ static IFRAME_REFERRER_POLICY = IFRAME_REFERRER_POLICY;
63
+ /** @see {@link IFRAME_BASE_STYLE} */
64
+ static IFRAME_BASE_STYLE = IFRAME_BASE_STYLE;
65
+ /** @see {@link DEFAULT_SIZE} */
66
+ static DEFAULT_SIZE = DEFAULT_SIZE;
67
+ /** @see {@link IFRAME_DEFAULT_STYLE} */
68
+ static IFRAME_DEFAULT_STYLE = IFRAME_DEFAULT_STYLE;
69
+ /**
70
+ * Renders a style record as inline CSS text, for a binding that needs a string rather than an
71
+ * object — Svelte's `style` attribute and `sdk-script`'s `HTMLElement.style.cssText`.
72
+ */
73
+ static frameStyleText = frameStyleText;
74
+ /**
75
+ * The two iframe attributes a snapshot drives, mapped from protocol terms to DOM ones.
76
+ *
77
+ * Every binding applies this on top of {@link AndcoButtonController.IFRAME_SANDBOX} and
78
+ * {@link AndcoButtonController.IFRAME_REFERRER_POLICY}: those two are fixed, this pair changes
79
+ * with the connection.
80
+ */
81
+ static frameProps(snapshot) {
82
+ return { src: snapshot.iframeURL, hidden: !snapshot.ready };
83
+ }
22
84
  #options;
23
85
  #listeners = new Set();
24
86
  #bridge;
@@ -51,7 +113,7 @@ export class AndcoButtonController {
51
113
  connect(iframe) {
52
114
  this.disconnect();
53
115
  this.#iframe = iframe;
54
- console.debug("[AndcoButtonController] connecting: %o", { iframeURL: this.#snapshot.iframeURL });
116
+ console.debug("[AndcoButtonController] connecting: %s", this.#snapshot.iframeURL);
55
117
  this.#publish({ status: "connecting", ready: false, error: null });
56
118
  try {
57
119
  this.#bridge = new AndcoButtonBridge(iframe, {
@@ -81,7 +143,7 @@ export class AndcoButtonController {
81
143
  const timedOut = event.error.code === ANDCO_PROTOCOL_ERROR_CODES.HANDSHAKE_TIMEOUT;
82
144
  if (timedOut && !this.#timedOutOnce && this.#iframe) {
83
145
  this.#timedOutOnce = true;
84
- console.debug("[AndcoButtonController] handshake timed out", { action: "reconnect", retry: 1 });
146
+ console.debug("[AndcoButtonController] handshake timed out; reconnecting once");
85
147
  this.connect(this.#iframe);
86
148
  return;
87
149
  }
@@ -94,6 +156,7 @@ export class AndcoButtonController {
94
156
  }
95
157
  return () => this.disconnect();
96
158
  }
159
+ /** Detaches the iframe bridge while retaining listeners and the last iframe for reuse. */
97
160
  disconnect() {
98
161
  this.#bridge?.destroy();
99
162
  this.#bridge = undefined;
@@ -114,14 +177,13 @@ export class AndcoButtonController {
114
177
  // A deliberate retry (the app calling this after showing the error) earns its own one-shot
115
178
  // auto-reconnect on a future timeout, same as the very first connect did.
116
179
  this.#timedOutOnce = false;
117
- console.debug("[AndcoButtonController] restarting after an error, reconnecting: %o", {
118
- code: this.#snapshot.error?.code,
119
- });
180
+ console.debug("[AndcoButtonController] restarting after an error, reconnecting: %O", this.#snapshot.error);
120
181
  this.connect(this.#iframe);
121
182
  return;
122
183
  }
123
184
  this.#publish({ status: this.#bridge ? "ready" : "idle", error: null });
124
185
  }
186
+ /** Releases the bridge, iframe reference, and all snapshot listeners. */
125
187
  destroy() {
126
188
  this.disconnect();
127
189
  this.#iframe = undefined;
@@ -144,6 +206,8 @@ export class AndcoButtonController {
144
206
  const state = callbackStateCreate(this.#options.client.config.endpoints.widget.origin, presentationId);
145
207
  if (this.#options.flow === "intent")
146
208
  return this.#presentIntent(generation, presentationId, state);
209
+ if (this.#options.presentation === "redirect")
210
+ return this.#presentRedirect(generation, presentationId);
147
211
  const prepared = await this.#options.client.oauth.createAuthorizationRequest({
148
212
  ...(this.#options.authorization ?? {}),
149
213
  state,
@@ -155,11 +219,35 @@ export class AndcoButtonController {
155
219
  this.#pending = prepared.data;
156
220
  this.#bridge?.resolvePresentation(presentationId, prepared.data.authorizationUrl.href);
157
221
  }
222
+ /**
223
+ * Presents the authorization by replacing this document.
224
+ *
225
+ * The hosted surface cannot do it: its sandbox grants no top navigation, so a URL resolved back to
226
+ * the iframe would only reach a popup that was never opened. The instance's own sign-in already
227
+ * knows how to redirect — it prepares the request, persists the transaction for the next load, and
228
+ * assigns the location — so the control delegates to it rather than repeating those steps. Nothing
229
+ * settles here on success: the document is gone, and the session arrives through Callback
230
+ * Consumption on the next load.
231
+ */
232
+ async #presentRedirect(generation, presentationId) {
233
+ console.debug("[AndcoButtonController] presenting by redirect: presentationId=%s", presentationId);
234
+ const signedIn = await this.#options.client.auth.signIn({
235
+ ...(this.#options.authorization ?? {}),
236
+ presentation: "redirect",
237
+ });
238
+ if (generation !== this.#generation)
239
+ return;
240
+ if (signedIn.error) {
241
+ // Tell the surface the presentation is over so it leaves its busy state; there is no popup to close.
242
+ this.#bridge?.resolvePresentation(presentationId);
243
+ return this.#fail(signedIn.error);
244
+ }
245
+ }
158
246
  /**
159
247
  * Resolves the Intent this control presents, reusing the instance's own URL construction.
160
248
  *
161
249
  * It does not build the URL itself. Two constructions of one presentation URL is exactly how the
162
- * direct and hosted paths drifted before, which is why `intents.presentationURL` is published.
250
+ * direct and hosted paths drifted before, which is why `intents.presentationURLFor` is published.
163
251
  */
164
252
  async #presentIntent(generation, presentationId, state) {
165
253
  const client = this.#options.client;
@@ -189,19 +277,21 @@ export class AndcoButtonController {
189
277
  catch (cause) {
190
278
  return this.#fail(AndcoError.from(cause, ANDCO_ERROR_CODES.INVALID_CONFIGURATION));
191
279
  }
192
- if (generation !== this.#generation)
280
+ if (generation !== this.#generation) {
193
281
  return;
194
- const url = await client.intents.presentationURL({
195
- intentId: resolvedId,
282
+ }
283
+ const url = await client.intents.presentationURLFor(resolvedId, {
196
284
  presentation: "popup",
197
285
  returnTo,
198
286
  errorReturnTo: returnTo,
199
287
  state,
200
288
  });
201
- if (generation !== this.#generation)
289
+ if (generation !== this.#generation) {
202
290
  return;
203
- if (url.error)
291
+ }
292
+ else if (url.error) {
204
293
  return this.#fail(url.error);
294
+ }
205
295
  this.#intentId = resolvedId;
206
296
  this.#bridge?.resolvePresentation(presentationId, url.data.href);
207
297
  }
@@ -243,7 +333,7 @@ export class AndcoButtonController {
243
333
  #fail(error, event) {
244
334
  // An app that never wired `onError` must not lose the failure entirely — a button that just sits
245
335
  // there disabled, with nothing in the console, is what made the last one of these hard to find.
246
- console.error("[AndcoButtonController] %s: %o", error.code, { message: error.message, details: error.details });
336
+ console.error("[AndcoButtonController] %O", error);
247
337
  this.#publish({ status: "error", error });
248
338
  this.#options.onError?.(error, event);
249
339
  }
@@ -1 +1 @@
1
- {"version":3,"file":"frame.d.ts","sourceRoot":"","sources":["../../src/browser/frame.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,eAAe,EACpB,KAAK,eAAe,EAEpB,KAAK,wBAAwB,EAC7B,KAAK,uBAAuB,EAC5B,KAAK,gCAAgC,EACrC,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,EAC3B,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EAErB,KAAK,eAAe,EACpB,KAAK,0BAA0B,EAEhC,MAAM,iBAAiB,CAAC;AAGzB,iEAAiE;AACjE,MAAM,MAAM,iBAAiB,GAAG;IAC9B,cAAc,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAC;IAC3C,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAC;IAC3C,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,gCAAgC,GAAG,uBAAuB,GAAG,wBAAwB,KAAK,IAAI,CAAC;IACpH,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,sBAAsB,GAAG,uBAAuB,KAAK,IAAI,CAAC;IAC9E,oBAAoB,CAAC,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,CAAC;IAC7D,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAC;CAC/C,CAAC;AAEF;;;;;;;;GAQG;AACH,qBAAa,iBAAiB;IAY1B,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAZ1B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAM;IAC1B,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,SAAS,CAA4C;IAC7D,OAAO,CAAC,IAAI,CAA0B;IACtC,OAAO,CAAC,cAAc,CAAqB;IAC3C,OAAO,CAAC,gBAAgB,CAAqB;IAC7C,OAAO,CAAC,iBAAiB,CAAK;IAC9B,OAAO,CAAC,kBAAkB,CAAK;IAC/B,OAAO,CAAC,SAAS,CAAS;gBAGP,MAAM,EAAE,iBAAiB,EACzB,OAAO,EAAE,iBAAiB;IAkB7C,OAAO,CAAC,aAAa;IAWrB,OAAO,CAAC,SAAS;IAKjB,OAAO,CAAC,OAAO;IAkDf,OAAO,CAAC,QAAQ,CAAC,aAAa,CAM5B;IAEF,OAAO,CAAC,QAAQ,CAAC,UAAU,CA8BzB;IAEF,OAAO,CAAC,QAAQ,CAAC,cAAc,CA0B7B;IAEF,+DAA+D;IAC/D,OAAO,CAAC,IAAI;IASZ;;;;;;;;;OASG;IACI,mBAAmB,CAAC,cAAc,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,GAAG;IAWrE;;;;;;OAMG;IACI,0BAA0B,CAAC,OAAO,EAAE,gBAAgB;IAI3D,oEAAoE;IAC7D,cAAc,CAAC,WAAW,EAAE,IAAI,CAAC,0BAA0B,EAAE,QAAQ,GAAG,WAAW,CAAC;IAU3F,0EAA0E;IACnE,OAAO;CASf"}
1
+ {"version":3,"file":"frame.d.ts","sourceRoot":"","sources":["../../src/browser/frame.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,eAAe,EACpB,KAAK,eAAe,EAEpB,KAAK,wBAAwB,EAC7B,KAAK,uBAAuB,EAC5B,KAAK,gCAAgC,EACrC,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,EAC3B,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EAErB,KAAK,eAAe,EACpB,KAAK,0BAA0B,EAEhC,MAAM,iBAAiB,CAAC;AAGzB,iEAAiE;AACjE,MAAM,MAAM,iBAAiB,GAAG;IAC9B,cAAc,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAC;IAC3C,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAC;IAC3C,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,gCAAgC,GAAG,uBAAuB,GAAG,wBAAwB,KAAK,IAAI,CAAC;IACpH,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,sBAAsB,GAAG,uBAAuB,KAAK,IAAI,CAAC;IAC9E,oBAAoB,CAAC,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,CAAC;IAC7D,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAC;CAC/C,CAAC;AAEF;;;;;;;;GAQG;AACH,qBAAa,iBAAiB;IAY1B,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAZ1B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAM;IAC1B,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,SAAS,CAA4C;IAC7D,OAAO,CAAC,IAAI,CAA0B;IACtC,OAAO,CAAC,cAAc,CAAqB;IAC3C,OAAO,CAAC,gBAAgB,CAAqB;IAC7C,OAAO,CAAC,iBAAiB,CAAK;IAC9B,OAAO,CAAC,kBAAkB,CAAK;IAC/B,OAAO,CAAC,SAAS,CAAS;gBAGP,MAAM,EAAE,iBAAiB,EACzB,OAAO,EAAE,iBAAiB;IAkB7C,OAAO,CAAC,aAAa;IAWrB,OAAO,CAAC,SAAS;IAKjB,OAAO,CAAC,OAAO;IAmDf,OAAO,CAAC,QAAQ,CAAC,aAAa,CAM5B;IAEF,OAAO,CAAC,QAAQ,CAAC,UAAU,CAgCzB;IAEF,OAAO,CAAC,QAAQ,CAAC,cAAc,CA2B7B;IAEF,+DAA+D;IAC/D,OAAO,CAAC,IAAI;IASZ;;;;;;;;;OASG;IACI,mBAAmB,CAAC,cAAc,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,GAAG;IAWrE;;;;;;OAMG;IACI,0BAA0B,CAAC,OAAO,EAAE,gBAAgB;IAI3D,oEAAoE;IAC7D,cAAc,CAAC,WAAW,EAAE,IAAI,CAAC,0BAA0B,EAAE,QAAQ,GAAG,WAAW,CAAC;IAU3F,0EAA0E;IACnE,OAAO;CASf"}
@@ -34,7 +34,7 @@ export class AndcoButtonBridge {
34
34
  if (this.url.origin === window.location.origin) {
35
35
  throw new AndcoError(ANDCO_ERROR_CODES.SAME_ORIGIN_ENDPOINT, { details: { endpointButton: this.url.href } });
36
36
  }
37
- console.debug("[AndcoButtonBridge] created: %o", { endpointButton: this.url.href });
37
+ console.debug("[AndcoButtonBridge] created: %s", this.url.href);
38
38
  iframe.addEventListener("load", this.startHandshake);
39
39
  this.startHandshake();
40
40
  }
@@ -62,11 +62,7 @@ export class AndcoButtonBridge {
62
62
  return;
63
63
  this.settled = true;
64
64
  this.stopHandshake();
65
- console.debug("[AndcoButtonBridge] handshake settled: %o", {
66
- attempts: this.handshakeAttempts,
67
- transport: this.transport,
68
- elapsedMs: Date.now() - this.handshakeStartedAt,
69
- });
65
+ console.debug("[AndcoButtonBridge] handshake settled: attempts=%d transport=%s elapsedMs=%d", this.handshakeAttempts, this.transport, Date.now() - this.handshakeStartedAt);
70
66
  this.options.onReady?.(message);
71
67
  return;
72
68
  }
@@ -75,7 +71,7 @@ export class AndcoButtonBridge {
75
71
  this.settled = true;
76
72
  this.stopHandshake();
77
73
  }
78
- console.warn("[AndcoButtonBridge] hosted surface reported an error: %o", message.error);
74
+ console.warn("[AndcoButtonBridge] hosted surface reported an error: %O", message.error);
79
75
  this.options.onError?.(message);
80
76
  return;
81
77
  }
@@ -142,9 +138,7 @@ export class AndcoButtonBridge {
142
138
  // makes the very first attempt throw a targetOrigin mismatch; the 250ms retry loop recovers
143
139
  // once the iframe has actually navigated, so a transient failure here is not fatal — but it is
144
140
  // worth a log line, since a run that never recovers looks identical to this from the outside.
145
- console.debug("[AndcoButtonBridge] init attempt %d could not post yet, retrying: %o", this.handshakeAttempts, {
146
- cause,
147
- });
141
+ console.debug("[AndcoButtonBridge] init attempt %d could not post yet, retrying: %O", this.handshakeAttempts, cause);
148
142
  }
149
143
  };
150
144
  startHandshake = () => {
@@ -160,10 +154,7 @@ export class AndcoButtonBridge {
160
154
  this.settled = true;
161
155
  this.stopHandshake();
162
156
  this.closePort();
163
- console.error("[AndcoButtonBridge] handshake timed out: %o", {
164
- attempts: this.handshakeAttempts,
165
- endpointButton: this.url.href,
166
- });
157
+ console.error("[AndcoButtonBridge] handshake timed out: attempts=%d endpointButton=%s", this.handshakeAttempts, this.url.href);
167
158
  const message = {
168
159
  protocol: ANDCO_SDK_PROTOCOL,
169
160
  version: ANDCO_PROTOCOL_VERSION,
@@ -228,7 +219,7 @@ export class AndcoButtonBridge {
228
219
  destroy() {
229
220
  if (this.destroyed)
230
221
  return;
231
- console.debug("[AndcoButtonBridge] destroyed: %o", { settled: this.settled, attempts: this.handshakeAttempts });
222
+ console.debug("[AndcoButtonBridge] destroyed: settled=%s attempts=%d", this.settled, this.handshakeAttempts);
232
223
  this.destroyed = true;
233
224
  window.removeEventListener("message", this.receiveWindow);
234
225
  this.iframe.removeEventListener("load", this.startHandshake);
@@ -1,6 +1,7 @@
1
1
  import { AndcoClient, type AndcoClientOptions } from "../client.js";
2
+ import { Result } from "../errors.js";
2
3
  import type { AndcoAuthorizationRequest } from "../oauth.js";
3
- import { type AndcoPresenter } from "../presenter.js";
4
+ import { AndcoPresenter, type AndcoPresentOptions } from "../presenter.js";
4
5
  export { AndcoButtonController, type AndcoButtonControllerOptions, type AndcoButtonFlow, type AndcoButtonSnapshot, type AndcoButtonStatus, } from "./controller.js";
5
6
  export { AndcoButtonBridge, type AndcoFrameOptions } from "./frame.js";
6
7
  export { type AndcoPopupMessage, type AndcoPopupWindow, type AndcoRelayOutcome, andcoPopupPresentationId, isAndcoNativeHost, openAndcoPopup, openAndcoPopupWindow, relayAndcoPopupCallback, } from "./popup.js";
@@ -17,7 +18,12 @@ export { type AndcoPopupMessage, type AndcoPopupWindow, type AndcoRelayOutcome,
17
18
  * an authorization may be pushed, and an Intent is created on demand. Resolving first is what got
18
19
  * the window blocked.
19
20
  */
20
- export declare function browserPresenter(): AndcoPresenter;
21
+ export declare class AndcoPresenterBrowser extends AndcoPresenter {
22
+ #private;
23
+ focus(presentationId: string): void;
24
+ present(options: AndcoPresentOptions): Promise<Result<URL | null>>;
25
+ }
26
+ /** Public-client options for browser sign-in, callback relay, and transaction recovery. */
21
27
  export type AndcoBrowserOptions = Omit<AndcoClientOptions, "presenter" | "source" | "clientSecret"> & {
22
28
  /** Rejected by type: a confidential credential must never reach a browser. */
23
29
  clientSecret?: never;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/browser/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,KAAK,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAIpE,OAAO,KAAK,EAAE,yBAAyB,EAAc,MAAM,aAAa,CAAC;AACzE,OAAO,EAAE,KAAK,cAAc,EAAkB,MAAM,iBAAiB,CAAC;AAItE,OAAO,EACL,qBAAqB,EACrB,KAAK,4BAA4B,EACjC,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,GACvB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,iBAAiB,EAAE,KAAK,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACvE,OAAO,EACL,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,wBAAwB,EACxB,iBAAiB,EACjB,cAAc,EACd,oBAAoB,EACpB,uBAAuB,GACxB,MAAM,YAAY,CAAC;AAEpB;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,IAAI,cAAc,CAqCjD;AAED,MAAM,MAAM,mBAAmB,GAAG,IAAI,CAAC,kBAAkB,EAAE,WAAW,GAAG,QAAQ,GAAG,cAAc,CAAC,GAAG;IACpG,8EAA8E;IAC9E,YAAY,CAAC,EAAE,KAAK,CAAC;IACrB,SAAS,CAAC,EAAE,cAAc,CAAC;IAC3B,iGAAiG;IACjG,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,uGAAuG;IACvG,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,uEAAuE;IACvE,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,CAAC;IAChC,+FAA+F;IAC/F,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B,CAAC;AAIF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,mBAAmB,GAAG,WAAW,CAmBvF;AA0CD,6FAA6F;AAC7F,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,yBAAyB,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,IAAI,CAMnH"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/browser/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,KAAK,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAGpE,OAAO,EAAqB,MAAM,EAAE,MAAM,cAAc,CAAC;AACzD,OAAO,KAAK,EAAE,yBAAyB,EAAc,MAAM,aAAa,CAAC;AACzE,OAAO,EAAE,cAAc,EAAE,KAAK,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAI3E,OAAO,EACL,qBAAqB,EACrB,KAAK,4BAA4B,EACjC,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,GACvB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,iBAAiB,EAAE,KAAK,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACvE,OAAO,EACL,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,wBAAwB,EACxB,iBAAiB,EACjB,cAAc,EACd,oBAAoB,EACpB,uBAAuB,GACxB,MAAM,YAAY,CAAC;AAEpB;;;;;;;;;;;;GAYG;AACH,qBAAa,qBAAsB,SAAQ,cAAc;;IAI9C,KAAK,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI;IAKtC,OAAO,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,GAAG,IAAI,CAAC,CAAC;CA8CzE;AAED,2FAA2F;AAC3F,MAAM,MAAM,mBAAmB,GAAG,IAAI,CAAC,kBAAkB,EAAE,WAAW,GAAG,QAAQ,GAAG,cAAc,CAAC,GAAG;IACpG,8EAA8E;IAC9E,YAAY,CAAC,EAAE,KAAK,CAAC;IACrB,SAAS,CAAC,EAAE,cAAc,CAAC;IAC3B,iGAAiG;IACjG,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,uGAAuG;IACvG,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,uEAAuE;IACvE,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,CAAC;IAChC,+FAA+F;IAC/F,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B,CAAC;AAIF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,mBAAmB,GAAG,WAAW,CAkBvF;AA0CD,6FAA6F;AAC7F,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,yBAAyB,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,IAAI,CAMnH"}
@@ -1,7 +1,7 @@
1
1
  import { AndcoClient } from "../client.js";
2
2
  import { ANDCO_ERROR_CODES, Result } from "../errors.js";
3
- import { PRESENT_TARGET } from "../presenter.js";
4
- import { WebStorage } from "../storage.js";
3
+ import { AndcoPresenter } from "../presenter.js";
4
+ import { LocalStorage } from "../storage.js";
5
5
  import { andcoPopupPresentationId, isAndcoNativeHost, openAndcoPopupWindow, relayAndcoPopupCallback } from "./popup.js";
6
6
  export { AndcoButtonController, } from "./controller.js";
7
7
  export { AndcoButtonBridge } from "./frame.js";
@@ -19,42 +19,57 @@ export { andcoPopupPresentationId, isAndcoNativeHost, openAndcoPopup, openAndcoP
19
19
  * an authorization may be pushed, and an Intent is created on demand. Resolving first is what got
20
20
  * the window blocked.
21
21
  */
22
- export function browserPresenter() {
23
- return {
24
- async present(options) {
25
- if (typeof window === "undefined")
26
- return Result.fail(ANDCO_ERROR_CODES.BROWSER_REQUIRED);
27
- if (options.kind === "oauth" && options.presentation === "redirect") {
28
- const resolved = await PRESENT_TARGET(options.url);
29
- if (resolved.error)
30
- return Result.fail(resolved.error);
31
- window.location.assign(resolved.data.href);
32
- // The document is being replaced. The result arrives on the next load, not here.
33
- return Result.ok(null);
34
- }
35
- if (isAndcoNativeHost() && window.__ANDCO_NATIVE_POPUP_HANDLER__ !== true) {
36
- return Result.fail(ANDCO_ERROR_CODES.PRESENTATION_UNSUPPORTED, {
37
- message: "the native Host cannot present a popup without navigating the Miniapp",
38
- });
22
+ export class AndcoPresenterBrowser extends AndcoPresenter {
23
+ /** Windows still open, by Presentation ID, so {@link AndcoPresenterBrowser.focus} can find them. */
24
+ #open = new Map();
25
+ focus(presentationId) {
26
+ console.debug("[AndcoPresenterBrowser.focus] %s open=%s", presentationId, this.#open.has(presentationId));
27
+ this.#open.get(presentationId)?.focus();
28
+ }
29
+ async present(options) {
30
+ if (typeof window === "undefined")
31
+ return Result.fail(ANDCO_ERROR_CODES.BROWSER_REQUIRED);
32
+ if (options.presentation === "redirect") {
33
+ const resolved = await AndcoPresenter.present(options.url);
34
+ if (resolved.error) {
35
+ console.error("[AndcoPresenterBrowser] for options %O got error: %O", options, resolved.error);
36
+ return Result.fail(resolved.error);
39
37
  }
40
- const popup = openAndcoPopupWindow({
41
- presentationId: options.presentationId,
42
- expectedOrigin: options.returnTo.origin,
43
- signal: options.signal,
38
+ console.debug("[AndcoPresenterBrowser] will window location assign: %s", resolved.data.href);
39
+ window.location.assign(resolved.data.href);
40
+ // The document is being replaced. The result arrives on the next load, not here.
41
+ return Result.ok(null);
42
+ }
43
+ if (isAndcoNativeHost() && window["__ANDCO_NATIVE_POPUP_HANDLER__"] !== true) {
44
+ return Result.fail(ANDCO_ERROR_CODES.PRESENTATION_UNSUPPORTED, {
45
+ message: "the native Host cannot present a popup without navigating the Miniapp",
44
46
  });
45
- // Abandon before the server work: a blocked window that still started an authorization or
46
- // created an Intent would leave a transaction nobody can complete.
47
- if (popup.blocked)
48
- return Result.fail(ANDCO_ERROR_CODES.POPUP_BLOCKED);
49
- const resolved = await PRESENT_TARGET(options.url);
47
+ }
48
+ const popup = openAndcoPopupWindow({
49
+ presentationId: options.presentationId,
50
+ expectedOrigin: options.returnTo.origin,
51
+ signal: options.signal,
52
+ target: options.kind === "intent" && options.presentation === "newtab" ? "newtab" : "popup",
53
+ });
54
+ // Abandon before the server work: a blocked window that still started an authorization or
55
+ // created an Intent would leave a transaction nobody can complete.
56
+ if (popup.blocked) {
57
+ return Result.fail(ANDCO_ERROR_CODES.POPUP_BLOCKED);
58
+ }
59
+ this.#open.set(options.presentationId, popup);
60
+ try {
61
+ const resolved = await AndcoPresenter.present(options.url);
50
62
  if (resolved.error) {
51
63
  popup.close();
52
64
  return Result.fail(resolved.error);
53
65
  }
54
66
  popup.navigate(resolved.data);
55
- return popup.result;
56
- },
57
- };
67
+ return await popup.result;
68
+ }
69
+ finally {
70
+ this.#open.delete(options.presentationId);
71
+ }
72
+ }
58
73
  }
59
74
  const TRANSACTION_KEY = "andco.transaction";
60
75
  /**
@@ -79,8 +94,8 @@ const TRANSACTION_KEY = "andco.transaction";
79
94
  * ```
80
95
  */
81
96
  export function createAndcoInstanceForBrowser(options) {
82
- const presenter = options.presenter ?? browserPresenter();
83
- const storage = options.storage ?? (typeof window === "undefined" ? undefined : new WebStorage(window.sessionStorage));
97
+ const presenter = options.presenter ?? new AndcoPresenterBrowser();
98
+ const storage = options.storage ?? (typeof window === "undefined" ? undefined : new LocalStorage());
84
99
  // Annotated because `source` closes over `client`; the closure only runs on the first read.
85
100
  const client = new AndcoClient({
86
101
  ...options,
@@ -74,6 +74,8 @@ export type AndcoPopupWindow = {
74
74
  navigate(url: URL): void;
75
75
  /** Settles with the relayed callback, `null` on dismissal, or an error. */
76
76
  readonly result: Promise<Result<URL | null>>;
77
+ /** Brings the window to the front; a browser may decline when no user activation is current. */
78
+ focus(): void;
77
79
  close(): void;
78
80
  };
79
81
  /**
@@ -95,6 +97,8 @@ export declare function openAndcoPopupWindow(options: {
95
97
  expectedOrigin: string;
96
98
  url?: URL;
97
99
  signal?: AbortSignal;
100
+ /** `"newtab"` opens a tab instead of a sized window; the relay back is the same. Default `"popup"`. */
101
+ target?: "popup" | "newtab";
98
102
  }): AndcoPopupWindow;
99
103
  /** Opens a popup straight to its destination and waits for the relayed callback. */
100
104
  export declare function openAndcoPopup(options: {
@@ -1 +1 @@
1
- {"version":3,"file":"popup.d.ts","sourceRoot":"","sources":["../../src/browser/popup.ts"],"names":[],"mappings":"AAOA,OAAO,EAAqB,UAAU,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAMrE,mGAAmG;AACnG,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,CAAC,CAAC;IACX,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,mEAAmE;AACnE,MAAM,MAAM,iBAAiB,GACzB;IAAE,MAAM,EAAE,cAAc,CAAA;CAAE,GAC1B;IAAE,MAAM,EAAE,WAAW,CAAA;CAAE,GACvB;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAAE,GAC7C;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,UAAU,CAAA;CAAE,CAAC;AAE9C,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,uEAAuE;QACvE,qBAAqB,CAAC,EAAE,OAAO,CAAC;QAChC,sFAAsF;QACtF,8BAA8B,CAAC,EAAE,OAAO,CAAC;QACzC,kBAAkB,CAAC,EAAE;YAAE,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;SAAE,CAAC;KAC7D;CACF;AAED,yEAAyE;AACzE,wBAAgB,iBAAiB,IAAI,OAAO,CAM3C;AAED;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,IAAI,MAAM,GAAG,IAAI,CAMxD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,uBAAuB,CACrC,cAAc,EAAE,WAAW,CAAC,MAAM,CAAC,EACnC,OAAO,GAAE;IACP;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,GAAG,CAAC;CACvB,GACL,iBAAiB,CAyCnB;AA6BD,sFAAsF;AACtF,MAAM,MAAM,gBAAgB,GAAG;IAC7B;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,6DAA6D;IAC7D,QAAQ,CAAC,GAAG,EAAE,GAAG,GAAG,IAAI,CAAC;IACzB,2EAA2E;IAC3E,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC;IAC7C,KAAK,IAAI,IAAI,CAAC;CACf,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE;IAC5C,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,GAAG,CAAC,EAAE,GAAG,CAAC;IACV,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GAAG,gBAAgB,CA+DnB;AAED,oFAAoF;AACpF,wBAAgB,cAAc,CAAC,OAAO,EAAE;IACtC,GAAG,EAAE,GAAG,CAAC;IACT,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,GAAG,IAAI,CAAC,CAAC,CAE9B"}
1
+ {"version":3,"file":"popup.d.ts","sourceRoot":"","sources":["../../src/browser/popup.ts"],"names":[],"mappings":"AAOA,OAAO,EAAqB,UAAU,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAMrE,mGAAmG;AACnG,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,CAAC,CAAC;IACX,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,mEAAmE;AACnE,MAAM,MAAM,iBAAiB,GACzB;IAAE,MAAM,EAAE,cAAc,CAAA;CAAE,GAC1B;IAAE,MAAM,EAAE,WAAW,CAAA;CAAE,GACvB;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAAE,GAC7C;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,UAAU,CAAA;CAAE,CAAC;AAE9C,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,uEAAuE;QACvE,qBAAqB,CAAC,EAAE,OAAO,CAAC;QAChC,sFAAsF;QACtF,8BAA8B,CAAC,EAAE,OAAO,CAAC;QACzC,kBAAkB,CAAC,EAAE;YAAE,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;SAAE,CAAC;KAC7D;CACF;AAED,yEAAyE;AACzE,wBAAgB,iBAAiB,IAAI,OAAO,CAM3C;AAED;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,IAAI,MAAM,GAAG,IAAI,CAMxD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,uBAAuB,CACrC,cAAc,EAAE,WAAW,CAAC,MAAM,CAAC,EACnC,OAAO,GAAE;IACP;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,GAAG,CAAC;CACvB,GACL,iBAAiB,CAyCnB;AAoCD,sFAAsF;AACtF,MAAM,MAAM,gBAAgB,GAAG;IAC7B;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,6DAA6D;IAC7D,QAAQ,CAAC,GAAG,EAAE,GAAG,GAAG,IAAI,CAAC;IACzB,2EAA2E;IAC3E,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC;IAC7C,gGAAgG;IAChG,KAAK,IAAI,IAAI,CAAC;IACd,KAAK,IAAI,IAAI,CAAC;CACf,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE;IAC5C,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,GAAG,CAAC,EAAE,GAAG,CAAC;IACV,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,uGAAuG;IACvG,MAAM,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;CAC7B,GAAG,gBAAgB,CAsEnB;AAED,oFAAoF;AACpF,wBAAgB,cAAc,CAAC,OAAO,EAAE;IACtC,GAAG,EAAE,GAAG,CAAC;IACT,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,GAAG,IAAI,CAAC,CAAC,CAE9B"}
@@ -84,18 +84,22 @@ function protocolResult(presentationId, url) {
84
84
  // An Intent reports a lifecycle outcome and nothing else. Whether money moved is read from the
85
85
  // Resource Server afterwards, never carried in a window message.
86
86
  const intent = url.searchParams.get("andco_intent_result");
87
- if (intent === "complete")
87
+ if (intent === "complete") {
88
88
  return { ...envelope, type: "intent.complete" };
89
- if (intent)
89
+ }
90
+ else if (intent) {
90
91
  return { ...envelope, type: "intent.dismiss" };
92
+ }
91
93
  const code = url.searchParams.get("code");
92
- if (code)
94
+ if (code) {
93
95
  return { ...envelope, type: "oauth.authorization_code", authorizationCode: code };
96
+ }
94
97
  const error = url.searchParams.get("error") ?? "invalid_request";
95
98
  // `access_denied` is the only OAuth error that carries a decision, and it is the one the
96
99
  // Authorization Server returns when the user declines.
97
- if (error === "access_denied")
100
+ if (error === "access_denied") {
98
101
  return { ...envelope, type: "oauth.dismiss" };
102
+ }
99
103
  const description = url.searchParams.get("error_description");
100
104
  return {
101
105
  ...envelope,
@@ -118,12 +122,19 @@ function protocolResult(presentationId, url) {
118
122
  * ```
119
123
  */
120
124
  export function openAndcoPopupWindow(options) {
121
- const popup = window.open(options.url?.href ?? "about:blank", `${ANDCO_POPUP_NAME_PREFIX}${options.presentationId}`, windowFeatures({ width: 620, height: 720 }));
125
+ const name = `${ANDCO_POPUP_NAME_PREFIX}${options.presentationId}`;
126
+ const href = options.url?.href ?? "about:blank";
127
+ console.debug("[openAndcoPopupWindow] opening %s %s", options.target ?? "popup", name);
128
+ // Without window features a browser opens a tab; with them, a popup window.
129
+ const popup = options.target === "newtab"
130
+ ? window.open(href, name)
131
+ : window.open(href, name, windowFeatures({ width: 620, height: 720 }));
122
132
  if (!popup) {
123
133
  return {
124
134
  blocked: true,
125
135
  navigate: () => { },
126
136
  result: Promise.resolve(Result.fail(ANDCO_ERROR_CODES.POPUP_BLOCKED)),
137
+ focus: () => { },
127
138
  close: () => { },
128
139
  };
129
140
  }
@@ -167,15 +178,20 @@ export function openAndcoPopupWindow(options) {
167
178
  popup.close();
168
179
  settle(Result.fail(ANDCO_ERROR_CODES.POPUP_TIMEOUT));
169
180
  }, POPUP_TIMEOUT_MS);
170
- options.signal?.addEventListener("abort", () => {
181
+ const abort = () => {
171
182
  popup.close();
172
183
  settle(Result.ok(null));
173
- });
184
+ };
185
+ if (options.signal?.aborted)
186
+ abort();
187
+ else
188
+ options.signal?.addEventListener("abort", abort, { once: true });
174
189
  });
175
190
  return {
176
191
  blocked: false,
177
192
  navigate: (url) => popup.location.assign(url.href),
178
193
  result,
194
+ focus: () => popup.focus(),
179
195
  close: () => popup.close(),
180
196
  };
181
197
  }
@@ -1,6 +1,7 @@
1
1
  import { AndcoClient, type AndcoClientOptions } from "../client.js";
2
2
  import { Result } from "../errors.js";
3
- import { type AndcoPresenter, type AndcoPresentOptions } from "../presenter.js";
3
+ import { AndcoPresenter, type AndcoPresentOptions } from "../presenter.js";
4
+ /** Browser launch, terminal output, and callback timeout overrides for CLI authorization. */
4
5
  export type AndcoCliPresenterOptions = {
5
6
  /** Opens the authorization URL. Defaults to printing it, so nothing is assumed about the host. */
6
7
  open?: (url: URL) => void | Promise<void>;
@@ -12,10 +13,6 @@ export type AndcoCliPresenterOptions = {
12
13
  /**
13
14
  * Presents an authorization from a terminal, using a one-shot loopback receiver.
14
15
  *
15
- * Intents are refused rather than mishandled: a terminal has no surface on which a person can
16
- * review and confirm a financial operation, and rewriting `redirect_uri` — which is what this
17
- * presenter does — is meaningless for an Intent.
18
- *
19
16
  * Every command-line integrator needs this exact thing, and until now every one wrote it: bind a
20
17
  * loopback port, refuse anything that is not the registered path, check the state, answer once, and
21
18
  * shut down. The Andco CLI's own version is ninety lines. Getting any of it wrong — accepting a
@@ -32,11 +29,12 @@ export type AndcoCliPresenterOptions = {
32
29
  * const { data: session } = await andco.auth.signIn();
33
30
  * ```
34
31
  */
35
- export declare class AndcoCliPresenter implements AndcoPresenter {
32
+ export declare class AndcoPresenterCli extends AndcoPresenter {
36
33
  private readonly options;
37
34
  constructor(options?: AndcoCliPresenterOptions);
38
35
  present(presentation: AndcoPresentOptions): Promise<Result<URL | null>>;
39
36
  }
37
+ /** Instance configuration with an optional CLI authorization presenter. */
40
38
  export type AndcoCliOptions = Omit<AndcoClientOptions, "presenter"> & {
41
39
  presenter?: AndcoPresenter;
42
40
  };
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,KAAK,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEpE,OAAO,EAAqB,MAAM,EAAE,MAAM,cAAc,CAAC;AACzD,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,mBAAmB,EAAkB,MAAM,iBAAiB,CAAC;AAGhG,MAAM,MAAM,wBAAwB,GAAG;IACrC,kGAAkG;IAClG,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1C,wEAAwE;IACxE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gFAAgF;IAChF,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,iBAAkB,YAAW,cAAc;IAC1C,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,GAAE,wBAA6B;IAE7D,OAAO,CAAC,YAAY,EAAE,mBAAmB,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,GAAG,IAAI,CAAC,CAAC;CAwC9E;AAED,MAAM,MAAM,eAAe,GAAG,IAAI,CAAC,kBAAkB,EAAE,WAAW,CAAC,GAAG;IACpE,SAAS,CAAC,EAAE,cAAc,CAAC;CAC5B,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,eAAe,GAAG,WAAW,CAE/E"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,KAAK,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEpE,OAAO,EAAqB,MAAM,EAAE,MAAM,cAAc,CAAC;AACzD,OAAO,EAAE,cAAc,EAAE,KAAK,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAG3E,6FAA6F;AAC7F,MAAM,MAAM,wBAAwB,GAAG;IACrC,kGAAkG;IAClG,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1C,wEAAwE;IACxE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gFAAgF;IAChF,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC,CAAC;AAEF;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAAa,iBAAkB,SAAQ,cAAc;IACvC,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,GAAE,wBAA6B;IAI7D,OAAO,CAAC,YAAY,EAAE,mBAAmB,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,GAAG,IAAI,CAAC,CAAC;CAqC9E;AAED,2EAA2E;AAC3E,MAAM,MAAM,eAAe,GAAG,IAAI,CAAC,kBAAkB,EAAE,WAAW,CAAC,GAAG;IACpE,SAAS,CAAC,EAAE,cAAc,CAAC;CAC5B,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,eAAe,GAAG,WAAW,CAE/E"}
package/dist/cli/index.js CHANGED
@@ -1,15 +1,11 @@
1
1
  import { AndcoClient } from "../client.js";
2
2
  import { isLoopback } from "../config.js";
3
3
  import { ANDCO_ERROR_CODES, Result } from "../errors.js";
4
- import { PRESENT_TARGET } from "../presenter.js";
4
+ import { AndcoPresenter } from "../presenter.js";
5
5
  import { LoopbackServer } from "./server.js";
6
6
  /**
7
7
  * Presents an authorization from a terminal, using a one-shot loopback receiver.
8
8
  *
9
- * Intents are refused rather than mishandled: a terminal has no surface on which a person can
10
- * review and confirm a financial operation, and rewriting `redirect_uri` — which is what this
11
- * presenter does — is meaningless for an Intent.
12
- *
13
9
  * Every command-line integrator needs this exact thing, and until now every one wrote it: bind a
14
10
  * loopback port, refuse anything that is not the registered path, check the state, answer once, and
15
11
  * shut down. The Andco CLI's own version is ninety lines. Getting any of it wrong — accepting a
@@ -26,17 +22,13 @@ import { LoopbackServer } from "./server.js";
26
22
  * const { data: session } = await andco.auth.signIn();
27
23
  * ```
28
24
  */
29
- export class AndcoCliPresenter {
25
+ export class AndcoPresenterCli extends AndcoPresenter {
30
26
  options;
31
27
  constructor(options = {}) {
28
+ super();
32
29
  this.options = options;
33
30
  }
34
31
  async present(presentation) {
35
- if (presentation.kind === "intent") {
36
- return Result.fail(ANDCO_ERROR_CODES.PRESENTATION_UNSUPPORTED, {
37
- message: "a terminal cannot present an Intent for review and confirmation",
38
- });
39
- }
40
32
  const configured = presentation.returnTo;
41
33
  if (configured.protocol !== "http:" || !isLoopback(configured)) {
42
34
  return Result.fail(ANDCO_ERROR_CODES.INVALID_CONFIGURATION, {
@@ -54,7 +46,9 @@ export class AndcoCliPresenter {
54
46
  });
55
47
  }
56
48
  try {
57
- const resolved = await PRESENT_TARGET(presentation.url);
49
+ // Resolved after the receiver is bound: a pushed request is created on demand, and there is
50
+ // nothing to push before the callback it must name exists.
51
+ const resolved = await AndcoPresenter.present(presentation.url);
58
52
  if (resolved.error)
59
53
  return Result.fail(resolved.error);
60
54
  // The bound port may differ from the configured one when it asked for any free port, and the
@@ -81,5 +75,5 @@ export class AndcoCliPresenter {
81
75
  * credential is `andco.session.set(...)`, not a write into a private storage key.
82
76
  */
83
77
  export function createAndcoInstanceForCLI(options) {
84
- return new AndcoClient({ ...options, presenter: options.presenter ?? new AndcoCliPresenter() });
78
+ return new AndcoClient({ ...options, presenter: options.presenter ?? new AndcoPresenterCli() });
85
79
  }
@@ -1,9 +1,14 @@
1
+ /** Local OAuth callback receiver. The caller owns state/PKCE validation and server cleanup. */
1
2
  export declare class LoopbackServer {
2
3
  #private;
3
4
  private constructor();
5
+ /** Starts listening; a missing port selects an available port reflected in `redirectUri`. */
4
6
  static start(configured: URL): Promise<LoopbackServer>;
7
+ /** Exact callback URL including the port allocated by the operating system. */
5
8
  get redirectUri(): URL;
9
+ /** Waits for a callback or returns `null` on timeout. Call `close()` after either outcome. */
6
10
  wait(timeoutMs: number): Promise<URL | null>;
11
+ /** Closes the listener and any remaining connections. */
7
12
  close(): Promise<void>;
8
13
  private listen;
9
14
  private static hostFor;
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/cli/server.ts"],"names":[],"mappings":"AAEA,qBAAa,cAAc;;IAKzB,OAAO;WAKM,KAAK,CAAC,UAAU,EAAE,GAAG,GAAG,OAAO,CAAC,cAAc,CAAC;IAc5D,IAAI,WAAW,IAAI,GAAG,CAErB;IAED,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,GAAG,IAAI,CAAC;IAS5C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAiDtB,OAAO,CAAC,MAAM;IAcd,OAAO,CAAC,MAAM,CAAC,OAAO;CAGvB"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/cli/server.ts"],"names":[],"mappings":"AAEA,+FAA+F;AAC/F,qBAAa,cAAc;;IAKzB,OAAO;IAKP,6FAA6F;WAChF,KAAK,CAAC,UAAU,EAAE,GAAG,GAAG,OAAO,CAAC,cAAc,CAAC;IAc5D,+EAA+E;IAC/E,IAAI,WAAW,IAAI,GAAG,CAErB;IAED,8FAA8F;IAC9F,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,GAAG,IAAI,CAAC;IAS5C,yDAAyD;IACzD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAiDtB,OAAO,CAAC,MAAM;IAcd,OAAO,CAAC,MAAM,CAAC,OAAO;CAGvB"}