@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.
- package/LICENSE +1 -1
- package/README.md +93 -49
- package/dist/auth.d.ts +5 -3
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +10 -17
- package/dist/browser/controller.d.ts +46 -0
- package/dist/browser/controller.d.ts.map +1 -1
- package/dist/browser/controller.js +102 -12
- package/dist/browser/frame.d.ts.map +1 -1
- package/dist/browser/frame.js +6 -15
- package/dist/browser/index.d.ts +8 -2
- package/dist/browser/index.d.ts.map +1 -1
- package/dist/browser/index.js +48 -33
- package/dist/browser/popup.d.ts +4 -0
- package/dist/browser/popup.d.ts.map +1 -1
- package/dist/browser/popup.js +23 -7
- package/dist/cli/index.d.ts +4 -6
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +7 -13
- package/dist/cli/server.d.ts +5 -0
- package/dist/cli/server.d.ts.map +1 -1
- package/dist/cli/server.js +5 -0
- package/dist/client.d.ts +66 -19
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +117 -78
- package/dist/config.d.ts +2 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/credentials.d.ts +58 -4
- package/dist/credentials.d.ts.map +1 -1
- package/dist/credentials.js +0 -0
- package/dist/errors.d.ts +23 -21
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +18 -20
- package/dist/globals.d.ts +25 -0
- package/dist/globals.d.ts.map +1 -0
- package/dist/globals.js +15 -0
- package/dist/grants-api.d.ts +34 -0
- package/dist/grants-api.d.ts.map +1 -0
- package/dist/grants-api.js +48 -0
- package/dist/grants.d.ts +16 -0
- package/dist/grants.d.ts.map +1 -0
- package/dist/grants.js +13 -0
- package/dist/index.d.ts +12 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -3
- package/dist/inflight.d.ts +31 -0
- package/dist/inflight.d.ts.map +1 -0
- package/dist/inflight.js +26 -0
- package/dist/intents.d.ts +346 -70
- package/dist/intents.d.ts.map +1 -1
- package/dist/intents.js +629 -112
- package/dist/oauth.d.ts +55 -6
- package/dist/oauth.d.ts.map +1 -1
- package/dist/oauth.js +86 -57
- package/dist/presenter.d.ts +25 -11
- package/dist/presenter.d.ts.map +1 -1
- package/dist/presenter.js +39 -6
- package/dist/resource.d.ts +109 -0
- package/dist/resource.d.ts.map +1 -0
- package/dist/resource.js +151 -0
- package/dist/rest.d.ts +23 -4
- package/dist/rest.d.ts.map +1 -1
- package/dist/rest.js +46 -5
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -0
- package/dist/server-metadata.generated.d.ts.map +1 -1
- package/dist/server-metadata.generated.js +8 -4
- package/dist/service.d.ts +109 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +241 -0
- package/dist/session-store.d.ts +12 -4
- package/dist/session-store.d.ts.map +1 -1
- package/dist/session-store.js +64 -14
- package/dist/storage.d.ts +16 -23
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +27 -25
- package/dist/tokens.d.ts +46 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +148 -0
- 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: %
|
|
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
|
|
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: %
|
|
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.
|
|
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
|
-
|
|
195
|
-
|
|
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
|
-
|
|
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] %
|
|
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;
|
|
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"}
|
package/dist/browser/frame.js
CHANGED
|
@@ -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: %
|
|
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:
|
|
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: %
|
|
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: %
|
|
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:
|
|
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:
|
|
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);
|
package/dist/browser/index.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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;
|
|
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"}
|
package/dist/browser/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AndcoClient } from "../client.js";
|
|
2
2
|
import { ANDCO_ERROR_CODES, Result } from "../errors.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
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
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return Result.fail(
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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 ??
|
|
83
|
-
const storage = options.storage ?? (typeof window === "undefined" ? undefined : new
|
|
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,
|
package/dist/browser/popup.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/browser/popup.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
}
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AndcoClient, type AndcoClientOptions } from "../client.js";
|
|
2
2
|
import { Result } from "../errors.js";
|
|
3
|
-
import {
|
|
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
|
|
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
|
};
|
package/dist/cli/index.d.ts.map
CHANGED
|
@@ -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,
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
|
78
|
+
return new AndcoClient({ ...options, presenter: options.presenter ?? new AndcoPresenterCli() });
|
|
85
79
|
}
|
package/dist/cli/server.d.ts
CHANGED
|
@@ -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;
|
package/dist/cli/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/cli/server.ts"],"names":[],"mappings":"AAEA,qBAAa,cAAc;;IAKzB,OAAO;
|
|
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"}
|