@andco/sdk 0.0.1
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 +202 -0
- package/README.md +176 -0
- package/dist/auth.d.ts +71 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +98 -0
- package/dist/browser/controller.d.ts +76 -0
- package/dist/browser/controller.d.ts.map +1 -0
- package/dist/browser/controller.js +215 -0
- package/dist/browser/frame.d.ts +65 -0
- package/dist/browser/frame.d.ts.map +1 -0
- package/dist/browser/frame.js +237 -0
- package/dist/browser/index.d.ts +52 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +117 -0
- package/dist/browser/popup.d.ts +105 -0
- package/dist/browser/popup.d.ts.map +1 -0
- package/dist/browser/popup.js +177 -0
- package/dist/cli/index.d.ts +47 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +72 -0
- package/dist/cli/server.d.ts +11 -0
- package/dist/cli/server.d.ts.map +1 -0
- package/dist/cli/server.js +91 -0
- package/dist/client.d.ts +135 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +210 -0
- package/dist/config.d.ts +82 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +109 -0
- package/dist/credentials.d.ts +76 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/credentials.js +0 -0
- package/dist/errors.d.ts +165 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +197 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/intents.d.ts +89 -0
- package/dist/intents.d.ts.map +1 -0
- package/dist/intents.js +157 -0
- package/dist/oauth.d.ts +147 -0
- package/dist/oauth.d.ts.map +1 -0
- package/dist/oauth.js +348 -0
- package/dist/presenter.d.ts +36 -0
- package/dist/presenter.d.ts.map +1 -0
- package/dist/presenter.js +1 -0
- package/dist/rest.d.ts +58 -0
- package/dist/rest.d.ts.map +1 -0
- package/dist/rest.js +48 -0
- package/dist/server/index.d.ts +21 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +27 -0
- package/dist/server-metadata.generated.d.ts +4 -0
- package/dist/server-metadata.generated.d.ts.map +1 -0
- package/dist/server-metadata.generated.js +49 -0
- package/dist/session-store.d.ts +83 -0
- package/dist/session-store.d.ts.map +1 -0
- package/dist/session-store.js +186 -0
- package/dist/storage.d.ts +61 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +47 -0
- package/package.json +56 -0
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single error type every Andco operation reports.
|
|
3
|
+
*
|
|
4
|
+
* Four separate error classes used to exist for authentication, intents, the resource API, and the
|
|
5
|
+
* SDK itself. Collapsing them means an integrator writes one `catch`-equivalent branch and compares
|
|
6
|
+
* one `code`, and the same string identifies the same condition in the Python SDK.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* const { data, error } = await andco.intents.get(intentId);
|
|
11
|
+
* if (error) {
|
|
12
|
+
* if (error.code === "insufficient_authorization") return reconnect();
|
|
13
|
+
* throw error;
|
|
14
|
+
* }
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export declare class AndcoError extends Error {
|
|
18
|
+
/** Stable identifier. Branch on this, never on `message`. */
|
|
19
|
+
readonly code: string;
|
|
20
|
+
/** HTTP status when the error came from a response. */
|
|
21
|
+
readonly status: number | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* The remaining fields of the error body.
|
|
24
|
+
*
|
|
25
|
+
* Andco reports more than a code — `required_scope` on an authorization failure, per-field notes
|
|
26
|
+
* on a rejected input. Branch on `code`, but surface these: without them an integrator can only
|
|
27
|
+
* tell a user that something failed, not which permission is missing.
|
|
28
|
+
*/
|
|
29
|
+
readonly details: Readonly<Record<string, unknown>> | undefined;
|
|
30
|
+
constructor(code: string, options?: {
|
|
31
|
+
message?: string;
|
|
32
|
+
status?: number;
|
|
33
|
+
cause?: unknown;
|
|
34
|
+
details?: Readonly<Record<string, unknown>>;
|
|
35
|
+
});
|
|
36
|
+
/** Normalizes anything thrown by a dependency into an `AndcoError`. */
|
|
37
|
+
static from(cause: unknown, fallbackCode: string): AndcoError;
|
|
38
|
+
/** Whether a value is an `AndcoError`. */
|
|
39
|
+
static is(cause: unknown): cause is AndcoError;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* A failure of an exchange with the resource API.
|
|
43
|
+
*
|
|
44
|
+
* Separate from `AndcoError` because this one always carries the exchange itself. `response` is the
|
|
45
|
+
* actual `Response`; when the request never reached the API it is `Response.error()`, the Fetch
|
|
46
|
+
* standard's own stand-in for a transport failure, so the field is never absent and a caller never
|
|
47
|
+
* branches on whether it exists.
|
|
48
|
+
*
|
|
49
|
+
* `error` is `code` under the name the OpenAPI contract gives it. The contract's `ApiError` calls
|
|
50
|
+
* the stable identifier `error`, so carrying both names is what lets a wrapped client stand in for
|
|
51
|
+
* a plain `openapi-fetch` client wherever a library's types expect one. Branch on `code`.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```ts
|
|
55
|
+
* const { data, error } = await andco.rest.http.GET("/accounts");
|
|
56
|
+
* if (error) return error.status === 403 ? reconnect() : report(error.code);
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export declare class AndcoAPIError extends AndcoError {
|
|
60
|
+
/** `code` under the contract's name for it. Branch on `code`; this exists for the type to line up. */
|
|
61
|
+
readonly error: string;
|
|
62
|
+
/** The exchange that failed. `Response.error()` when the request never got a response. */
|
|
63
|
+
readonly response: Response;
|
|
64
|
+
constructor(code: string, options: {
|
|
65
|
+
response: Response;
|
|
66
|
+
message?: string;
|
|
67
|
+
status?: number;
|
|
68
|
+
cause?: unknown;
|
|
69
|
+
details?: Readonly<Record<string, unknown>>;
|
|
70
|
+
});
|
|
71
|
+
/**
|
|
72
|
+
* Builds one from what `withResult` hands an error normalizer.
|
|
73
|
+
*
|
|
74
|
+
* `payload` is the failure body openapi-fetch already parsed — or, when the request never
|
|
75
|
+
* reached the API, whatever `fetch` threw. `response` tells the two apart: a transport failure
|
|
76
|
+
* arrives with `Response.error()`, whose `type` is `"error"`.
|
|
77
|
+
*
|
|
78
|
+
* The body's own fields are copied onto the error, not only filed under `details`. The wrapped
|
|
79
|
+
* client's type says a failure carries what the contract declares — `required_scope` on an
|
|
80
|
+
* authorization failure, and so on — and reading one of those must not return `undefined` merely
|
|
81
|
+
* because the SDK had put it somewhere else.
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```ts
|
|
85
|
+
* this.http = withResult(this.raw, AndcoAPIError.fromOpenAPIFetch);
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
static fromOpenAPIFetch(payload: unknown, response: Response): AndcoAPIError;
|
|
89
|
+
/**
|
|
90
|
+
* What `AndcoAPIError` owns. The contract declares a `code` field of its own, distinct from the
|
|
91
|
+
* identifier this class calls `code`, so a failure body is not allowed to shadow any of these —
|
|
92
|
+
* it would replace a value the SDK guarantees with one it merely relayed.
|
|
93
|
+
*/
|
|
94
|
+
private static readonly OWN_FIELDS;
|
|
95
|
+
/** The Andco API reports failures as `{ error: "code" }`; anything else is an unusable response. */
|
|
96
|
+
private static codeOf;
|
|
97
|
+
/** Keeps the rest of the error body, and promotes `message` so it reaches `error.message`. */
|
|
98
|
+
private static bodyOf;
|
|
99
|
+
/**
|
|
100
|
+
* A network error response stands in for an exchange that never happened, so it carries no real
|
|
101
|
+
* status — reporting its `0` would read as an HTTP status that no server ever sent.
|
|
102
|
+
*/
|
|
103
|
+
private static statusOf;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* The shape every Andco method returns. Nothing throws, so an integrator never wraps a call in
|
|
107
|
+
* `try`/`catch` to find out whether it worked.
|
|
108
|
+
*
|
|
109
|
+
* Builders and constructors are the deliberate exception: a malformed client identifier or an
|
|
110
|
+
* unparseable endpoint is a programming error that must fail at startup rather than be threaded
|
|
111
|
+
* through every later call.
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```ts
|
|
115
|
+
* const { data, error } = await andco.oauth.refresh(refreshToken);
|
|
116
|
+
* if (error) return signOut();
|
|
117
|
+
* await store.set(data);
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
export type Result<T> = {
|
|
121
|
+
data: T;
|
|
122
|
+
error: null;
|
|
123
|
+
} | {
|
|
124
|
+
data: null;
|
|
125
|
+
error: AndcoError;
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* Constructors for successful and failed results.
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* ```ts
|
|
132
|
+
* return Result.ok(session);
|
|
133
|
+
* return Result.fail("session_missing");
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
export declare const Result: {
|
|
137
|
+
/** Wraps a value as a successful result. */
|
|
138
|
+
ok<T>(data: T): Result<T>;
|
|
139
|
+
/** Wraps a failure as a result, accepting either a code or an existing error. */
|
|
140
|
+
fail<T = never>(error: AndcoError | string, options?: {
|
|
141
|
+
message?: string;
|
|
142
|
+
status?: number;
|
|
143
|
+
cause?: unknown;
|
|
144
|
+
}): Result<T>;
|
|
145
|
+
};
|
|
146
|
+
/** Error codes the SDK itself raises, as opposed to those relayed from the Authorization Server. */
|
|
147
|
+
export declare const ANDCO_ERROR_CODES: {
|
|
148
|
+
readonly BROWSER_REQUIRED: "browser_required";
|
|
149
|
+
readonly BROWSER_UNAVAILABLE: "browser_unavailable";
|
|
150
|
+
readonly BROWSER_FORBIDDEN: "browser_forbidden";
|
|
151
|
+
readonly INVALID_CALLBACK: "invalid_callback";
|
|
152
|
+
readonly INVALID_CONFIGURATION: "invalid_configuration";
|
|
153
|
+
readonly HANDSHAKE_TIMEOUT: "handshake_timeout";
|
|
154
|
+
readonly INVALID_IFRAME: "invalid_iframe";
|
|
155
|
+
readonly INVALID_INTENT_ID: "invalid_intent_id";
|
|
156
|
+
readonly INVALID_RESPONSE: "invalid_response";
|
|
157
|
+
readonly POPUP_BLOCKED: "popup_blocked";
|
|
158
|
+
readonly POPUP_TIMEOUT: "popup_timeout";
|
|
159
|
+
readonly PRESENTATION_UNSUPPORTED: "presentation_unsupported";
|
|
160
|
+
readonly SAME_ORIGIN_ENDPOINT: "same_origin_endpoint";
|
|
161
|
+
readonly SESSION_MISSING: "session_missing";
|
|
162
|
+
readonly STORAGE_FAILED: "storage_failed";
|
|
163
|
+
readonly UNTRUSTED_ORIGIN: "untrusted_origin";
|
|
164
|
+
};
|
|
165
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,UAAW,SAAQ,KAAK;IACnC,6DAA6D;IAC7D,SAAgB,IAAI,EAAE,MAAM,CAAC;IAC7B,uDAAuD;IACvD,SAAgB,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3C;;;;;;OAMG;IACH,SAAgB,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC;gBAGrE,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;KAAO;IASnH,uEAAuE;WACzD,IAAI,CAAC,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,GAAG,UAAU;IAMpE,0CAA0C;WAC5B,EAAE,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,UAAU;CAGtD;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,aAAc,SAAQ,UAAU;IAC3C,sGAAsG;IACtG,SAAgB,KAAK,EAAE,MAAM,CAAC;IAC9B,0FAA0F;IAC1F,SAAgB,QAAQ,EAAE,QAAQ,CAAC;gBAGjC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE;QACP,QAAQ,EAAE,QAAQ,CAAC;QACnB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,KAAK,CAAC,EAAE,OAAO,CAAC;QAChB,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;KAC7C;IAQH;;;;;;;;;;;;;;;;OAgBG;WACW,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,GAAG,aAAa;IAiBnF;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,UAAU,CAU/B;IAEH,oGAAoG;IACpG,OAAO,CAAC,MAAM,CAAC,MAAM;IAUrB,8FAA8F;IAC9F,OAAO,CAAC,MAAM,CAAC,MAAM;IAerB;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ;CAMxB;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,MAAM,CAAC,CAAC,IAAI;IAAE,IAAI,EAAE,CAAC,CAAC;IAAC,KAAK,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,UAAU,CAAA;CAAE,CAAC;AAErF;;;;;;;;GAQG;AACH,eAAO,MAAM,MAAM;IACjB,4CAA4C;OACzC,CAAC,QAAQ,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC;IAIzB,iFAAiF;SAC5E,CAAC,iBACG,UAAU,GAAG,MAAM,YACjB;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,GAC9D,MAAM,CAAC,CAAC,CAAC;CAGb,CAAC;AAEF,oGAAoG;AACpG,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;CAiBpB,CAAC"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single error type every Andco operation reports.
|
|
3
|
+
*
|
|
4
|
+
* Four separate error classes used to exist for authentication, intents, the resource API, and the
|
|
5
|
+
* SDK itself. Collapsing them means an integrator writes one `catch`-equivalent branch and compares
|
|
6
|
+
* one `code`, and the same string identifies the same condition in the Python SDK.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* const { data, error } = await andco.intents.get(intentId);
|
|
11
|
+
* if (error) {
|
|
12
|
+
* if (error.code === "insufficient_authorization") return reconnect();
|
|
13
|
+
* throw error;
|
|
14
|
+
* }
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export class AndcoError extends Error {
|
|
18
|
+
/** Stable identifier. Branch on this, never on `message`. */
|
|
19
|
+
code;
|
|
20
|
+
/** HTTP status when the error came from a response. */
|
|
21
|
+
status;
|
|
22
|
+
/**
|
|
23
|
+
* The remaining fields of the error body.
|
|
24
|
+
*
|
|
25
|
+
* Andco reports more than a code — `required_scope` on an authorization failure, per-field notes
|
|
26
|
+
* on a rejected input. Branch on `code`, but surface these: without them an integrator can only
|
|
27
|
+
* tell a user that something failed, not which permission is missing.
|
|
28
|
+
*/
|
|
29
|
+
details;
|
|
30
|
+
constructor(code, options = {}) {
|
|
31
|
+
super(options.message ?? code, options.cause === undefined ? undefined : { cause: options.cause });
|
|
32
|
+
this.name = "AndcoError";
|
|
33
|
+
this.code = code;
|
|
34
|
+
this.status = options.status;
|
|
35
|
+
this.details = options.details;
|
|
36
|
+
}
|
|
37
|
+
/** Normalizes anything thrown by a dependency into an `AndcoError`. */
|
|
38
|
+
static from(cause, fallbackCode) {
|
|
39
|
+
if (AndcoError.is(cause))
|
|
40
|
+
return cause;
|
|
41
|
+
const message = cause instanceof Error ? cause.message : undefined;
|
|
42
|
+
return new AndcoError(fallbackCode, { message, cause });
|
|
43
|
+
}
|
|
44
|
+
/** Whether a value is an `AndcoError`. */
|
|
45
|
+
static is(cause) {
|
|
46
|
+
return cause instanceof AndcoError;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* A failure of an exchange with the resource API.
|
|
51
|
+
*
|
|
52
|
+
* Separate from `AndcoError` because this one always carries the exchange itself. `response` is the
|
|
53
|
+
* actual `Response`; when the request never reached the API it is `Response.error()`, the Fetch
|
|
54
|
+
* standard's own stand-in for a transport failure, so the field is never absent and a caller never
|
|
55
|
+
* branches on whether it exists.
|
|
56
|
+
*
|
|
57
|
+
* `error` is `code` under the name the OpenAPI contract gives it. The contract's `ApiError` calls
|
|
58
|
+
* the stable identifier `error`, so carrying both names is what lets a wrapped client stand in for
|
|
59
|
+
* a plain `openapi-fetch` client wherever a library's types expect one. Branch on `code`.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```ts
|
|
63
|
+
* const { data, error } = await andco.rest.http.GET("/accounts");
|
|
64
|
+
* if (error) return error.status === 403 ? reconnect() : report(error.code);
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export class AndcoAPIError extends AndcoError {
|
|
68
|
+
/** `code` under the contract's name for it. Branch on `code`; this exists for the type to line up. */
|
|
69
|
+
error;
|
|
70
|
+
/** The exchange that failed. `Response.error()` when the request never got a response. */
|
|
71
|
+
response;
|
|
72
|
+
constructor(code, options) {
|
|
73
|
+
super(code, options);
|
|
74
|
+
this.name = "AndcoAPIError";
|
|
75
|
+
this.error = code;
|
|
76
|
+
this.response = options.response;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Builds one from what `withResult` hands an error normalizer.
|
|
80
|
+
*
|
|
81
|
+
* `payload` is the failure body openapi-fetch already parsed — or, when the request never
|
|
82
|
+
* reached the API, whatever `fetch` threw. `response` tells the two apart: a transport failure
|
|
83
|
+
* arrives with `Response.error()`, whose `type` is `"error"`.
|
|
84
|
+
*
|
|
85
|
+
* The body's own fields are copied onto the error, not only filed under `details`. The wrapped
|
|
86
|
+
* client's type says a failure carries what the contract declares — `required_scope` on an
|
|
87
|
+
* authorization failure, and so on — and reading one of those must not return `undefined` merely
|
|
88
|
+
* because the SDK had put it somewhere else.
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```ts
|
|
92
|
+
* this.http = withResult(this.raw, AndcoAPIError.fromOpenAPIFetch);
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
static fromOpenAPIFetch(payload, response) {
|
|
96
|
+
const { message, details } = AndcoAPIError.bodyOf(payload);
|
|
97
|
+
const error = new AndcoAPIError(AndcoAPIError.codeOf(payload), {
|
|
98
|
+
response,
|
|
99
|
+
status: AndcoAPIError.statusOf(response),
|
|
100
|
+
message,
|
|
101
|
+
details,
|
|
102
|
+
cause: payload,
|
|
103
|
+
});
|
|
104
|
+
for (const [key, value] of Object.entries(details ?? {})) {
|
|
105
|
+
if (!AndcoAPIError.OWN_FIELDS.has(key)) {
|
|
106
|
+
error[key] = value;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return error;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* What `AndcoAPIError` owns. The contract declares a `code` field of its own, distinct from the
|
|
113
|
+
* identifier this class calls `code`, so a failure body is not allowed to shadow any of these —
|
|
114
|
+
* it would replace a value the SDK guarantees with one it merely relayed.
|
|
115
|
+
*/
|
|
116
|
+
static OWN_FIELDS = new Set([
|
|
117
|
+
"name",
|
|
118
|
+
"message",
|
|
119
|
+
"stack",
|
|
120
|
+
"cause",
|
|
121
|
+
"code",
|
|
122
|
+
"error",
|
|
123
|
+
"status",
|
|
124
|
+
"response",
|
|
125
|
+
"details",
|
|
126
|
+
]);
|
|
127
|
+
/** The Andco API reports failures as `{ error: "code" }`; anything else is an unusable response. */
|
|
128
|
+
static codeOf(payload) {
|
|
129
|
+
if (payload && typeof payload === "object") {
|
|
130
|
+
const { error } = payload;
|
|
131
|
+
if (typeof error === "string") {
|
|
132
|
+
return error;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return "request_failed";
|
|
136
|
+
}
|
|
137
|
+
/** Keeps the rest of the error body, and promotes `message` so it reaches `error.message`. */
|
|
138
|
+
static bodyOf(payload) {
|
|
139
|
+
if (!payload || typeof payload !== "object") {
|
|
140
|
+
return { message: undefined, details: undefined };
|
|
141
|
+
}
|
|
142
|
+
const { error: _code, message, ...rest } = payload;
|
|
143
|
+
const hasDetails = Object.keys(rest).length > 0;
|
|
144
|
+
return {
|
|
145
|
+
message: typeof message === "string" ? message : undefined,
|
|
146
|
+
details: hasDetails ? rest : undefined,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* A network error response stands in for an exchange that never happened, so it carries no real
|
|
151
|
+
* status — reporting its `0` would read as an HTTP status that no server ever sent.
|
|
152
|
+
*/
|
|
153
|
+
static statusOf(response) {
|
|
154
|
+
if (response.type === "error") {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
return response.status;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Constructors for successful and failed results.
|
|
162
|
+
*
|
|
163
|
+
* @example
|
|
164
|
+
* ```ts
|
|
165
|
+
* return Result.ok(session);
|
|
166
|
+
* return Result.fail("session_missing");
|
|
167
|
+
* ```
|
|
168
|
+
*/
|
|
169
|
+
export const Result = {
|
|
170
|
+
/** Wraps a value as a successful result. */
|
|
171
|
+
ok(data) {
|
|
172
|
+
return { data, error: null };
|
|
173
|
+
},
|
|
174
|
+
/** Wraps a failure as a result, accepting either a code or an existing error. */
|
|
175
|
+
fail(error, options = {}) {
|
|
176
|
+
return { data: null, error: typeof error === "string" ? new AndcoError(error, options) : error };
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
/** Error codes the SDK itself raises, as opposed to those relayed from the Authorization Server. */
|
|
180
|
+
export const ANDCO_ERROR_CODES = {
|
|
181
|
+
BROWSER_REQUIRED: "browser_required",
|
|
182
|
+
BROWSER_UNAVAILABLE: "browser_unavailable",
|
|
183
|
+
BROWSER_FORBIDDEN: "browser_forbidden",
|
|
184
|
+
INVALID_CALLBACK: "invalid_callback",
|
|
185
|
+
INVALID_CONFIGURATION: "invalid_configuration",
|
|
186
|
+
HANDSHAKE_TIMEOUT: "handshake_timeout",
|
|
187
|
+
INVALID_IFRAME: "invalid_iframe",
|
|
188
|
+
INVALID_INTENT_ID: "invalid_intent_id",
|
|
189
|
+
INVALID_RESPONSE: "invalid_response",
|
|
190
|
+
POPUP_BLOCKED: "popup_blocked",
|
|
191
|
+
POPUP_TIMEOUT: "popup_timeout",
|
|
192
|
+
PRESENTATION_UNSUPPORTED: "presentation_unsupported",
|
|
193
|
+
SAME_ORIGIN_ENDPOINT: "same_origin_endpoint",
|
|
194
|
+
SESSION_MISSING: "session_missing",
|
|
195
|
+
STORAGE_FAILED: "storage_failed",
|
|
196
|
+
UNTRUSTED_ORIGIN: "untrusted_origin",
|
|
197
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { RANDOM_UUID } from "@andco/protocol";
|
|
2
|
+
export { AndcoAuth, type AndcoAuthOptions, type AndcoSignInOptions } from "./auth.js";
|
|
3
|
+
export { AndcoClient, type AndcoClientAuthed, type AndcoClientOptions, type AndcoGlobals, type AndcoResourceServer, } from "./client.js";
|
|
4
|
+
export { AndcoConfig, type AndcoConfigInput, type AndcoScope, type AndcoTheme, isLoopback, } from "./config.js";
|
|
5
|
+
export { ANDCO_REFRESH_SKEW_SECONDS, type AndcoCredentials, type AndcoSession, type AndcoUser, credentialsFromSession, isCredentials, isExpired, sessionIdentityKey, tokenFor, } from "./credentials.js";
|
|
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";
|
|
8
|
+
export { type AndcoAuthorizationDetail, type AndcoAuthorizationOptions, type AndcoAuthorizationRequest, type AndcoDeviceAuthorization, AndcoOAuth, type AndcoOAuthOptions, type AndcoResourceAuthorization, type AndcoTransportMode, } from "./oauth.js";
|
|
9
|
+
export type { AndcoPresentation, AndcoPresenter, AndcoPresentOptions } from "./presenter.js";
|
|
10
|
+
export { type AndcoHttpClient, type AndcoPageOptions, type AndcoRequestOptions, AndcoRest, type AndcoRestOptions, } from "./rest.js";
|
|
11
|
+
export { type AndcoSessionSource, AndcoSessionStore, type AndcoSessionStoreOptions } from "./session-store.js";
|
|
12
|
+
export { type AndcoLock, type AndcoStorage, InProcessLock, MemoryStorage, WebStorage, } from "./storage.js";
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +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,GACb,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,YAAY,EAAE,iBAAiB,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAC7F,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
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { RANDOM_UUID } from "@andco/protocol";
|
|
2
|
+
export { AndcoAuth } from "./auth.js";
|
|
3
|
+
export { AndcoClient, } from "./client.js";
|
|
4
|
+
export { AndcoConfig, isLoopback, } from "./config.js";
|
|
5
|
+
export { ANDCO_REFRESH_SKEW_SECONDS, credentialsFromSession, isCredentials, isExpired, sessionIdentityKey, tokenFor, } from "./credentials.js";
|
|
6
|
+
export { ANDCO_ERROR_CODES, AndcoAPIError, AndcoError, Result } from "./errors.js";
|
|
7
|
+
export { AndcoIntents, } from "./intents.js";
|
|
8
|
+
export { AndcoOAuth, } from "./oauth.js";
|
|
9
|
+
export { AndcoRest, } from "./rest.js";
|
|
10
|
+
export { AndcoSessionStore } from "./session-store.js";
|
|
11
|
+
export { InProcessLock, MemoryStorage, WebStorage, } from "./storage.js";
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { type AndCoRandomSource } from "@andco/protocol";
|
|
2
|
+
import { type AndCoEventPage, type AndCoFinancialEvent, type AndCoIntent } from "@andco/protocol/transport";
|
|
3
|
+
import { Result } from "./errors.js";
|
|
4
|
+
import type { AndcoRest } from "./rest.js";
|
|
5
|
+
export type { AndCoEventPage as AndcoEventPage, AndCoFinancialEvent as AndcoFinancialEvent, AndCoIntent as AndcoIntent, };
|
|
6
|
+
/** An intent identifier, or a factory that produces one after the user activates a launcher. */
|
|
7
|
+
export type AndcoIntentId = string | (() => Promise<string>);
|
|
8
|
+
export type AndcoIntentCreateOptions = {
|
|
9
|
+
/** A stable transport key. One is generated when omitted. */
|
|
10
|
+
idempotencyKey?: string;
|
|
11
|
+
};
|
|
12
|
+
export type AndcoEventSubscriptionOptions = {
|
|
13
|
+
/** Resume after a checkpoint this subject's subscription previously acknowledged. */
|
|
14
|
+
after?: string;
|
|
15
|
+
/** Receives transport, authorization, cursor, and handler failures. */
|
|
16
|
+
onError: (error: unknown) => void;
|
|
17
|
+
/**
|
|
18
|
+
* Fires after a handler succeeds, carrying the cursor that is now safe to persist.
|
|
19
|
+
*
|
|
20
|
+
* Pushed rather than read off the returned object, so the return value can stay a plain function
|
|
21
|
+
* and a caller never has to retain a subscription just to learn where it got to.
|
|
22
|
+
*/
|
|
23
|
+
onCheckpoint?: (cursor: string) => void;
|
|
24
|
+
pollIntervalMs?: number;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Creates and reads Intents under the current Grant, and observes the durable facts they produce.
|
|
28
|
+
*
|
|
29
|
+
* This is the domain-neutral Intent lifecycle only. Deposit, withdrawal, automatic charge, and the
|
|
30
|
+
* four typed event subscriptions that used to live here now belong to the Bank Resource Server
|
|
31
|
+
* Definition, which composes `create`, `events`, and `subscribe` exactly as a third party would —
|
|
32
|
+
* a defect in this extension point is meant to surface in Andco's own code first.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* const { data: deposit } = await andco.intents.create("deposit", depositInput);
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export declare class AndcoIntents {
|
|
40
|
+
#private;
|
|
41
|
+
constructor(rest: AndcoRest, crypto?: AndCoRandomSource);
|
|
42
|
+
/**
|
|
43
|
+
* Creates an Intent of the given type. The exact Grant determines what it may do and where.
|
|
44
|
+
*
|
|
45
|
+
* `type` is both sent as the body's own `type` and checked against the response's, so a Resource
|
|
46
|
+
* Server Definition never makes its caller repeat the Intent type it already named, and catches a
|
|
47
|
+
* mismatched creation immediately rather than handing back a value shaped like the wrong kind of
|
|
48
|
+
* Intent.
|
|
49
|
+
*
|
|
50
|
+
* `T` defaults to the Intent union of Andco's own contract; a third-party Resource Server
|
|
51
|
+
* Definition names its own response type here.
|
|
52
|
+
*/
|
|
53
|
+
create<T = AndCoIntent>(type: string, input: object, options?: AndcoIntentCreateOptions): Promise<Result<T>>;
|
|
54
|
+
/** Reads the authoritative state of one Intent. This, not a callback, is the source of truth. */
|
|
55
|
+
get<T = AndCoIntent>(intentId: string): Promise<Result<T>>;
|
|
56
|
+
/** Executes an intent whose Grant permits it. Completion must still be confirmed with `get`. */
|
|
57
|
+
execute(intentId: string): Promise<Result<void>>;
|
|
58
|
+
/** Reads one page of durable Intent facts, replaying after an opaque checkpoint. */
|
|
59
|
+
events(intentId: string, after?: string, signal?: AbortSignal): Promise<Result<AndCoEventPage>>;
|
|
60
|
+
/**
|
|
61
|
+
* Subscribes to a durable, paged feed of facts, reusing the Protocol Contract's paging, cursor,
|
|
62
|
+
* checkpoint, and poll-interval reconciliation.
|
|
63
|
+
*
|
|
64
|
+
* Domain-neutral by design: the caller supplies both the page fetcher (`read`) and the `subject`
|
|
65
|
+
* that scopes its cursor namespace, so nothing here assumes an Intent, an account, or any other
|
|
66
|
+
* kind of subject. That is what lets a Resource Server Definition — the Bank one included —
|
|
67
|
+
* compose this loop for its own event feed instead of rebuilding paging and checkpointing from
|
|
68
|
+
* scratch, which is the one genuinely difficult piece of this surface.
|
|
69
|
+
*
|
|
70
|
+
* Every subscription returns its own unsubscribe function, so binding one to a framework effect —
|
|
71
|
+
* `useEffect`, Svelte's `$effect`, Vue's `watchEffect` — is a single line.
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```ts
|
|
75
|
+
* const unsubscribe = andco.intents.subscribe(
|
|
76
|
+
* { kind: "intent", id: intentId },
|
|
77
|
+
* (after, signal) => andco.intents.events(intentId, after, signal),
|
|
78
|
+
* (event) => (event.type.startsWith("deposit.") ? event : null),
|
|
79
|
+
* (event) => setDeposit(event),
|
|
80
|
+
* { onError },
|
|
81
|
+
* );
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
subscribe<E extends AndCoFinancialEvent>(subject: {
|
|
85
|
+
kind: string;
|
|
86
|
+
id: string;
|
|
87
|
+
}, read: (after: string | undefined, signal: AbortSignal) => Promise<Result<AndCoEventPage>>, decode: (event: AndCoFinancialEvent) => E | null, handler: (event: E) => void | Promise<void>, options: AndcoEventSubscriptionOptions): () => void;
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=intents.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"intents.d.ts","sourceRoot":"","sources":["../src/intents.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,iBAAiB,EAAe,MAAM,iBAAiB,CAAC;AACtE,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,mBAAmB,EACxB,KAAK,WAAW,EAEjB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAiC,MAAM,EAAE,MAAM,aAAa,CAAC;AACpE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAE3C,YAAY,EACV,cAAc,IAAI,cAAc,EAChC,mBAAmB,IAAI,mBAAmB,EAC1C,WAAW,IAAI,WAAW,GAC3B,CAAC;AAEF,gGAAgG;AAChG,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,CAAC,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;AAK7D,MAAM,MAAM,wBAAwB,GAAG;IACrC,6DAA6D;IAC7D,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,6BAA6B,GAAG;IAC1C,qFAAqF;IACrF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uEAAuE;IACvE,OAAO,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IAClC;;;;;OAKG;IACH,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,qBAAa,YAAY;;gBAIJ,IAAI,EAAE,SAAS,EAAE,MAAM,CAAC,EAAE,iBAAiB;IAK9D;;;;;;;;;;OAUG;IACU,MAAM,CAAC,CAAC,GAAG,WAAW,EACjC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,wBAA6B,GACrC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IAuBrB,iGAAiG;IACpF,GAAG,CAAC,CAAC,GAAG,WAAW,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IAavE,gGAAgG;IACnF,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAa7D,oFAAoF;IACvE,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;IAgB5G;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACI,SAAS,CAAC,CAAC,SAAS,mBAAmB,EAC5C,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,EACrC,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,EACzF,MAAM,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,CAAC,GAAG,IAAI,EAChD,OAAO,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,EAC3C,OAAO,EAAE,6BAA6B,GACrC,MAAM,IAAI;CAuBd"}
|
package/dist/intents.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { RANDOM_UUID } from "@andco/protocol";
|
|
2
|
+
import { subscribeFinancialEvents, } from "@andco/protocol/transport";
|
|
3
|
+
import { ANDCO_ERROR_CODES, AndcoError, Result } from "./errors.js";
|
|
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
|
+
const IDEMPOTENCY_KEY_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
6
|
+
/**
|
|
7
|
+
* Creates and reads Intents under the current Grant, and observes the durable facts they produce.
|
|
8
|
+
*
|
|
9
|
+
* This is the domain-neutral Intent lifecycle only. Deposit, withdrawal, automatic charge, and the
|
|
10
|
+
* four typed event subscriptions that used to live here now belong to the Bank Resource Server
|
|
11
|
+
* Definition, which composes `create`, `events`, and `subscribe` exactly as a third party would —
|
|
12
|
+
* a defect in this extension point is meant to surface in Andco's own code first.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* const { data: deposit } = await andco.intents.create("deposit", depositInput);
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export class AndcoIntents {
|
|
20
|
+
#rest;
|
|
21
|
+
#crypto;
|
|
22
|
+
constructor(rest, crypto) {
|
|
23
|
+
this.#rest = rest;
|
|
24
|
+
this.#crypto = crypto;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Creates an Intent of the given type. The exact Grant determines what it may do and where.
|
|
28
|
+
*
|
|
29
|
+
* `type` is both sent as the body's own `type` and checked against the response's, so a Resource
|
|
30
|
+
* Server Definition never makes its caller repeat the Intent type it already named, and catches a
|
|
31
|
+
* mismatched creation immediately rather than handing back a value shaped like the wrong kind of
|
|
32
|
+
* Intent.
|
|
33
|
+
*
|
|
34
|
+
* `T` defaults to the Intent union of Andco's own contract; a third-party Resource Server
|
|
35
|
+
* Definition names its own response type here.
|
|
36
|
+
*/
|
|
37
|
+
async create(type, input, options = {}) {
|
|
38
|
+
const idempotencyKey = options.idempotencyKey ?? RANDOM_UUID(this.#crypto);
|
|
39
|
+
if (!IDEMPOTENCY_KEY_PATTERN.test(idempotencyKey)) {
|
|
40
|
+
return Result.fail(ANDCO_ERROR_CODES.INVALID_CONFIGURATION, { message: "malformed Idempotency-Key" });
|
|
41
|
+
}
|
|
42
|
+
let data;
|
|
43
|
+
try {
|
|
44
|
+
({ data } = await this.#rest.http
|
|
45
|
+
// The generated body type is a bank-specific union; this method is neutral over `type`.
|
|
46
|
+
.POST("/intents", {
|
|
47
|
+
params: { header: { "Idempotency-Key": idempotencyKey } },
|
|
48
|
+
body: { ...input, type },
|
|
49
|
+
})
|
|
50
|
+
.throwOnError());
|
|
51
|
+
}
|
|
52
|
+
catch (cause) {
|
|
53
|
+
return Result.fail(cause);
|
|
54
|
+
}
|
|
55
|
+
if (data?.type !== type) {
|
|
56
|
+
return Result.fail(ANDCO_ERROR_CODES.INVALID_RESPONSE, { message: `expected a ${type} intent` });
|
|
57
|
+
}
|
|
58
|
+
return Result.ok(data);
|
|
59
|
+
}
|
|
60
|
+
/** Reads the authoritative state of one Intent. This, not a callback, is the source of truth. */
|
|
61
|
+
async get(intentId) {
|
|
62
|
+
const invalid = assertIntentId(intentId);
|
|
63
|
+
if (invalid)
|
|
64
|
+
return Result.fail(invalid);
|
|
65
|
+
try {
|
|
66
|
+
const { data } = await this.#rest.http
|
|
67
|
+
.GET("/intents/{intent_id}", { params: { path: { intent_id: intentId } } })
|
|
68
|
+
.throwOnError();
|
|
69
|
+
return Result.ok(data);
|
|
70
|
+
}
|
|
71
|
+
catch (cause) {
|
|
72
|
+
return Result.fail(cause);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/** Executes an intent whose Grant permits it. Completion must still be confirmed with `get`. */
|
|
76
|
+
async execute(intentId) {
|
|
77
|
+
const invalid = assertIntentId(intentId);
|
|
78
|
+
if (invalid)
|
|
79
|
+
return Result.fail(invalid);
|
|
80
|
+
try {
|
|
81
|
+
await this.#rest.http
|
|
82
|
+
.POST("/intents/{intent_id}/execute", { params: { path: { intent_id: intentId } } })
|
|
83
|
+
.throwOnError();
|
|
84
|
+
return Result.ok(undefined);
|
|
85
|
+
}
|
|
86
|
+
catch (cause) {
|
|
87
|
+
return Result.fail(cause);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** Reads one page of durable Intent facts, replaying after an opaque checkpoint. */
|
|
91
|
+
async events(intentId, after, signal) {
|
|
92
|
+
const invalid = assertIntentId(intentId);
|
|
93
|
+
if (invalid)
|
|
94
|
+
return Result.fail(invalid);
|
|
95
|
+
try {
|
|
96
|
+
const { data } = await this.#rest.http
|
|
97
|
+
.GET("/intents/{intent_id}/events", {
|
|
98
|
+
params: { path: { intent_id: intentId }, query: { after, limit: 100 } },
|
|
99
|
+
signal,
|
|
100
|
+
})
|
|
101
|
+
.throwOnError();
|
|
102
|
+
return Result.ok(data);
|
|
103
|
+
}
|
|
104
|
+
catch (cause) {
|
|
105
|
+
return Result.fail(cause);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Subscribes to a durable, paged feed of facts, reusing the Protocol Contract's paging, cursor,
|
|
110
|
+
* checkpoint, and poll-interval reconciliation.
|
|
111
|
+
*
|
|
112
|
+
* Domain-neutral by design: the caller supplies both the page fetcher (`read`) and the `subject`
|
|
113
|
+
* that scopes its cursor namespace, so nothing here assumes an Intent, an account, or any other
|
|
114
|
+
* kind of subject. That is what lets a Resource Server Definition — the Bank one included —
|
|
115
|
+
* compose this loop for its own event feed instead of rebuilding paging and checkpointing from
|
|
116
|
+
* scratch, which is the one genuinely difficult piece of this surface.
|
|
117
|
+
*
|
|
118
|
+
* Every subscription returns its own unsubscribe function, so binding one to a framework effect —
|
|
119
|
+
* `useEffect`, Svelte's `$effect`, Vue's `watchEffect` — is a single line.
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```ts
|
|
123
|
+
* const unsubscribe = andco.intents.subscribe(
|
|
124
|
+
* { kind: "intent", id: intentId },
|
|
125
|
+
* (after, signal) => andco.intents.events(intentId, after, signal),
|
|
126
|
+
* (event) => (event.type.startsWith("deposit.") ? event : null),
|
|
127
|
+
* (event) => setDeposit(event),
|
|
128
|
+
* { onError },
|
|
129
|
+
* );
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
subscribe(subject, read, decode, handler, options) {
|
|
133
|
+
const subscription = subscribeFinancialEvents(async (after, signal) => {
|
|
134
|
+
const page = await read(after, signal);
|
|
135
|
+
if (page.error)
|
|
136
|
+
throw page.error;
|
|
137
|
+
return page.data;
|
|
138
|
+
}, decode,
|
|
139
|
+
// The checkpoint is pushed only once a handler has succeeded, so persisting it can never
|
|
140
|
+
// acknowledge an event the application failed to process.
|
|
141
|
+
async (event) => {
|
|
142
|
+
await handler(event);
|
|
143
|
+
options.onCheckpoint?.(event.cursor);
|
|
144
|
+
}, {
|
|
145
|
+
...(options.after === undefined ? {} : { after: options.after }),
|
|
146
|
+
onError: options.onError,
|
|
147
|
+
...(options.pollIntervalMs === undefined ? {} : { pollIntervalMs: options.pollIntervalMs }),
|
|
148
|
+
}, subject);
|
|
149
|
+
return () => subscription.unsubscribe();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/** Intent identifiers are UUIDs; anything else never reaches the network. */
|
|
153
|
+
function assertIntentId(value) {
|
|
154
|
+
return INTENT_ID_PATTERN.test(value)
|
|
155
|
+
? null
|
|
156
|
+
: new AndcoError(ANDCO_ERROR_CODES.INVALID_INTENT_ID, { message: `${value} is not a valid identifier` });
|
|
157
|
+
}
|