@andco/sdk 0.0.1 → 0.0.3
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 +33 -10
- package/dist/auth.d.ts +3 -0
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +26 -10
- package/dist/browser/controller.d.ts +9 -1
- package/dist/browser/controller.d.ts.map +1 -1
- package/dist/browser/controller.js +70 -3
- package/dist/browser/frame.d.ts +4 -3
- package/dist/browser/frame.d.ts.map +1 -1
- package/dist/browser/frame.js +6 -5
- package/dist/browser/index.d.ts +11 -5
- package/dist/browser/index.d.ts.map +1 -1
- package/dist/browser/index.js +29 -9
- package/dist/browser/popup.d.ts +2 -1
- package/dist/browser/popup.d.ts.map +1 -1
- package/dist/browser/popup.js +16 -3
- package/dist/cli/index.d.ts +5 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +14 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +15 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/intents.d.ts +71 -1
- package/dist/intents.d.ts.map +1 -1
- package/dist/intents.js +147 -5
- package/dist/presenter.d.ts +41 -7
- package/dist/presenter.d.ts.map +1 -1
- package/dist/presenter.js +7 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -61,9 +61,8 @@ const andco = createAndcoInstanceForCLI({
|
|
|
61
61
|
const { data: session } = await andco.auth.signIn();
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
-
Every builder returns an `AndcoClient`. There
|
|
65
|
-
|
|
66
|
-
client shapes now.
|
|
64
|
+
Every builder returns an `AndcoClient`. There are two client shapes: `AndcoClient`, the
|
|
65
|
+
unauthenticated arm, and `AndcoClientAuthed`, the authenticated one.
|
|
67
66
|
|
|
68
67
|
## Public, no credentials involved
|
|
69
68
|
|
|
@@ -79,7 +78,7 @@ if (error) throw error;
|
|
|
79
78
|
|
|
80
79
|
Two ways to get an `AndcoClientAuthed`:
|
|
81
80
|
|
|
82
|
-
- `andco.with(credentials)` binds
|
|
81
|
+
- `andco.with(credentials)` binds _explicit_ credentials for one unit of work. It accepts
|
|
83
82
|
`AndcoCredentials`, an `AndcoSession`, or a bare access-token string — the bare string exists
|
|
84
83
|
for the common server case where a handler already pulled a token out of its own session:
|
|
85
84
|
|
|
@@ -109,8 +108,7 @@ own table, or a plain closure — without adopting the SDK's own persistence.
|
|
|
109
108
|
|
|
110
109
|
## Resource Server Definitions
|
|
111
110
|
|
|
112
|
-
|
|
113
|
-
implements:
|
|
111
|
+
A Resource Server Definition is any object that implements:
|
|
114
112
|
|
|
115
113
|
```ts
|
|
116
114
|
interface AndcoResourceServer<Client> {
|
|
@@ -130,12 +128,17 @@ satisfy `AndcoCredentials` and can be passed straight to `use(...)`.
|
|
|
130
128
|
`andco.intents` is domain-neutral: it only knows how to create, read, execute, and observe an
|
|
131
129
|
Intent generically. It does not know about deposits, withdrawals, or any other domain concept —
|
|
132
130
|
those belong to whatever Resource Server Definition owns that domain, composed over this same
|
|
133
|
-
surface (see [Resource Server Definitions](#resource-server-definitions) above)
|
|
131
|
+
surface (see [Resource Server Definitions](#resource-server-definitions) above), such as the AndCo
|
|
132
|
+
Bank Resource Server Definition in [`@andco/bank-sdk`](../bank-sdk/README.md).
|
|
134
133
|
|
|
135
134
|
```ts
|
|
136
|
-
const { data: intent, error } = await andco.intents.create(
|
|
137
|
-
|
|
138
|
-
|
|
135
|
+
const { data: intent, error } = await andco.intents.create(
|
|
136
|
+
"deposit",
|
|
137
|
+
depositInput,
|
|
138
|
+
{
|
|
139
|
+
idempotencyKey: "deposit:8472",
|
|
140
|
+
},
|
|
141
|
+
);
|
|
139
142
|
if (error) throw error;
|
|
140
143
|
```
|
|
141
144
|
|
|
@@ -150,6 +153,26 @@ if (error) throw error;
|
|
|
150
153
|
- `subscribe(subject, read, decode, handler, options)` — a durable, paged subscription loop, with
|
|
151
154
|
its own checkpoint and poll-interval reconciliation. Returns an unsubscribe function.
|
|
152
155
|
|
|
156
|
+
For the actual bank operations — `createDeposit`, `createWithdrawal`, `createAutomaticCharge`,
|
|
157
|
+
`closeDeposit`, and account/intent event listeners — see
|
|
158
|
+
[`@andco/bank-sdk`](../bank-sdk/README.md), whose `Bank` definition composes this same
|
|
159
|
+
domain-neutral surface:
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
import { Bank } from "@andco/bank-sdk";
|
|
163
|
+
|
|
164
|
+
const bankDefinition = new Bank({ clientId, resource: "https://api.andco.cl" });
|
|
165
|
+
const bank = bankDefinition.use(andco.with(session));
|
|
166
|
+
|
|
167
|
+
const { data: deposit, error } = await bank.intents.createDeposit(
|
|
168
|
+
depositInput,
|
|
169
|
+
{
|
|
170
|
+
idempotencyKey: "deposit:8472",
|
|
171
|
+
},
|
|
172
|
+
);
|
|
173
|
+
if (error) throw error;
|
|
174
|
+
```
|
|
175
|
+
|
|
153
176
|
Present the resulting intent with `@andco/sdk-react`, `@andco/sdk-vue`, `@andco/sdk-svelte`, or
|
|
154
177
|
`@andco/sdk-script`. The browser must retrieve the authoritative intent state after presentation;
|
|
155
178
|
callback parameters are correlation data, not financial state.
|
package/dist/auth.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type AndCoRandomSource } from "@andco/protocol";
|
|
2
|
+
import type { AndcoConfig } from "./config.js";
|
|
2
3
|
import type { AndcoSession } from "./credentials.js";
|
|
3
4
|
import { Result } from "./errors.js";
|
|
4
5
|
import type { AndcoAuthorizationOptions, AndcoAuthorizationRequest, AndcoOAuth } from "./oauth.js";
|
|
@@ -8,6 +9,8 @@ export type AndcoSignInOptions = AndcoAuthorizationOptions & {
|
|
|
8
9
|
presentation?: AndcoPresentation;
|
|
9
10
|
};
|
|
10
11
|
export type AndcoAuthOptions = {
|
|
12
|
+
/** Read for the registered callback, which has to be known before the request is prepared. */
|
|
13
|
+
config: AndcoConfig;
|
|
11
14
|
oauth: AndcoOAuth;
|
|
12
15
|
store: AndcoSessionStore;
|
|
13
16
|
presenter: AndcoPresenter;
|
package/dist/auth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,iBAAiB,
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,iBAAiB,EAAwB,MAAM,iBAAiB,CAAC;AAC/E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAqB,MAAM,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,KAAK,EAAE,yBAAyB,EAAE,yBAAyB,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACnG,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACxE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAE5D,MAAM,MAAM,kBAAkB,GAAG,yBAAyB,GAAG;IAC3D,YAAY,CAAC,EAAE,iBAAiB,CAAC;CAClC,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,8FAA8F;IAC9F,MAAM,EAAE,WAAW,CAAC;IACpB,KAAK,EAAE,UAAU,CAAC;IAClB,KAAK,EAAE,iBAAiB,CAAC;IACzB,SAAS,EAAE,cAAc,CAAC;IAC1B,oFAAoF;IACpF,QAAQ,CAAC,EAAE,MAAM,MAAM,CAAC;IACxB,0FAA0F;IAC1F,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,CAAC,OAAO,EAAE,yBAAyB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnF,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,SAAS;;gBAGD,OAAO,EAAE,gBAAgB;IAI5C;;;;;OAKG;IACU,MAAM,CAAC,OAAO,GAAE,kBAAuB,GAAG,OAAO,CAAC,MAAM,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;IA0C3F,iGAAiG;IACpF,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAI7C;;;;;;;;OAQG;IACI,QAAQ,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,KAAK,IAAI,GAAG,MAAM,IAAI;IAK7E,gFAAgF;IACnE,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;IAO/D,uFAAuF;IAC1E,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;CAWtD"}
|
package/dist/auth.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RANDOM_UUID } from "@andco/protocol";
|
|
1
|
+
import { RANDOM_UUID, SafeURL } from "@andco/protocol";
|
|
2
2
|
import { ANDCO_ERROR_CODES, Result } from "./errors.js";
|
|
3
3
|
/**
|
|
4
4
|
* Sign-in for a runtime that owns exactly one session.
|
|
@@ -29,17 +29,31 @@ export class AndcoAuth {
|
|
|
29
29
|
* result arrives on the next page load, through the store's own callback consumption.
|
|
30
30
|
*/
|
|
31
31
|
async signIn(options = {}) {
|
|
32
|
-
const { oauth, presenter, store } = this.#options;
|
|
32
|
+
const { config, oauth, presenter, store } = this.#options;
|
|
33
33
|
const presentationId = this.#options.randomId ? this.#options.randomId() : RANDOM_UUID(this.#options.crypto);
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
// The callback has to be known before the request is prepared, because the presenter needs it
|
|
35
|
+
// to open its window. It is registered configuration, so it is knowable without the round trip.
|
|
36
|
+
const configured = options.redirectTo ?? config.redirectTo;
|
|
37
|
+
if (!configured) {
|
|
38
|
+
return Result.fail(ANDCO_ERROR_CODES.INVALID_CONFIGURATION, {
|
|
39
|
+
message: "redirectTo is required, on the instance or on the request",
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
// Deferred: preparing the request may push it (PAR), and a presenter has to open its window
|
|
43
|
+
// during the user activation rather than after a network round trip.
|
|
44
|
+
let prepared;
|
|
39
45
|
const presented = await presenter.present({
|
|
40
|
-
|
|
46
|
+
kind: "oauth",
|
|
47
|
+
url: async () => {
|
|
48
|
+
const request = await oauth.createAuthorizationRequest(options);
|
|
49
|
+
if (request.error)
|
|
50
|
+
return Result.fail(request.error);
|
|
51
|
+
await this.#options.persistTransaction?.(request.data);
|
|
52
|
+
prepared = request.data;
|
|
53
|
+
return Result.ok(request.data.authorizationUrl);
|
|
54
|
+
},
|
|
41
55
|
presentation: options.presentation ?? "popup",
|
|
42
|
-
returnTo,
|
|
56
|
+
returnTo: new SafeURL(configured),
|
|
43
57
|
presentationId,
|
|
44
58
|
});
|
|
45
59
|
if (presented.error)
|
|
@@ -47,7 +61,9 @@ export class AndcoAuth {
|
|
|
47
61
|
// Either the document was replaced, or the user dismissed. Neither is an error.
|
|
48
62
|
if (!presented.data)
|
|
49
63
|
return Result.ok(null);
|
|
50
|
-
|
|
64
|
+
if (!prepared)
|
|
65
|
+
return Result.fail(ANDCO_ERROR_CODES.INVALID_CALLBACK, { message: "no transaction was prepared" });
|
|
66
|
+
const exchanged = await oauth.exchangeCallback({ callbackUrl: presented.data, request: prepared });
|
|
51
67
|
if (exchanged.error)
|
|
52
68
|
return Result.fail(exchanged.error);
|
|
53
69
|
const written = await store.set(exchanged.data);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AndCoClickEvent, AndCoErrorEvent, AndCoReadyEvent, AndCoThemeMode } from "@andco/protocol";
|
|
2
2
|
import type { AndcoClient } from "../client.js";
|
|
3
3
|
import { AndcoError } from "../errors.js";
|
|
4
|
+
import type { AndcoIntentId, AndcoPresentationResult } from "../intents.js";
|
|
4
5
|
import type { AndcoAuthorizationOptions } from "../oauth.js";
|
|
5
6
|
import type { AndcoPresentation } from "../presenter.js";
|
|
6
7
|
/** The hosted workflow an iframe presents. */
|
|
@@ -17,6 +18,12 @@ export type AndcoButtonControllerOptions = {
|
|
|
17
18
|
/** The instance whose configuration, protocol, and session this surface acts on. */
|
|
18
19
|
client: AndcoClient;
|
|
19
20
|
flow?: AndcoButtonFlow;
|
|
21
|
+
/**
|
|
22
|
+
* Required when `flow` is `"intent"`. Resolved after the user activates, never before: the iframe
|
|
23
|
+
* holds the activation and opens the window first, so creating the Intent earlier would make one
|
|
24
|
+
* on every render and lose the activation that the window needs.
|
|
25
|
+
*/
|
|
26
|
+
intentId?: AndcoIntentId;
|
|
20
27
|
/** Widget release to load. Defaults to the release the SDK was built against. */
|
|
21
28
|
release?: number;
|
|
22
29
|
/** Changes while the application runs, unlike endpoints and versions. */
|
|
@@ -30,7 +37,8 @@ export type AndcoButtonControllerOptions = {
|
|
|
30
37
|
authorization?: AndcoAuthorizationOptions;
|
|
31
38
|
onReady?: (event: AndCoReadyEvent) => void;
|
|
32
39
|
onActivate?: (event: AndCoClickEvent) => void;
|
|
33
|
-
|
|
40
|
+
/** Carries the Presentation Result on an Intent flow, and nothing on an authorization. */
|
|
41
|
+
onComplete?: (outcome?: AndcoPresentationResult) => void;
|
|
34
42
|
onDismiss?: () => void;
|
|
35
43
|
onError?: (error: AndcoError, event?: AndCoErrorEvent) => void;
|
|
36
44
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../src/browser/controller.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,eAAe,EAIf,eAAe,EACf,cAAc,EACf,MAAM,iBAAiB,CAAC;AAEzB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EAAqB,UAAU,EAAE,MAAM,cAAc,CAAC;AAC7D,OAAO,KAAK,EAAE,yBAAyB,EAA6B,MAAM,aAAa,CAAC;AACxF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAGzD,8CAA8C;AAC9C,MAAM,MAAM,eAAe,GAAG,eAAe,GAAG,QAAQ,CAAC;AAEzD,wDAAwD;AACxD,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,YAAY,GAAG,OAAO,GAAG,aAAa,GAAG,UAAU,GAAG,WAAW,GAAG,OAAO,CAAC;AAErH,MAAM,MAAM,mBAAmB,GAAG;IAChC,MAAM,EAAE,iBAAiB,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,4BAA4B,GAAG;IACzC,oFAAoF;IACpF,MAAM,EAAE,WAAW,CAAC;IACpB,IAAI,CAAC,EAAE,eAAe,CAAC;IACvB,iFAAiF;IACjF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yEAAyE;IACzE,SAAS,CAAC,EAAE,cAAc,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,oGAAoG;IACpG,YAAY,CAAC,EAAE,iBAAiB,CAAC;IACjC,wFAAwF;IACxF,aAAa,CAAC,EAAE,yBAAyB,CAAC;IAC1C,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAC;IAC3C,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAC;IAC9C,UAAU,CAAC,EAAE,
|
|
1
|
+
{"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../src/browser/controller.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,eAAe,EAIf,eAAe,EACf,cAAc,EACf,MAAM,iBAAiB,CAAC;AAEzB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EAAqB,UAAU,EAAE,MAAM,cAAc,CAAC;AAC7D,OAAO,KAAK,EAAE,aAAa,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AAC5E,OAAO,KAAK,EAAE,yBAAyB,EAA6B,MAAM,aAAa,CAAC;AACxF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAGzD,8CAA8C;AAC9C,MAAM,MAAM,eAAe,GAAG,eAAe,GAAG,QAAQ,CAAC;AAEzD,wDAAwD;AACxD,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,YAAY,GAAG,OAAO,GAAG,aAAa,GAAG,UAAU,GAAG,WAAW,GAAG,OAAO,CAAC;AAErH,MAAM,MAAM,mBAAmB,GAAG;IAChC,MAAM,EAAE,iBAAiB,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,4BAA4B,GAAG;IACzC,oFAAoF;IACpF,MAAM,EAAE,WAAW,CAAC;IACpB,IAAI,CAAC,EAAE,eAAe,CAAC;IACvB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,iFAAiF;IACjF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yEAAyE;IACzE,SAAS,CAAC,EAAE,cAAc,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,oGAAoG;IACpG,YAAY,CAAC,EAAE,iBAAiB,CAAC;IACjC,wFAAwF;IACxF,aAAa,CAAC,EAAE,yBAAyB,CAAC;IAC1C,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAC;IAC3C,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAC;IAC9C,0FAA0F;IAC1F,UAAU,CAAC,EAAE,CAAC,OAAO,CAAC,EAAE,uBAAuB,KAAK,IAAI,CAAC;IACzD,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,CAAC,EAAE,eAAe,KAAK,IAAI,CAAC;CAChE,CAAC;AAEF,KAAK,QAAQ,GAAG,CAAC,QAAQ,EAAE,mBAAmB,KAAK,IAAI,CAAC;AAExD;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,qBAAqB;;gBAeb,OAAO,EAAE,4BAA4B;IAKxD,IAAW,QAAQ,IAAI,mBAAmB,CAEzC;IAED,uEAAuE;IAChE,SAAS,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,IAAI;IAQhD,kEAAkE;IAC3D,OAAO,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,IAAI;IAiD9C,UAAU,IAAI,IAAI;IAKzB;;;;;;;OAOG;IACI,OAAO,IAAI,IAAI;IAiBf,OAAO,IAAI,IAAI;CAsKvB"}
|
|
@@ -26,6 +26,7 @@ export class AndcoButtonController {
|
|
|
26
26
|
#snapshot;
|
|
27
27
|
#pending;
|
|
28
28
|
#presentationId;
|
|
29
|
+
#intentId;
|
|
29
30
|
#generation = 0;
|
|
30
31
|
/** One automatic reconnect per `connect()` call, so a single slow network blip self-heals without
|
|
31
32
|
* bothering the app — but a persistently broken endpoint still surfaces as a visible `"error"`
|
|
@@ -108,6 +109,7 @@ export class AndcoButtonController {
|
|
|
108
109
|
restart() {
|
|
109
110
|
this.#generation += 1;
|
|
110
111
|
this.#pending = undefined;
|
|
112
|
+
this.#intentId = undefined;
|
|
111
113
|
if (this.#snapshot.status === "error" && this.#iframe) {
|
|
112
114
|
// A deliberate retry (the app calling this after showing the error) earns its own one-shot
|
|
113
115
|
// auto-reconnect on a future timeout, same as the very first connect did.
|
|
@@ -126,7 +128,7 @@ export class AndcoButtonController {
|
|
|
126
128
|
this.#listeners.clear();
|
|
127
129
|
}
|
|
128
130
|
/**
|
|
129
|
-
* Prepares
|
|
131
|
+
* Prepares a presentation once the user has activated the hosted control.
|
|
130
132
|
*
|
|
131
133
|
* Deferred on purpose: the iframe holds the user activation and opens the popup synchronously,
|
|
132
134
|
* then asks for the URL. Preparing it earlier would waste a transaction on every render and,
|
|
@@ -135,19 +137,73 @@ export class AndcoButtonController {
|
|
|
135
137
|
async #authorize() {
|
|
136
138
|
const generation = ++this.#generation;
|
|
137
139
|
this.#publish({ status: "authorizing", error: null });
|
|
140
|
+
const presentationId = this.#presentationId;
|
|
138
141
|
// The hosted surface opens the popup, so the callback has to know to deliver its result there.
|
|
139
142
|
// Encoding that in the state is the protocol's own mechanism and the only one available: the
|
|
140
143
|
// callback document cannot read a cross-origin opener's location.
|
|
144
|
+
const state = callbackStateCreate(this.#options.client.config.endpoints.widget.origin, presentationId);
|
|
145
|
+
if (this.#options.flow === "intent")
|
|
146
|
+
return this.#presentIntent(generation, presentationId, state);
|
|
141
147
|
const prepared = await this.#options.client.oauth.createAuthorizationRequest({
|
|
142
148
|
...(this.#options.authorization ?? {}),
|
|
143
|
-
state
|
|
149
|
+
state,
|
|
144
150
|
});
|
|
145
151
|
if (generation !== this.#generation)
|
|
146
152
|
return;
|
|
147
153
|
if (prepared.error)
|
|
148
154
|
return this.#fail(prepared.error);
|
|
149
155
|
this.#pending = prepared.data;
|
|
150
|
-
this.#bridge?.
|
|
156
|
+
this.#bridge?.resolvePresentation(presentationId, prepared.data.authorizationUrl.href);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Resolves the Intent this control presents, reusing the instance's own URL construction.
|
|
160
|
+
*
|
|
161
|
+
* 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.
|
|
163
|
+
*/
|
|
164
|
+
async #presentIntent(generation, presentationId, state) {
|
|
165
|
+
const client = this.#options.client;
|
|
166
|
+
const intentId = this.#options.intentId;
|
|
167
|
+
if (!intentId) {
|
|
168
|
+
return this.#fail(new AndcoError(ANDCO_ERROR_CODES.INVALID_CONFIGURATION, {
|
|
169
|
+
message: 'a control with flow="intent" requires an intentId',
|
|
170
|
+
}));
|
|
171
|
+
}
|
|
172
|
+
// Out of scope for now, and refused rather than silently resolving to nothing: an Intent result
|
|
173
|
+
// that comes back by redirect has no consumer yet, so the presentation would never settle.
|
|
174
|
+
if (this.#options.presentation === "redirect") {
|
|
175
|
+
return this.#fail(new AndcoError(ANDCO_ERROR_CODES.PRESENTATION_UNSUPPORTED, {
|
|
176
|
+
message: 'an Intent cannot be presented with presentation="redirect" yet',
|
|
177
|
+
}));
|
|
178
|
+
}
|
|
179
|
+
const returnTo = client.config.redirectTo;
|
|
180
|
+
if (!returnTo) {
|
|
181
|
+
return this.#fail(new AndcoError(ANDCO_ERROR_CODES.INVALID_CONFIGURATION, {
|
|
182
|
+
message: "an Intent Presentation needs the instance's redirectTo",
|
|
183
|
+
}));
|
|
184
|
+
}
|
|
185
|
+
let resolvedId;
|
|
186
|
+
try {
|
|
187
|
+
resolvedId = typeof intentId === "string" ? intentId : await intentId();
|
|
188
|
+
}
|
|
189
|
+
catch (cause) {
|
|
190
|
+
return this.#fail(AndcoError.from(cause, ANDCO_ERROR_CODES.INVALID_CONFIGURATION));
|
|
191
|
+
}
|
|
192
|
+
if (generation !== this.#generation)
|
|
193
|
+
return;
|
|
194
|
+
const url = await client.intents.presentationURL({
|
|
195
|
+
intentId: resolvedId,
|
|
196
|
+
presentation: "popup",
|
|
197
|
+
returnTo,
|
|
198
|
+
errorReturnTo: returnTo,
|
|
199
|
+
state,
|
|
200
|
+
});
|
|
201
|
+
if (generation !== this.#generation)
|
|
202
|
+
return;
|
|
203
|
+
if (url.error)
|
|
204
|
+
return this.#fail(url.error);
|
|
205
|
+
this.#intentId = resolvedId;
|
|
206
|
+
this.#bridge?.resolvePresentation(presentationId, url.data.href);
|
|
151
207
|
}
|
|
152
208
|
async #settle(event) {
|
|
153
209
|
const generation = this.#generation;
|
|
@@ -170,6 +226,17 @@ export class AndcoButtonController {
|
|
|
170
226
|
}
|
|
171
227
|
if (generation !== this.#generation)
|
|
172
228
|
return;
|
|
229
|
+
// An Intent completion is a lifecycle fact. What it means financially is read, not relayed.
|
|
230
|
+
if (event.type === "intent.complete" && this.#intentId) {
|
|
231
|
+
const intent = await this.#options.client.intents.get(this.#intentId);
|
|
232
|
+
if (generation !== this.#generation)
|
|
233
|
+
return;
|
|
234
|
+
if (intent.error)
|
|
235
|
+
return this.#fail(intent.error);
|
|
236
|
+
this.#publish({ status: "complete", error: null });
|
|
237
|
+
this.#options.onComplete?.({ outcome: "complete", intent: intent.data });
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
173
240
|
this.#publish({ status: "complete", error: null });
|
|
174
241
|
this.#options.onComplete?.();
|
|
175
242
|
}
|
package/dist/browser/frame.d.ts
CHANGED
|
@@ -40,15 +40,16 @@ export declare class AndcoButtonBridge {
|
|
|
40
40
|
/** Sends a validated protocol message to the hosted iframe. */
|
|
41
41
|
private post;
|
|
42
42
|
/**
|
|
43
|
-
* Resolves a deferred
|
|
43
|
+
* Resolves a deferred presentation the hosted button asked this side to prepare — an
|
|
44
|
+
* authorization or an Intent, which is why it is not named after either.
|
|
44
45
|
* Omit the URL to close the pending popup after resolution fails.
|
|
45
46
|
*
|
|
46
47
|
* @example
|
|
47
48
|
* ```ts
|
|
48
|
-
* bridge.
|
|
49
|
+
* bridge.resolvePresentation(presentationId, "https://app.example.com/oauth/start");
|
|
49
50
|
* ```
|
|
50
51
|
*/
|
|
51
|
-
|
|
52
|
+
resolvePresentation(presentationId: string, url?: string | URL): void;
|
|
52
53
|
/**
|
|
53
54
|
* Forwards a validated same-origin host callback to the iframe that owns the popup.
|
|
54
55
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"frame.d.ts","sourceRoot":"","sources":["../../src/browser/frame.ts"],"names":[],"mappings":"AAAA,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;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"}
|
package/dist/browser/frame.js
CHANGED
|
@@ -185,21 +185,22 @@ export class AndcoButtonBridge {
|
|
|
185
185
|
this.iframe.contentWindow?.postMessage(message, this.url.origin);
|
|
186
186
|
}
|
|
187
187
|
/**
|
|
188
|
-
* Resolves a deferred
|
|
188
|
+
* Resolves a deferred presentation the hosted button asked this side to prepare — an
|
|
189
|
+
* authorization or an Intent, which is why it is not named after either.
|
|
189
190
|
* Omit the URL to close the pending popup after resolution fails.
|
|
190
191
|
*
|
|
191
192
|
* @example
|
|
192
193
|
* ```ts
|
|
193
|
-
* bridge.
|
|
194
|
+
* bridge.resolvePresentation(presentationId, "https://app.example.com/oauth/start");
|
|
194
195
|
* ```
|
|
195
196
|
*/
|
|
196
|
-
|
|
197
|
+
resolvePresentation(presentationId, url) {
|
|
197
198
|
const message = {
|
|
198
199
|
protocol: ANDCO_SDK_PROTOCOL,
|
|
199
200
|
version: ANDCO_PROTOCOL_VERSION,
|
|
200
|
-
type: "
|
|
201
|
+
type: "presentation-response",
|
|
201
202
|
presentationId,
|
|
202
|
-
|
|
203
|
+
url: url?.toString(),
|
|
203
204
|
};
|
|
204
205
|
this.post(message);
|
|
205
206
|
}
|
package/dist/browser/index.d.ts
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
import { AndcoClient, type AndcoClientOptions } from "../client.js";
|
|
2
2
|
import type { AndcoAuthorizationRequest } from "../oauth.js";
|
|
3
|
-
import type
|
|
3
|
+
import { type AndcoPresenter } from "../presenter.js";
|
|
4
4
|
export { AndcoButtonController, type AndcoButtonControllerOptions, type AndcoButtonFlow, type AndcoButtonSnapshot, type AndcoButtonStatus, } from "./controller.js";
|
|
5
5
|
export { AndcoButtonBridge, type AndcoFrameOptions } from "./frame.js";
|
|
6
6
|
export { type AndcoPopupMessage, type AndcoPopupWindow, type AndcoRelayOutcome, andcoPopupPresentationId, isAndcoNativeHost, openAndcoPopup, openAndcoPopupWindow, relayAndcoPopupCallback, } from "./popup.js";
|
|
7
7
|
/**
|
|
8
|
-
* Presents an authorization in a browser.
|
|
8
|
+
* Presents an authorization or an Intent in a browser.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* A browser treats both alike — one window, one callback — so it reads `kind` only to refuse what
|
|
11
|
+
* it cannot do. It detects the first-party Andco Host and routes over the native bridge instead of
|
|
12
|
+
* opening a window, so a Miniapp runs the same build in a browser tab and inside the Host without
|
|
13
|
+
* branching on the platform. That is why there is no mobile presenter and no mobile builder.
|
|
14
|
+
*
|
|
15
|
+
* The window opens *before* the destination is resolved. A browser grants `window.open` only during
|
|
16
|
+
* a user activation, and both flows need a server round trip to learn where the window should go:
|
|
17
|
+
* an authorization may be pushed, and an Intent is created on demand. Resolving first is what got
|
|
18
|
+
* the window blocked.
|
|
13
19
|
*/
|
|
14
20
|
export declare function browserPresenter(): AndcoPresenter;
|
|
15
21
|
export type AndcoBrowserOptions = Omit<AndcoClientOptions, "presenter" | "source" | "clientSecret"> & {
|
|
@@ -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,KAAK,
|
|
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"}
|
package/dist/browser/index.js
CHANGED
|
@@ -1,24 +1,34 @@
|
|
|
1
1
|
import { AndcoClient } from "../client.js";
|
|
2
2
|
import { ANDCO_ERROR_CODES, Result } from "../errors.js";
|
|
3
|
+
import { PRESENT_TARGET } from "../presenter.js";
|
|
3
4
|
import { WebStorage } from "../storage.js";
|
|
4
|
-
import { andcoPopupPresentationId, isAndcoNativeHost,
|
|
5
|
+
import { andcoPopupPresentationId, isAndcoNativeHost, openAndcoPopupWindow, relayAndcoPopupCallback } from "./popup.js";
|
|
5
6
|
export { AndcoButtonController, } from "./controller.js";
|
|
6
7
|
export { AndcoButtonBridge } from "./frame.js";
|
|
7
8
|
export { andcoPopupPresentationId, isAndcoNativeHost, openAndcoPopup, openAndcoPopupWindow, relayAndcoPopupCallback, } from "./popup.js";
|
|
8
9
|
/**
|
|
9
|
-
* Presents an authorization in a browser.
|
|
10
|
+
* Presents an authorization or an Intent in a browser.
|
|
10
11
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* A browser treats both alike — one window, one callback — so it reads `kind` only to refuse what
|
|
13
|
+
* it cannot do. It detects the first-party Andco Host and routes over the native bridge instead of
|
|
14
|
+
* opening a window, so a Miniapp runs the same build in a browser tab and inside the Host without
|
|
15
|
+
* branching on the platform. That is why there is no mobile presenter and no mobile builder.
|
|
16
|
+
*
|
|
17
|
+
* The window opens *before* the destination is resolved. A browser grants `window.open` only during
|
|
18
|
+
* a user activation, and both flows need a server round trip to learn where the window should go:
|
|
19
|
+
* an authorization may be pushed, and an Intent is created on demand. Resolving first is what got
|
|
20
|
+
* the window blocked.
|
|
14
21
|
*/
|
|
15
22
|
export function browserPresenter() {
|
|
16
23
|
return {
|
|
17
24
|
async present(options) {
|
|
18
25
|
if (typeof window === "undefined")
|
|
19
26
|
return Result.fail(ANDCO_ERROR_CODES.BROWSER_REQUIRED);
|
|
20
|
-
if (options.presentation === "redirect") {
|
|
21
|
-
|
|
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);
|
|
22
32
|
// The document is being replaced. The result arrives on the next load, not here.
|
|
23
33
|
return Result.ok(null);
|
|
24
34
|
}
|
|
@@ -27,12 +37,22 @@ export function browserPresenter() {
|
|
|
27
37
|
message: "the native Host cannot present a popup without navigating the Miniapp",
|
|
28
38
|
});
|
|
29
39
|
}
|
|
30
|
-
|
|
31
|
-
url: options.url,
|
|
40
|
+
const popup = openAndcoPopupWindow({
|
|
32
41
|
presentationId: options.presentationId,
|
|
33
42
|
expectedOrigin: options.returnTo.origin,
|
|
34
43
|
signal: options.signal,
|
|
35
44
|
});
|
|
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);
|
|
50
|
+
if (resolved.error) {
|
|
51
|
+
popup.close();
|
|
52
|
+
return Result.fail(resolved.error);
|
|
53
|
+
}
|
|
54
|
+
popup.navigate(resolved.data);
|
|
55
|
+
return popup.result;
|
|
36
56
|
},
|
|
37
57
|
};
|
|
38
58
|
}
|
package/dist/browser/popup.d.ts
CHANGED
|
@@ -41,7 +41,8 @@ export declare function isAndcoNativeHost(): boolean;
|
|
|
41
41
|
*/
|
|
42
42
|
export declare function andcoPopupPresentationId(): string | null;
|
|
43
43
|
/**
|
|
44
|
-
* Hands this document's callback to the window that
|
|
44
|
+
* Hands this document's callback — an authorization or an Intent outcome — to the window that
|
|
45
|
+
* opened it, then closes.
|
|
45
46
|
*
|
|
46
47
|
* The popup cannot complete the exchange itself: the PKCE verifier was generated by the opener and
|
|
47
48
|
* never left it. So this is a courier, not a consumer — it carries the callback URL back and dies.
|
|
@@ -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
|
|
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"}
|
package/dist/browser/popup.js
CHANGED
|
@@ -28,7 +28,8 @@ export function andcoPopupPresentationId() {
|
|
|
28
28
|
return PRESENTATION_ID_PATTERN.test(presentationId) ? presentationId : null;
|
|
29
29
|
}
|
|
30
30
|
/**
|
|
31
|
-
* Hands this document's callback to the window that
|
|
31
|
+
* Hands this document's callback — an authorization or an Intent outcome — to the window that
|
|
32
|
+
* opened it, then closes.
|
|
32
33
|
*
|
|
33
34
|
* The popup cannot complete the exchange itself: the PKCE verifier was generated by the opener and
|
|
34
35
|
* never left it. So this is a courier, not a consumer — it carries the callback URL back and dies.
|
|
@@ -41,7 +42,11 @@ export function relayAndcoPopupCallback(allowedOrigins, options = {}) {
|
|
|
41
42
|
if (typeof window === "undefined")
|
|
42
43
|
return { status: "not_callback" };
|
|
43
44
|
const url = options.callbackUrl ? new URL(options.callbackUrl, window.location.href) : new URL(window.location.href);
|
|
44
|
-
const isCallback = Boolean(options.callbackUrl) ||
|
|
45
|
+
const isCallback = Boolean(options.callbackUrl) ||
|
|
46
|
+
url.searchParams.has("code") ||
|
|
47
|
+
url.searchParams.has("error") ||
|
|
48
|
+
// An Intent callback carries no authorization code; its outcome is the whole payload.
|
|
49
|
+
url.searchParams.has("andco_intent_result");
|
|
45
50
|
if (!isCallback)
|
|
46
51
|
return { status: "not_callback" };
|
|
47
52
|
const presentationId = andcoPopupPresentationId();
|
|
@@ -62,8 +67,9 @@ export function relayAndcoPopupCallback(allowedOrigins, options = {}) {
|
|
|
62
67
|
const message = openerOrigin === window.location.origin ? sdkMessage(presentationId, url) : protocolResult(presentationId, url);
|
|
63
68
|
// Strip before leaving. If closing is refused, the code must not remain visible in the address
|
|
64
69
|
// bar or in the window name.
|
|
65
|
-
for (const parameter of ["code", "state", "error", "error_description"])
|
|
70
|
+
for (const parameter of ["code", "state", "error", "error_description", "andco_intent_result", "intent_id"]) {
|
|
66
71
|
url.searchParams.delete(parameter);
|
|
72
|
+
}
|
|
67
73
|
window.history.replaceState(window.history.state, "", url);
|
|
68
74
|
window.name = "";
|
|
69
75
|
window.opener?.postMessage(message, openerOrigin);
|
|
@@ -75,6 +81,13 @@ function sdkMessage(presentationId, url) {
|
|
|
75
81
|
}
|
|
76
82
|
function protocolResult(presentationId, url) {
|
|
77
83
|
const envelope = { protocol: ANDCO_SDK_PROTOCOL, version: ANDCO_PROTOCOL_VERSION, presentationId };
|
|
84
|
+
// An Intent reports a lifecycle outcome and nothing else. Whether money moved is read from the
|
|
85
|
+
// Resource Server afterwards, never carried in a window message.
|
|
86
|
+
const intent = url.searchParams.get("andco_intent_result");
|
|
87
|
+
if (intent === "complete")
|
|
88
|
+
return { ...envelope, type: "intent.complete" };
|
|
89
|
+
if (intent)
|
|
90
|
+
return { ...envelope, type: "intent.dismiss" };
|
|
78
91
|
const code = url.searchParams.get("code");
|
|
79
92
|
if (code)
|
|
80
93
|
return { ...envelope, type: "oauth.authorization_code", authorizationCode: code };
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AndcoClient, type AndcoClientOptions } from "../client.js";
|
|
2
2
|
import { Result } from "../errors.js";
|
|
3
|
-
import type
|
|
3
|
+
import { type AndcoPresenter, type AndcoPresentOptions } from "../presenter.js";
|
|
4
4
|
export type AndcoCliPresenterOptions = {
|
|
5
5
|
/** Opens the authorization URL. Defaults to printing it, so nothing is assumed about the host. */
|
|
6
6
|
open?: (url: URL) => void | Promise<void>;
|
|
@@ -12,6 +12,10 @@ export type AndcoCliPresenterOptions = {
|
|
|
12
12
|
/**
|
|
13
13
|
* Presents an authorization from a terminal, using a one-shot loopback receiver.
|
|
14
14
|
*
|
|
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
|
+
*
|
|
15
19
|
* Every command-line integrator needs this exact thing, and until now every one wrote it: bind a
|
|
16
20
|
* loopback port, refuse anything that is not the registered path, check the state, answer once, and
|
|
17
21
|
* shut down. The Andco CLI's own version is ninety lines. Getting any of it wrong — accepting a
|
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,KAAK,
|
|
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"}
|
package/dist/cli/index.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
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
5
|
import { LoopbackServer } from "./server.js";
|
|
5
6
|
/**
|
|
6
7
|
* Presents an authorization from a terminal, using a one-shot loopback receiver.
|
|
7
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
|
+
*
|
|
8
13
|
* Every command-line integrator needs this exact thing, and until now every one wrote it: bind a
|
|
9
14
|
* loopback port, refuse anything that is not the registered path, check the state, answer once, and
|
|
10
15
|
* shut down. The Andco CLI's own version is ninety lines. Getting any of it wrong — accepting a
|
|
@@ -27,6 +32,11 @@ export class AndcoCliPresenter {
|
|
|
27
32
|
this.options = options;
|
|
28
33
|
}
|
|
29
34
|
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
|
+
}
|
|
30
40
|
const configured = presentation.returnTo;
|
|
31
41
|
if (configured.protocol !== "http:" || !isLoopback(configured)) {
|
|
32
42
|
return Result.fail(ANDCO_ERROR_CODES.INVALID_CONFIGURATION, {
|
|
@@ -44,9 +54,12 @@ export class AndcoCliPresenter {
|
|
|
44
54
|
});
|
|
45
55
|
}
|
|
46
56
|
try {
|
|
57
|
+
const resolved = await PRESENT_TARGET(presentation.url);
|
|
58
|
+
if (resolved.error)
|
|
59
|
+
return Result.fail(resolved.error);
|
|
47
60
|
// The bound port may differ from the configured one when it asked for any free port, and the
|
|
48
61
|
// authorization request must carry the exact URI the server will redirect to.
|
|
49
|
-
const url = new URL(
|
|
62
|
+
const url = new URL(resolved.data.href);
|
|
50
63
|
url.searchParams.set("redirect_uri", receiver.redirectUri.href);
|
|
51
64
|
if (this.options.open)
|
|
52
65
|
await this.options.open(url);
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,gBAAgB,EAAyB,MAAM,aAAa,CAAC;AAC7F,OAAO,EAAE,KAAK,gBAAgB,EAAE,KAAK,YAAY,EAAyC,MAAM,kBAAkB,CAAC;AAEnH,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,KAAK,yBAAyB,EAAE,UAAU,EAAE,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;AACjG,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACrD,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,KAAK,kBAAkB,EAAE,iBAAiB,EAAmB,MAAM,oBAAoB,CAAC;AACjG,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAE5D,oFAAoF;AACpF,MAAM,WAAW,mBAAmB,CAAC,MAAM;IACzC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;;;;OAMG;IACH,GAAG,CAAC,WAAW,EAAE,gBAAgB,GAAG,MAAM,CAAC;CAC5C;AAED,kHAAkH;AAClH,MAAM,MAAM,YAAY,GAAG;IACzB,KAAK,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAC/B,+GAA+G;IAC/G,MAAM,EAAE,iBAAiB,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,gBAAgB,GAAG;IAClD,4FAA4F;IAC5F,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,OAAO,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC;IAChC,0DAA0D;IAC1D,SAAS,CAAC,EAAE,kBAAkB,CAAC;IAC/B,wFAAwF;IACxF,SAAS,CAAC,EAAE,cAAc,CAAC;IAC3B,kEAAkE;IAClE,kBAAkB,CAAC,EAAE,CAAC,OAAO,EAAE,yBAAyB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClF,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,mGAAmG;IACnG,cAAc,CAAC,EAAE,YAAY,GAAG,IAAI,CAAC;IACrC,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,2FAA2F;IAC3F,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,WAAW;IACtB,SAAgB,MAAM,EAAE,WAAW,CAAC;IACpC,SAAgB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5C,SAAgB,OAAO,EAAE,YAAY,CAAC;IACtC,SAAgB,KAAK,EAAE,UAAU,CAAC;IAClC,SAAgB,IAAI,EAAE,SAAS,CAAC;IAChC,SAAgB,OAAO,EAAE,YAAY,CAAC;IACtC,SAAgB,IAAI,EAAE,SAAS,CAAC;IAChC,SAAgB,OAAO,EAAE,iBAAiB,CAAC;IAC3C,mFAAmF;IACnF,SAAgB,QAAQ,EAAG,KAAK,CAAU;gBAEvB,OAAO,EAAE,kBAAkB;
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/client.js
CHANGED
|
@@ -61,15 +61,25 @@ export class AndcoClient {
|
|
|
61
61
|
source: options.source,
|
|
62
62
|
persist: options.persist,
|
|
63
63
|
});
|
|
64
|
+
// One presenter for the whole instance: authorization and Intent are the same seam, so a
|
|
65
|
+
// runtime that can present one can present the other. Not public — nothing outside the
|
|
66
|
+
// instance chooses how it presents, and every mirror of this type would have to carry it.
|
|
67
|
+
const presenter = options.presenter ?? statelessPresenter();
|
|
64
68
|
this.auth = new AndcoAuth({
|
|
69
|
+
config: this.config,
|
|
65
70
|
oauth: this.oauth,
|
|
66
71
|
store: this.session,
|
|
67
|
-
presenter
|
|
72
|
+
presenter,
|
|
68
73
|
persistTransaction: options.persistTransaction,
|
|
69
74
|
crypto: this.globals.crypto,
|
|
70
75
|
});
|
|
71
76
|
this.rest = new AndcoRest({ config: this.config, fetch: this.globals.fetch, credentials: this.session });
|
|
72
|
-
this.intents = new AndcoIntents(
|
|
77
|
+
this.intents = new AndcoIntents({
|
|
78
|
+
rest: this.rest,
|
|
79
|
+
config: this.config,
|
|
80
|
+
presenter,
|
|
81
|
+
crypto: this.globals.crypto,
|
|
82
|
+
});
|
|
73
83
|
}
|
|
74
84
|
/**
|
|
75
85
|
* Binds *explicit* credentials for one unit of work, owning its own REST surface.
|
|
@@ -160,11 +170,13 @@ class AndcoAuthedClientImpl {
|
|
|
160
170
|
this.globals = owner.globals;
|
|
161
171
|
this.oauth = owner.oauth;
|
|
162
172
|
this.rest = new AndcoRest({ config: owner.config, fetch: owner.globals.fetch, credentials });
|
|
163
|
-
|
|
173
|
+
// No presenter: `with(...)` is the per-request server shape, and a request presents nothing.
|
|
174
|
+
this.intents = new AndcoIntents({ rest: this.rest, config: owner.config, crypto: owner.globals.crypto });
|
|
164
175
|
// In-memory and unlinked from `credentials`: it exists only so `auth` has something to operate
|
|
165
176
|
// on, since a bare token or a caller-supplied resolver carries no persistable session shape.
|
|
166
177
|
this.session = new AndcoSessionStore({ config: owner.config, oauth: owner.oauth, persist: false });
|
|
167
178
|
this.auth = new AndcoAuth({
|
|
179
|
+
config: owner.config,
|
|
168
180
|
oauth: owner.oauth,
|
|
169
181
|
store: this.session,
|
|
170
182
|
presenter: statelessPresenter(),
|
package/dist/index.d.ts
CHANGED
|
@@ -4,9 +4,9 @@ export { AndcoClient, type AndcoClientAuthed, type AndcoClientOptions, type Andc
|
|
|
4
4
|
export { AndcoConfig, type AndcoConfigInput, type AndcoScope, type AndcoTheme, isLoopback, } from "./config.js";
|
|
5
5
|
export { ANDCO_REFRESH_SKEW_SECONDS, type AndcoCredentials, type AndcoSession, type AndcoUser, credentialsFromSession, isCredentials, isExpired, sessionIdentityKey, tokenFor, } from "./credentials.js";
|
|
6
6
|
export { ANDCO_ERROR_CODES, AndcoAPIError, AndcoError, Result } from "./errors.js";
|
|
7
|
-
export { type AndcoEventPage, type AndcoEventSubscriptionOptions, type AndcoFinancialEvent, type AndcoIntent, type AndcoIntentCreateOptions, type AndcoIntentId, AndcoIntents, } from "./intents.js";
|
|
7
|
+
export { type AndcoEventPage, type AndcoEventSubscriptionOptions, type AndcoFinancialEvent, type AndcoIntent, type AndcoIntentCreateOptions, type AndcoIntentId, type AndcoIntentPresentOptions, AndcoIntents, type AndcoIntentsOptions, type AndcoPresentationResult, } from "./intents.js";
|
|
8
8
|
export { type AndcoAuthorizationDetail, type AndcoAuthorizationOptions, type AndcoAuthorizationRequest, type AndcoDeviceAuthorization, AndcoOAuth, type AndcoOAuthOptions, type AndcoResourceAuthorization, type AndcoTransportMode, } from "./oauth.js";
|
|
9
|
-
export type
|
|
9
|
+
export { type AndcoPresentation, type AndcoPresenter, type AndcoPresentIntent, type AndcoPresentOAuth, type AndcoPresentOptions, type AndcoPresentTarget, PRESENT_TARGET, } from "./presenter.js";
|
|
10
10
|
export { type AndcoHttpClient, type AndcoPageOptions, type AndcoRequestOptions, AndcoRest, type AndcoRestOptions, } from "./rest.js";
|
|
11
11
|
export { type AndcoSessionSource, AndcoSessionStore, type AndcoSessionStoreOptions } from "./session-store.js";
|
|
12
12
|
export { type AndcoLock, type AndcoStorage, InProcessLock, MemoryStorage, WebStorage, } from "./storage.js";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,KAAK,gBAAgB,EAAE,KAAK,kBAAkB,EAAE,MAAM,WAAW,CAAC;AACtF,OAAO,EACL,WAAW,EACX,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACvB,KAAK,YAAY,EACjB,KAAK,mBAAmB,GACzB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,WAAW,EACX,KAAK,gBAAgB,EACrB,KAAK,UAAU,EACf,KAAK,UAAU,EACf,UAAU,GACX,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,0BAA0B,EAC1B,KAAK,gBAAgB,EACrB,KAAK,YAAY,EACjB,KAAK,SAAS,EACd,sBAAsB,EACtB,aAAa,EACb,SAAS,EACT,kBAAkB,EAClB,QAAQ,GACT,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACnF,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,6BAA6B,EAClC,KAAK,mBAAmB,EACxB,KAAK,WAAW,EAChB,KAAK,wBAAwB,EAC7B,KAAK,aAAa,EAClB,YAAY,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,KAAK,gBAAgB,EAAE,KAAK,kBAAkB,EAAE,MAAM,WAAW,CAAC;AACtF,OAAO,EACL,WAAW,EACX,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACvB,KAAK,YAAY,EACjB,KAAK,mBAAmB,GACzB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,WAAW,EACX,KAAK,gBAAgB,EACrB,KAAK,UAAU,EACf,KAAK,UAAU,EACf,UAAU,GACX,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,0BAA0B,EAC1B,KAAK,gBAAgB,EACrB,KAAK,YAAY,EACjB,KAAK,SAAS,EACd,sBAAsB,EACtB,aAAa,EACb,SAAS,EACT,kBAAkB,EAClB,QAAQ,GACT,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACnF,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,6BAA6B,EAClC,KAAK,mBAAmB,EACxB,KAAK,WAAW,EAChB,KAAK,wBAAwB,EAC7B,KAAK,aAAa,EAClB,KAAK,yBAAyB,EAC9B,YAAY,EACZ,KAAK,mBAAmB,EACxB,KAAK,uBAAuB,GAC7B,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,KAAK,wBAAwB,EAC7B,KAAK,yBAAyB,EAC9B,KAAK,yBAAyB,EAC9B,KAAK,wBAAwB,EAC7B,UAAU,EACV,KAAK,iBAAiB,EACtB,KAAK,0BAA0B,EAC/B,KAAK,kBAAkB,GACxB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACvB,cAAc,GACf,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,EACxB,SAAS,EACT,KAAK,gBAAgB,GACtB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,KAAK,kBAAkB,EAAE,iBAAiB,EAAE,KAAK,wBAAwB,EAAE,MAAM,oBAAoB,CAAC;AAC/G,OAAO,EACL,KAAK,SAAS,EACd,KAAK,YAAY,EACjB,aAAa,EACb,aAAa,EACb,UAAU,GACX,MAAM,cAAc,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ export { ANDCO_REFRESH_SKEW_SECONDS, credentialsFromSession, isCredentials, isEx
|
|
|
6
6
|
export { ANDCO_ERROR_CODES, AndcoAPIError, AndcoError, Result } from "./errors.js";
|
|
7
7
|
export { AndcoIntents, } from "./intents.js";
|
|
8
8
|
export { AndcoOAuth, } from "./oauth.js";
|
|
9
|
+
export { PRESENT_TARGET, } from "./presenter.js";
|
|
9
10
|
export { AndcoRest, } from "./rest.js";
|
|
10
11
|
export { AndcoSessionStore } from "./session-store.js";
|
|
11
12
|
export { InProcessLock, MemoryStorage, WebStorage, } from "./storage.js";
|
package/dist/intents.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type AndCoRandomSource } from "@andco/protocol";
|
|
2
2
|
import { type AndCoEventPage, type AndCoFinancialEvent, type AndCoIntent } from "@andco/protocol/transport";
|
|
3
|
+
import type { AndcoConfig } from "./config.js";
|
|
3
4
|
import { Result } from "./errors.js";
|
|
5
|
+
import type { AndcoPresentation, AndcoPresenter } from "./presenter.js";
|
|
4
6
|
import type { AndcoRest } from "./rest.js";
|
|
5
7
|
export type { AndCoEventPage as AndcoEventPage, AndCoFinancialEvent as AndcoFinancialEvent, AndCoIntent as AndcoIntent, };
|
|
6
8
|
/** An intent identifier, or a factory that produces one after the user activates a launcher. */
|
|
@@ -9,6 +11,37 @@ export type AndcoIntentCreateOptions = {
|
|
|
9
11
|
/** A stable transport key. One is generated when omitted. */
|
|
10
12
|
idempotencyKey?: string;
|
|
11
13
|
};
|
|
14
|
+
export type AndcoIntentsOptions = {
|
|
15
|
+
rest: AndcoRest;
|
|
16
|
+
/**
|
|
17
|
+
* Required to present. A Resource Server Definition that composes this lifecycle omits it and
|
|
18
|
+
* gets creation, reads, events and subscriptions — presentation belongs to the Andco Instance.
|
|
19
|
+
*/
|
|
20
|
+
config?: AndcoConfig;
|
|
21
|
+
presenter?: AndcoPresenter;
|
|
22
|
+
crypto?: AndCoRandomSource;
|
|
23
|
+
};
|
|
24
|
+
/** Everything a caller may vary for one Intent Presentation. */
|
|
25
|
+
export type AndcoIntentPresentOptions = {
|
|
26
|
+
/** An id, or a factory resolved *after* the presenter has its window. */
|
|
27
|
+
intentId: AndcoIntentId;
|
|
28
|
+
/** Exact callback Andco returns to. Defaults to the instance's registered `redirectTo`. */
|
|
29
|
+
returnTo?: string | URL;
|
|
30
|
+
/** Exact callback reached on dismissal. Defaults to `returnTo`; both must share one origin. */
|
|
31
|
+
errorReturnTo?: string | URL;
|
|
32
|
+
signal?: AbortSignal;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* The authoritative outcome of one Intent Presentation.
|
|
36
|
+
*
|
|
37
|
+
* `outcome` is what the window reported; `intent` is what the Resource Server says. Only the second
|
|
38
|
+
* one is money. They are both here because an application needs to tell "the user walked away" from
|
|
39
|
+
* "the user finished and it is still processing", and the read alone cannot say which.
|
|
40
|
+
*/
|
|
41
|
+
export type AndcoPresentationResult<T = AndCoIntent> = {
|
|
42
|
+
outcome: "complete" | "dismiss";
|
|
43
|
+
intent: T;
|
|
44
|
+
};
|
|
12
45
|
export type AndcoEventSubscriptionOptions = {
|
|
13
46
|
/** Resume after a checkpoint this subject's subscription previously acknowledged. */
|
|
14
47
|
after?: string;
|
|
@@ -38,7 +71,7 @@ export type AndcoEventSubscriptionOptions = {
|
|
|
38
71
|
*/
|
|
39
72
|
export declare class AndcoIntents {
|
|
40
73
|
#private;
|
|
41
|
-
constructor(
|
|
74
|
+
constructor(options: AndcoIntentsOptions);
|
|
42
75
|
/**
|
|
43
76
|
* Creates an Intent of the given type. The exact Grant determines what it may do and where.
|
|
44
77
|
*
|
|
@@ -51,6 +84,43 @@ export declare class AndcoIntents {
|
|
|
51
84
|
* Definition names its own response type here.
|
|
52
85
|
*/
|
|
53
86
|
create<T = AndCoIntent>(type: string, input: object, options?: AndcoIntentCreateOptions): Promise<Result<T>>;
|
|
87
|
+
/**
|
|
88
|
+
* The Andco-owned URL that presents one Intent, with its callbacks bound.
|
|
89
|
+
*
|
|
90
|
+
* Published rather than folded into `present` because the Hosted Intent Button needs the same URL
|
|
91
|
+
* with a different `state`: its callback returns to the widget that opened the window, not to
|
|
92
|
+
* this document. Two constructions of one URL is precisely the drift ADR-0014 records, and a bare
|
|
93
|
+
* `authorization_url` is an input to presentation, not a destination — Platform renders a 404 for
|
|
94
|
+
* any of the four parameters below that is missing.
|
|
95
|
+
*/
|
|
96
|
+
presentationURL(options: {
|
|
97
|
+
intentId: string;
|
|
98
|
+
presentation: AndcoPresentation;
|
|
99
|
+
returnTo: URL;
|
|
100
|
+
errorReturnTo: URL;
|
|
101
|
+
state: string;
|
|
102
|
+
}): Promise<Result<URL>>;
|
|
103
|
+
/**
|
|
104
|
+
* Presents one Intent and answers with its authoritative outcome.
|
|
105
|
+
*
|
|
106
|
+
* This is the Direct Popup Presentation: the programmatic path for UI that cannot mount a Hosted
|
|
107
|
+
* Intent Button. It owns the whole interaction — the window, the four callback parameters, the
|
|
108
|
+
* correlation, and the read afterwards — so that no Project has to reach for a window primitive
|
|
109
|
+
* to complete a financial operation.
|
|
110
|
+
*
|
|
111
|
+
* The Intent is read again once the window settles, because a callback reports that a window
|
|
112
|
+
* finished and that is not the same fact as a completed deposit. A dismissal is read too: the
|
|
113
|
+
* user may have closed the window on an operation the bank had already accepted.
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```ts
|
|
117
|
+
* const { data, error } = await andco.intents.present<AndCoDepositIntent>({
|
|
118
|
+
* intentId: async () => (await bank.intents.createDeposit(input)).data.id,
|
|
119
|
+
* });
|
|
120
|
+
* if (data?.outcome === "complete") refreshBalance();
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
present<T = AndCoIntent>(options: AndcoIntentPresentOptions): Promise<Result<AndcoPresentationResult<T>>>;
|
|
54
124
|
/** Reads the authoritative state of one Intent. This, not a callback, is the source of truth. */
|
|
55
125
|
get<T = AndCoIntent>(intentId: string): Promise<Result<T>>;
|
|
56
126
|
/** Executes an intent whose Grant permits it. Completion must still be confirmed with `get`. */
|
package/dist/intents.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"intents.d.ts","sourceRoot":"","sources":["../src/intents.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"intents.d.ts","sourceRoot":"","sources":["../src/intents.ts"],"names":[],"mappings":"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"}
|
package/dist/intents.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { RANDOM_UUID } from "@andco/protocol";
|
|
2
|
-
import { subscribeFinancialEvents, } from "@andco/protocol/transport";
|
|
1
|
+
import { CALLBACK_STATE_FROM, callbackStateCreate, RANDOM_UUID, SafeURL, } from "@andco/protocol";
|
|
2
|
+
import { ANDCO_INTENT_AUTHORIZATION_URL, subscribeFinancialEvents, } from "@andco/protocol/transport";
|
|
3
3
|
import { ANDCO_ERROR_CODES, AndcoError, Result } from "./errors.js";
|
|
4
4
|
const INTENT_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
5
5
|
const IDEMPOTENCY_KEY_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
@@ -18,10 +18,14 @@ const IDEMPOTENCY_KEY_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
|
18
18
|
*/
|
|
19
19
|
export class AndcoIntents {
|
|
20
20
|
#rest;
|
|
21
|
+
#config;
|
|
22
|
+
#presenter;
|
|
21
23
|
#crypto;
|
|
22
|
-
constructor(
|
|
23
|
-
this.#rest = rest;
|
|
24
|
-
this.#
|
|
24
|
+
constructor(options) {
|
|
25
|
+
this.#rest = options.rest;
|
|
26
|
+
this.#config = options.config;
|
|
27
|
+
this.#presenter = options.presenter;
|
|
28
|
+
this.#crypto = options.crypto;
|
|
25
29
|
}
|
|
26
30
|
/**
|
|
27
31
|
* Creates an Intent of the given type. The exact Grant determines what it may do and where.
|
|
@@ -57,6 +61,115 @@ export class AndcoIntents {
|
|
|
57
61
|
}
|
|
58
62
|
return Result.ok(data);
|
|
59
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* The Andco-owned URL that presents one Intent, with its callbacks bound.
|
|
66
|
+
*
|
|
67
|
+
* Published rather than folded into `present` because the Hosted Intent Button needs the same URL
|
|
68
|
+
* with a different `state`: its callback returns to the widget that opened the window, not to
|
|
69
|
+
* this document. Two constructions of one URL is precisely the drift ADR-0014 records, and a bare
|
|
70
|
+
* `authorization_url` is an input to presentation, not a destination — Platform renders a 404 for
|
|
71
|
+
* any of the four parameters below that is missing.
|
|
72
|
+
*/
|
|
73
|
+
async presentationURL(options) {
|
|
74
|
+
if (!this.#config)
|
|
75
|
+
return Result.fail(presentationUnsupported());
|
|
76
|
+
const invalid = assertIntentId(options.intentId);
|
|
77
|
+
if (invalid)
|
|
78
|
+
return Result.fail(invalid);
|
|
79
|
+
if (options.returnTo.origin !== options.errorReturnTo.origin) {
|
|
80
|
+
// Platform refuses a mismatch by rendering a 404. Failing here says why instead.
|
|
81
|
+
return Result.fail(ANDCO_ERROR_CODES.INVALID_CONFIGURATION, {
|
|
82
|
+
message: "returnTo and errorReturnTo must share one origin",
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
const read = await this.get(options.intentId);
|
|
86
|
+
if (read.error)
|
|
87
|
+
return Result.fail(read.error);
|
|
88
|
+
let url;
|
|
89
|
+
try {
|
|
90
|
+
url = ANDCO_INTENT_AUTHORIZATION_URL(read.data, this.#config.endpoints.api);
|
|
91
|
+
}
|
|
92
|
+
catch (cause) {
|
|
93
|
+
return Result.fail(AndcoError.from(cause, ANDCO_ERROR_CODES.INVALID_RESPONSE));
|
|
94
|
+
}
|
|
95
|
+
url.searchParams.set("return_uri", options.returnTo.href);
|
|
96
|
+
url.searchParams.set("error_return_uri", options.errorReturnTo.href);
|
|
97
|
+
url.searchParams.set("presentation", options.presentation);
|
|
98
|
+
url.searchParams.set("state", options.state);
|
|
99
|
+
return Result.ok(url);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Presents one Intent and answers with its authoritative outcome.
|
|
103
|
+
*
|
|
104
|
+
* This is the Direct Popup Presentation: the programmatic path for UI that cannot mount a Hosted
|
|
105
|
+
* Intent Button. It owns the whole interaction — the window, the four callback parameters, the
|
|
106
|
+
* correlation, and the read afterwards — so that no Project has to reach for a window primitive
|
|
107
|
+
* to complete a financial operation.
|
|
108
|
+
*
|
|
109
|
+
* The Intent is read again once the window settles, because a callback reports that a window
|
|
110
|
+
* finished and that is not the same fact as a completed deposit. A dismissal is read too: the
|
|
111
|
+
* user may have closed the window on an operation the bank had already accepted.
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```ts
|
|
115
|
+
* const { data, error } = await andco.intents.present<AndCoDepositIntent>({
|
|
116
|
+
* intentId: async () => (await bank.intents.createDeposit(input)).data.id,
|
|
117
|
+
* });
|
|
118
|
+
* if (data?.outcome === "complete") refreshBalance();
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
async present(options) {
|
|
122
|
+
if (!this.#config || !this.#presenter)
|
|
123
|
+
return Result.fail(presentationUnsupported());
|
|
124
|
+
const returnTo = options.returnTo ? new SafeURL(options.returnTo) : this.#config.redirectTo;
|
|
125
|
+
if (!returnTo) {
|
|
126
|
+
return Result.fail(ANDCO_ERROR_CODES.INVALID_CONFIGURATION, {
|
|
127
|
+
message: "an Intent Presentation needs a returnTo, and this instance has no redirectTo",
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
const errorReturnTo = options.errorReturnTo ? new SafeURL(options.errorReturnTo) : returnTo;
|
|
131
|
+
const presentationId = RANDOM_UUID(this.#crypto);
|
|
132
|
+
// The same encoding OAuth uses: it binds the opener origin the callback must post back to, and
|
|
133
|
+
// its alphabet is the one Platform accepts for `state`.
|
|
134
|
+
const state = callbackStateCreate(returnTo.origin, presentationId);
|
|
135
|
+
// Resolved inside the thunk, so the window is already open when the Intent is created.
|
|
136
|
+
let intentId;
|
|
137
|
+
const presented = await this.#presenter.present({
|
|
138
|
+
kind: "intent",
|
|
139
|
+
presentation: "popup",
|
|
140
|
+
url: async () => {
|
|
141
|
+
try {
|
|
142
|
+
intentId = typeof options.intentId === "string" ? options.intentId : await options.intentId();
|
|
143
|
+
}
|
|
144
|
+
catch (cause) {
|
|
145
|
+
return Result.fail(AndcoError.from(cause, ANDCO_ERROR_CODES.INVALID_CONFIGURATION));
|
|
146
|
+
}
|
|
147
|
+
return this.presentationURL({ intentId, presentation: "popup", returnTo, errorReturnTo, state });
|
|
148
|
+
},
|
|
149
|
+
get intentId() {
|
|
150
|
+
return intentId ?? "";
|
|
151
|
+
},
|
|
152
|
+
returnTo,
|
|
153
|
+
errorReturnTo,
|
|
154
|
+
presentationId,
|
|
155
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
156
|
+
});
|
|
157
|
+
if (presented.error)
|
|
158
|
+
return Result.fail(presented.error);
|
|
159
|
+
if (!intentId) {
|
|
160
|
+
// The presenter settled without ever asking for a destination, so nothing was presented.
|
|
161
|
+
return Result.fail(presentationUnsupported());
|
|
162
|
+
}
|
|
163
|
+
const outcome = INTENT_RESULT_FROM(presented.data, { presentationId });
|
|
164
|
+
if (outcome.error)
|
|
165
|
+
return Result.fail(outcome.error);
|
|
166
|
+
console.debug("[AndcoIntents.present] settled: %s %o", outcome.data, { intentId, presentationId });
|
|
167
|
+
// Authoritative, and read on a dismissal too: a closed window says nothing about the money.
|
|
168
|
+
const intent = await this.get(intentId);
|
|
169
|
+
if (intent.error)
|
|
170
|
+
return Result.fail(intent.error);
|
|
171
|
+
return Result.ok({ outcome: outcome.data, intent: intent.data });
|
|
172
|
+
}
|
|
60
173
|
/** Reads the authoritative state of one Intent. This, not a callback, is the source of truth. */
|
|
61
174
|
async get(intentId) {
|
|
62
175
|
const invalid = assertIntentId(intentId);
|
|
@@ -149,6 +262,35 @@ export class AndcoIntents {
|
|
|
149
262
|
return () => subscription.unsubscribe();
|
|
150
263
|
}
|
|
151
264
|
}
|
|
265
|
+
/**
|
|
266
|
+
* Reads the lifecycle outcome out of the callback a presentation came back with.
|
|
267
|
+
*
|
|
268
|
+
* A closed window with no callback is a dismissal, never a failure. A callback whose `state` does
|
|
269
|
+
* not carry this presentation is not ours and is refused rather than guessed at.
|
|
270
|
+
*/
|
|
271
|
+
function INTENT_RESULT_FROM(callbackUrl, presentation) {
|
|
272
|
+
if (!callbackUrl)
|
|
273
|
+
return Result.ok("dismiss");
|
|
274
|
+
const state = callbackUrl.searchParams.get("state");
|
|
275
|
+
const parsed = state ? CALLBACK_STATE_FROM(state) : null;
|
|
276
|
+
if (!parsed || parsed.presentationId !== presentation.presentationId) {
|
|
277
|
+
return Result.fail(ANDCO_ERROR_CODES.INVALID_CALLBACK, {
|
|
278
|
+
message: "the callback does not belong to this presentation",
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
const result = callbackUrl.searchParams.get("andco_intent_result");
|
|
282
|
+
if (result === "complete")
|
|
283
|
+
return Result.ok("complete");
|
|
284
|
+
if (result === "dismiss")
|
|
285
|
+
return Result.ok("dismiss");
|
|
286
|
+
return Result.fail(ANDCO_ERROR_CODES.INVALID_CALLBACK, { message: "the callback carries no Intent result" });
|
|
287
|
+
}
|
|
288
|
+
/** One message for every surface that has the lifecycle but cannot put a window in front of anyone. */
|
|
289
|
+
function presentationUnsupported() {
|
|
290
|
+
return new AndcoError(ANDCO_ERROR_CODES.PRESENTATION_UNSUPPORTED, {
|
|
291
|
+
message: "this Intent surface was composed without a presenter; present through the Andco Instance instead",
|
|
292
|
+
});
|
|
293
|
+
}
|
|
152
294
|
/** Intent identifiers are UUIDs; anything else never reaches the network. */
|
|
153
295
|
function assertIntentId(value) {
|
|
154
296
|
return INTENT_ID_PATTERN.test(value)
|
package/dist/presenter.d.ts
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Result } from "./errors.js";
|
|
2
2
|
/** How an authorization is put in front of the user. */
|
|
3
3
|
export type AndcoPresentation = "popup" | "redirect";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
/**
|
|
5
|
+
* The destination, or a thunk that resolves it.
|
|
6
|
+
*
|
|
7
|
+
* A thunk is what lets a presenter open its window *during* the user activation and navigate it
|
|
8
|
+
* afterwards. Both flows need it: an authorization request may be pushed (PAR) before its URL
|
|
9
|
+
* exists, and an Intent is created only once someone asked for it. Awaiting either before opening
|
|
10
|
+
* is what gets the window blocked.
|
|
11
|
+
*/
|
|
12
|
+
export type AndcoPresentTarget = URL | (() => Promise<Result<URL>>);
|
|
13
|
+
type AndcoPresentBase = {
|
|
14
|
+
url: AndcoPresentTarget;
|
|
7
15
|
/** Exact callback the Authorization Server will reach on success. */
|
|
8
16
|
returnTo: URL;
|
|
9
17
|
/** Exact callback reached on failure. Defaults to `returnTo`. */
|
|
@@ -12,11 +20,34 @@ export type AndcoPresentOptions = {
|
|
|
12
20
|
presentationId: string;
|
|
13
21
|
signal?: AbortSignal;
|
|
14
22
|
};
|
|
23
|
+
/** An authorization. Its callback carries a single-use code the opener must exchange. */
|
|
24
|
+
export type AndcoPresentOAuth = AndcoPresentBase & {
|
|
25
|
+
kind: "oauth";
|
|
26
|
+
presentation: AndcoPresentation;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* An Intent. Its callback carries a lifecycle outcome only — the authenticated read decides the
|
|
30
|
+
* financial fact — and `intentId` is what lets a presenter with no window, such as a native Host,
|
|
31
|
+
* resolve current state from the Resource Server instead.
|
|
32
|
+
*/
|
|
33
|
+
export type AndcoPresentIntent = AndcoPresentBase & {
|
|
34
|
+
kind: "intent";
|
|
35
|
+
/** Popup only for now: nothing consumes an Intent result that comes back by redirect. */
|
|
36
|
+
presentation: "popup";
|
|
37
|
+
/** Empty until the thunk has resolved; a presenter reads it after resolution, never before. */
|
|
38
|
+
readonly intentId: string;
|
|
39
|
+
};
|
|
40
|
+
export type AndcoPresentOptions = AndcoPresentOAuth | AndcoPresentIntent;
|
|
15
41
|
/**
|
|
16
42
|
* The boundary between the protocol and whatever shows it to a person.
|
|
17
43
|
*
|
|
18
|
-
* A presenter receives
|
|
19
|
-
*
|
|
44
|
+
* A presenter receives a destination and answers with the callback URL that came back, or `null`
|
|
45
|
+
* when the user dismissed. It owns no protocol: no PKCE, no state, no token exchange, and no
|
|
46
|
+
* opinion about what a completed Intent means.
|
|
47
|
+
*
|
|
48
|
+
* `kind` is the seam's only domain knowledge, and it exists because a presenter without a window
|
|
49
|
+
* cannot treat the two alike: a native Host runs its own consent screen for an authorization and
|
|
50
|
+
* its own review screen for an Intent. A browser presents both the same way and ignores it.
|
|
20
51
|
*
|
|
21
52
|
* Keeping it this narrow is what makes sign-in testable without a browser — a test supplies a
|
|
22
53
|
* presenter that returns a prepared callback URL and the whole flow runs in plain Node. It is also
|
|
@@ -26,11 +57,14 @@ export type AndcoPresentOptions = {
|
|
|
26
57
|
* @example
|
|
27
58
|
* ```ts
|
|
28
59
|
* const presenter: AndcoPresenter = {
|
|
29
|
-
* present: async ({
|
|
60
|
+
* present: async ({ returnTo }) => Result.ok(new URL(`${returnTo}?code=test&state=${state}`)),
|
|
30
61
|
* };
|
|
31
62
|
* ```
|
|
32
63
|
*/
|
|
33
64
|
export interface AndcoPresenter {
|
|
34
65
|
present(options: AndcoPresentOptions): Promise<Result<URL | null>>;
|
|
35
66
|
}
|
|
67
|
+
/** Resolves a presentation target, whether it was already a URL or still a thunk. */
|
|
68
|
+
export declare function PRESENT_TARGET(target: AndcoPresentTarget): Promise<Result<URL>>;
|
|
69
|
+
export {};
|
|
36
70
|
//# sourceMappingURL=presenter.d.ts.map
|
package/dist/presenter.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"presenter.d.ts","sourceRoot":"","sources":["../src/presenter.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"presenter.d.ts","sourceRoot":"","sources":["../src/presenter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC,wDAAwD;AACxD,MAAM,MAAM,iBAAiB,GAAG,OAAO,GAAG,UAAU,CAAC;AAErD;;;;;;;GAOG;AACH,MAAM,MAAM,kBAAkB,GAAG,GAAG,GAAG,CAAC,MAAM,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;AAEpE,KAAK,gBAAgB,GAAG;IACtB,GAAG,EAAE,kBAAkB,CAAC;IACxB,qEAAqE;IACrE,QAAQ,EAAE,GAAG,CAAC;IACd,iEAAiE;IACjE,aAAa,CAAC,EAAE,GAAG,CAAC;IACpB,iEAAiE;IACjE,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,CAAC;AAEF,yFAAyF;AACzF,MAAM,MAAM,iBAAiB,GAAG,gBAAgB,GAAG;IACjD,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,EAAE,iBAAiB,CAAC;CACjC,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,GAAG,gBAAgB,GAAG;IAClD,IAAI,EAAE,QAAQ,CAAC;IACf,yFAAyF;IACzF,YAAY,EAAE,OAAO,CAAC;IACtB,+FAA+F;IAC/F,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,iBAAiB,GAAG,kBAAkB,CAAC;AAEzE;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC;CACpE;AAED,qFAAqF;AACrF,wBAAgB,cAAc,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAG/E"}
|
package/dist/presenter.js
CHANGED
|
@@ -1 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
import { Result } from "./errors.js";
|
|
2
|
+
/** Resolves a presentation target, whether it was already a URL or still a thunk. */
|
|
3
|
+
export function PRESENT_TARGET(target) {
|
|
4
|
+
if (target instanceof URL)
|
|
5
|
+
return Promise.resolve(Result.ok(target));
|
|
6
|
+
return target();
|
|
7
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@andco/sdk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
],
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@andco/openapi-fetch": "0.0.1",
|
|
37
|
-
"@andco/protocol": "0.0.
|
|
37
|
+
"@andco/protocol": "0.0.3",
|
|
38
38
|
"openid-client": "^6.8.8"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|