@shuttle.technology/authorization 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +191 -0
- package/README.md +1124 -2
- package/dist/accountLists.d.ts +28 -0
- package/dist/accountLists.js +27 -0
- package/dist/aesGcmSiv.d.ts +14 -0
- package/dist/aesGcmSiv.js +160 -0
- package/dist/auth.d.ts +68 -0
- package/dist/auth.js +189 -0
- package/dist/balances.d.ts +31 -0
- package/dist/balances.js +44 -0
- package/dist/canonicalize.d.ts +19 -0
- package/dist/canonicalize.js +107 -0
- package/dist/client.d.ts +152 -0
- package/dist/client.js +781 -0
- package/dist/clientTypes.d.ts +56 -0
- package/dist/clientTypes.js +5 -0
- package/dist/configuration.d.ts +48 -0
- package/dist/configuration.js +72 -0
- package/dist/consentDebit.d.ts +36 -0
- package/dist/consentDebit.js +95 -0
- package/dist/credentials.d.ts +116 -0
- package/dist/credentials.js +315 -0
- package/dist/debitPermissionWire.d.ts +90 -0
- package/dist/debitPermissionWire.js +118 -0
- package/dist/debitPermissions.d.ts +39 -0
- package/dist/debitPermissions.js +117 -0
- package/dist/deviceKey.d.ts +13 -0
- package/dist/deviceKey.js +28 -0
- package/dist/deviceRecovery.d.ts +40 -0
- package/dist/deviceRecovery.js +143 -0
- package/dist/deviceShare.d.ts +9 -0
- package/dist/deviceShare.js +24 -0
- package/dist/deviceShareEnvelope.d.ts +13 -0
- package/dist/deviceShareEnvelope.js +139 -0
- package/dist/deviceShares.d.ts +91 -0
- package/dist/deviceShares.js +204 -0
- package/dist/errors.d.ts +280 -0
- package/dist/errors.js +85 -0
- package/dist/linkedChecks.d.ts +17 -0
- package/dist/linkedChecks.js +31 -0
- package/dist/linkedRecovery.d.ts +40 -0
- package/dist/linkedRecovery.js +49 -0
- package/dist/linkedRegistrations.d.ts +11 -0
- package/dist/linkedRegistrations.js +109 -0
- package/dist/linkedSendDisplay.d.ts +73 -0
- package/dist/linkedSendDisplay.js +84 -0
- package/dist/linkedWalletSignals.d.ts +22 -0
- package/dist/linkedWalletSignals.js +101 -0
- package/dist/linkedWallets.d.ts +401 -0
- package/dist/linkedWallets.js +436 -0
- package/dist/me.d.ts +27 -0
- package/dist/me.js +30 -0
- package/dist/mediatedOperation.d.ts +27 -0
- package/dist/mediatedOperation.js +68 -0
- package/dist/mobile.d.ts +64 -0
- package/dist/mobile.js +69 -0
- package/dist/modeC.d.ts +131 -0
- package/dist/modeC.js +248 -0
- package/dist/modeCLinked.d.ts +207 -0
- package/dist/modeCLinked.js +270 -0
- package/dist/pages.d.ts +22 -0
- package/dist/pages.js +45 -0
- package/dist/poll.d.ts +15 -0
- package/dist/poll.js +76 -0
- package/dist/redirects.d.ts +45 -0
- package/dist/redirects.js +108 -0
- package/dist/requestGrant.d.ts +169 -0
- package/dist/requestGrant.js +243 -0
- package/dist/sends.d.ts +89 -0
- package/dist/sends.js +127 -0
- package/dist/serviceGrants.d.ts +52 -0
- package/dist/serviceGrants.js +46 -0
- package/dist/slice4.d.ts +187 -0
- package/dist/slice4.js +299 -0
- package/dist/streams.d.ts +212 -0
- package/dist/streams.js +282 -0
- package/dist/trackedWallets.d.ts +53 -0
- package/dist/trackedWallets.js +104 -0
- package/dist/transferStatus.d.ts +45 -0
- package/dist/transferStatus.js +43 -0
- package/dist/walletCreateGrant.d.ts +62 -0
- package/dist/walletCreateGrant.js +89 -0
- package/dist/walletDisplayParts.d.ts +17 -0
- package/dist/walletDisplayParts.js +47 -0
- package/dist/walletEvents.d.ts +43 -0
- package/dist/walletEvents.js +97 -0
- package/dist/walletLimits.d.ts +65 -0
- package/dist/walletLimits.js +102 -0
- package/dist/walletOperations.d.ts +209 -0
- package/dist/walletOperations.js +317 -0
- package/dist/webRenewal.d.ts +92 -0
- package/dist/webRenewal.js +179 -0
- package/package.json +65 -3
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type AuthIntent } from "./auth.ts";
|
|
2
|
+
import type { Session } from "./client.ts";
|
|
3
|
+
/**
|
|
4
|
+
* HostedSignIn is a sign-in started on the provider's hosted page. The person
|
|
5
|
+
* picks the method there. On the web the state and the S256 verifier stay in
|
|
6
|
+
* this client's memory until `exchangeHandoff` receives the redirect back; on
|
|
7
|
+
* mobile the system browser hands the redirect back to the same call, which
|
|
8
|
+
* exchanges it and answers the session.
|
|
9
|
+
*/
|
|
10
|
+
export interface HostedSignIn {
|
|
11
|
+
intent: AuthIntent;
|
|
12
|
+
launchUrl: string;
|
|
13
|
+
session?: Session;
|
|
14
|
+
}
|
|
15
|
+
/** hostedSignInUrl is the provider's sign-in page for one client, intent, state and S256 challenge. */
|
|
16
|
+
export declare function hostedSignInUrl(authorizationOrigin: string, input: {
|
|
17
|
+
clientId: string;
|
|
18
|
+
redirectUri: string;
|
|
19
|
+
state: string;
|
|
20
|
+
challenge: string;
|
|
21
|
+
intent: AuthIntent;
|
|
22
|
+
}): string;
|
|
23
|
+
/**
|
|
24
|
+
* handoffCallback reads the redirect back from a sign-in at the exact
|
|
25
|
+
* registered redirect. A sign-in begun on the hosted page returns the code and
|
|
26
|
+
* the state alone; one begun through the attempt API also names its attempt.
|
|
27
|
+
*/
|
|
28
|
+
export declare function handoffCallback(value: string, expected: string): {
|
|
29
|
+
attemptId?: string;
|
|
30
|
+
state: string;
|
|
31
|
+
code: string;
|
|
32
|
+
};
|
|
33
|
+
/** restoreCallback reads the redirect back from a web restore at the exact registered redirect. */
|
|
34
|
+
export declare function restoreCallback(value: string, expected: string): {
|
|
35
|
+
restoreId: string;
|
|
36
|
+
state: string;
|
|
37
|
+
code: string;
|
|
38
|
+
};
|
|
39
|
+
/** The app's own exact origin: https on any host, or http on a loopback host. */
|
|
40
|
+
export declare function exactAppOrigin(value: string): boolean;
|
|
41
|
+
/** A redirect back to the app: https on any host, or http on a loopback host; no credentials, query or fragment. */
|
|
42
|
+
export declare function exactAppRedirect(value: string): boolean;
|
|
43
|
+
export declare function exactHttpsOrigin(value: string): boolean;
|
|
44
|
+
export declare function exactHttpsRedirect(value: string): boolean;
|
|
45
|
+
export declare function httpsURL(value: unknown): value is string;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// The provider's top-level sign-in page, the redirects that come back to the
|
|
2
|
+
// application from a sign-in or a restore, and the exact HTTPS shapes the
|
|
3
|
+
// configuration and the provider's answers are checked against.
|
|
4
|
+
import { ClientFlowError, opaqueId } from "./auth.js";
|
|
5
|
+
/** hostedSignInUrl is the provider's sign-in page for one client, intent, state and S256 challenge. */
|
|
6
|
+
export function hostedSignInUrl(authorizationOrigin, input) {
|
|
7
|
+
const start = new URL("/v1/auth/start", authorizationOrigin);
|
|
8
|
+
start.searchParams.set("client_id", input.clientId);
|
|
9
|
+
start.searchParams.set("redirect_uri", input.redirectUri);
|
|
10
|
+
start.searchParams.set("state", input.state);
|
|
11
|
+
start.searchParams.set("code_challenge", input.challenge);
|
|
12
|
+
start.searchParams.set("code_challenge_method", "S256");
|
|
13
|
+
start.searchParams.set("intent", input.intent);
|
|
14
|
+
return start.toString();
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* handoffCallback reads the redirect back from a sign-in at the exact
|
|
18
|
+
* registered redirect. A sign-in begun on the hosted page returns the code and
|
|
19
|
+
* the state alone; one begun through the attempt API also names its attempt.
|
|
20
|
+
*/
|
|
21
|
+
export function handoffCallback(value, expected) {
|
|
22
|
+
const callback = exactRedirect(value, expected);
|
|
23
|
+
const code = callback.searchParams.getAll("code");
|
|
24
|
+
const state = callback.searchParams.getAll("state");
|
|
25
|
+
const attemptId = callback.searchParams.getAll("attempt_id");
|
|
26
|
+
const size = callback.searchParams.size;
|
|
27
|
+
if (code.length !== 1 || state.length !== 1 || code[0] === "" || state[0] === "" || !((size === 2 && attemptId.length === 0) || (size === 3 && attemptId.length === 1 && opaqueId(attemptId[0])))) {
|
|
28
|
+
throw new ClientFlowError("invalid_callback");
|
|
29
|
+
}
|
|
30
|
+
return attemptId.length === 1 ? { code: code[0], state: state[0], attemptId: attemptId[0] } : { code: code[0], state: state[0] };
|
|
31
|
+
}
|
|
32
|
+
/** restoreCallback reads the redirect back from a web restore at the exact registered redirect. */
|
|
33
|
+
export function restoreCallback(value, expected) {
|
|
34
|
+
const callback = exactRedirect(value, expected);
|
|
35
|
+
const code = callback.searchParams.getAll("code");
|
|
36
|
+
const state = callback.searchParams.getAll("state");
|
|
37
|
+
const restoreId = callback.searchParams.getAll("restore_id");
|
|
38
|
+
if (callback.searchParams.size !== 3 || code.length !== 1 || state.length !== 1 || restoreId.length !== 1 || code[0] === "" || state[0] === "" || !opaqueId(restoreId[0]))
|
|
39
|
+
throw new ClientFlowError("invalid_callback");
|
|
40
|
+
return { code: code[0], state: state[0], restoreId: restoreId[0] };
|
|
41
|
+
}
|
|
42
|
+
function exactRedirect(value, expected) {
|
|
43
|
+
let callback;
|
|
44
|
+
try {
|
|
45
|
+
callback = new URL(value);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
throw new ClientFlowError("invalid_callback");
|
|
49
|
+
}
|
|
50
|
+
const target = new URL(expected);
|
|
51
|
+
if (`${callback.origin}${callback.pathname}` !== `${target.origin}${target.pathname}` || callback.hash !== "")
|
|
52
|
+
throw new ClientFlowError("invalid_callback");
|
|
53
|
+
return callback;
|
|
54
|
+
}
|
|
55
|
+
/** A loopback host a browser treats as a secure context over plain http (local development). */
|
|
56
|
+
function loopbackHost(hostname) {
|
|
57
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
|
|
58
|
+
}
|
|
59
|
+
/** The app's own exact origin: https on any host, or http on a loopback host. */
|
|
60
|
+
export function exactAppOrigin(value) {
|
|
61
|
+
try {
|
|
62
|
+
const parsed = new URL(value);
|
|
63
|
+
const secure = parsed.protocol === "https:" || (parsed.protocol === "http:" && loopbackHost(parsed.hostname));
|
|
64
|
+
return secure && parsed.username === "" && parsed.password === "" && parsed.pathname === "/" && parsed.search === "" && parsed.hash === "" && parsed.origin === value;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** A redirect back to the app: https on any host, or http on a loopback host; no credentials, query or fragment. */
|
|
71
|
+
export function exactAppRedirect(value) {
|
|
72
|
+
try {
|
|
73
|
+
const parsed = new URL(value);
|
|
74
|
+
const secure = parsed.protocol === "https:" || (parsed.protocol === "http:" && loopbackHost(parsed.hostname));
|
|
75
|
+
return secure && parsed.username === "" && parsed.password === "" && parsed.search === "" && parsed.hash === "";
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
export function exactHttpsOrigin(value) {
|
|
82
|
+
try {
|
|
83
|
+
const parsed = new URL(value);
|
|
84
|
+
return parsed.protocol === "https:" && parsed.username === "" && parsed.password === "" && parsed.pathname === "/" && parsed.search === "" && parsed.hash === "" && parsed.origin === value;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
export function exactHttpsRedirect(value) {
|
|
91
|
+
try {
|
|
92
|
+
const parsed = new URL(value);
|
|
93
|
+
return parsed.protocol === "https:" && parsed.username === "" && parsed.password === "" && parsed.search === "" && parsed.hash === "";
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
export function httpsURL(value) {
|
|
100
|
+
if (typeof value !== "string")
|
|
101
|
+
return false;
|
|
102
|
+
try {
|
|
103
|
+
return new URL(value).protocol === "https:";
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/** RequestPublicJwk is the closed enrollment key: no private member exists. */
|
|
2
|
+
export interface RequestPublicJwk {
|
|
3
|
+
readonly kty: "EC";
|
|
4
|
+
readonly crv: "P-256";
|
|
5
|
+
readonly x: string;
|
|
6
|
+
readonly y: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* RequestSigningKey is one client-held key. `privateKey` is non-extractable and
|
|
10
|
+
* usable only for local ES256 signing; it is never serialized and never leaves
|
|
11
|
+
* this object.
|
|
12
|
+
*/
|
|
13
|
+
export interface RequestSigningKey {
|
|
14
|
+
readonly handle: string;
|
|
15
|
+
readonly publicJwk: RequestPublicJwk;
|
|
16
|
+
readonly thumbprint: string;
|
|
17
|
+
readonly privateKey: CryptoKey;
|
|
18
|
+
}
|
|
19
|
+
/** RequestGrantBounds is the frozen finite transfer authority of one grant. */
|
|
20
|
+
export interface RequestGrantBounds {
|
|
21
|
+
readonly network: string;
|
|
22
|
+
readonly recipientAllowlist: readonly string[];
|
|
23
|
+
readonly maxAmountRaw: string;
|
|
24
|
+
readonly maxFeeRaw: string;
|
|
25
|
+
}
|
|
26
|
+
/** RequestGrantBinding is the complete scoped identity the grant froze. */
|
|
27
|
+
export interface RequestGrantBinding {
|
|
28
|
+
readonly tenantId: string;
|
|
29
|
+
readonly applicationId: string;
|
|
30
|
+
readonly environmentId: string;
|
|
31
|
+
readonly clientId: string;
|
|
32
|
+
readonly accountId: string;
|
|
33
|
+
readonly sessionId: string;
|
|
34
|
+
readonly servicePrincipalId: string;
|
|
35
|
+
readonly walletId: string;
|
|
36
|
+
readonly walletGeneration: string;
|
|
37
|
+
readonly mappingVersion: string;
|
|
38
|
+
}
|
|
39
|
+
/** RequestGrantResult is the protected `session.authorize_requests` result. */
|
|
40
|
+
export interface RequestGrantResult {
|
|
41
|
+
readonly requestGrantId: string;
|
|
42
|
+
readonly publicKeyThumbprint: string;
|
|
43
|
+
/** Integer NumericDate seconds (source §2.2), not an RFC 3339 string. */
|
|
44
|
+
readonly serverTime: number;
|
|
45
|
+
/** Integer NumericDate seconds (source §2.2), not an RFC 3339 string. */
|
|
46
|
+
readonly expiresAt: number;
|
|
47
|
+
readonly binding: RequestGrantBinding;
|
|
48
|
+
readonly bounds: RequestGrantBounds;
|
|
49
|
+
}
|
|
50
|
+
/** RequestGrantEnrollment is the caller-declared enrollment intent. */
|
|
51
|
+
export interface RequestGrantEnrollment {
|
|
52
|
+
readonly ownerId: string;
|
|
53
|
+
readonly servicePrincipalId: string;
|
|
54
|
+
readonly walletId: string;
|
|
55
|
+
readonly network: string;
|
|
56
|
+
readonly recipientAllowlist: readonly string[];
|
|
57
|
+
readonly maxAmountRaw: string;
|
|
58
|
+
readonly maxFeeRaw: string;
|
|
59
|
+
}
|
|
60
|
+
/** RequestGrantEnrollmentCreate is the closed OperationCreate of source §2.2. */
|
|
61
|
+
export interface RequestGrantEnrollmentCreate {
|
|
62
|
+
readonly kind: "session.authorize_requests";
|
|
63
|
+
readonly owner_type: "account";
|
|
64
|
+
readonly owner_id: string;
|
|
65
|
+
readonly payload: {
|
|
66
|
+
readonly public_jwk: RequestPublicJwk;
|
|
67
|
+
readonly service_principal_id: string;
|
|
68
|
+
readonly wallet_id: string;
|
|
69
|
+
readonly network: string;
|
|
70
|
+
readonly recipient_allowlist: readonly string[];
|
|
71
|
+
readonly max_amount_raw: string;
|
|
72
|
+
readonly max_fee_raw: string;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/** PairedRequestGrant is one grant result bound to the exact key that owns it. */
|
|
76
|
+
export interface PairedRequestGrant {
|
|
77
|
+
readonly grant: RequestGrantResult;
|
|
78
|
+
readonly key: RequestSigningKey;
|
|
79
|
+
}
|
|
80
|
+
/** MaxRequestGrantRecipients is the source §2.8 allowlist bound. */
|
|
81
|
+
export declare const MaxRequestGrantRecipients = 10;
|
|
82
|
+
/** MaxRequestGrantLifetimeSeconds is the source §2.5 ceiling of fifteen minutes. */
|
|
83
|
+
export declare const MaxRequestGrantLifetimeSeconds: number;
|
|
84
|
+
/**
|
|
85
|
+
* generateRequestSigningKey creates the non-extractable P-256 key of source
|
|
86
|
+
* §2.5. It refuses a runtime that returns an extractable private key or a
|
|
87
|
+
* public JWK carrying any private member, so a weakened WebCrypto cannot
|
|
88
|
+
* silently become custody of a user key.
|
|
89
|
+
*/
|
|
90
|
+
export declare function generateRequestSigningKey(crypto: Crypto): Promise<RequestSigningKey>;
|
|
91
|
+
/**
|
|
92
|
+
* jwkThumbprint is the RFC 7638 SHA-256 thumbprint in unpadded base64url over
|
|
93
|
+
* exactly the required EC members. The canonicalizer already orders them
|
|
94
|
+
* lexicographically, which is the order RFC 7638 prescribes.
|
|
95
|
+
*/
|
|
96
|
+
export declare function jwkThumbprint(crypto: Crypto, key: RequestPublicJwk): Promise<string>;
|
|
97
|
+
/**
|
|
98
|
+
* buildRequestGrantEnrollment returns the closed `session.authorize_requests`
|
|
99
|
+
* OperationCreate. Every bound is validated locally first, so an empty or
|
|
100
|
+
* over-length allowlist, a duplicate or wildcard recipient and a noncanonical
|
|
101
|
+
* or out-of-range ceiling never leave the client.
|
|
102
|
+
*/
|
|
103
|
+
export declare function buildRequestGrantEnrollment(key: RequestSigningKey, enrollment: RequestGrantEnrollment): RequestGrantEnrollmentCreate;
|
|
104
|
+
/**
|
|
105
|
+
* RequestGrantEnrollmentHost is the operation contract `enrollRequestGrant`
|
|
106
|
+
* drives: creation, fresh provider-hosted passkey confirmation, explicit
|
|
107
|
+
* execution and the protected result read remain four separate calls, exactly
|
|
108
|
+
* as every other local operation in this SDK. The host is supplied by the
|
|
109
|
+
* caller so this one function can be pointed at the served route.
|
|
110
|
+
*/
|
|
111
|
+
export interface RequestGrantEnrollmentHost {
|
|
112
|
+
create(create: RequestGrantEnrollmentCreate, idempotencyKey: string): Promise<{
|
|
113
|
+
operationId: string;
|
|
114
|
+
version: string;
|
|
115
|
+
}>;
|
|
116
|
+
confirm(operationId: string): Promise<{
|
|
117
|
+
operationId: string;
|
|
118
|
+
version: string;
|
|
119
|
+
}>;
|
|
120
|
+
execute(operationId: string, version: string, idempotencyKey: string): Promise<void>;
|
|
121
|
+
result(operationId: string): Promise<unknown>;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* enrollRequestGrant performs one explicit enrollment: it never runs from a
|
|
125
|
+
* refresh, reload or login callback, and it returns the protected grant result
|
|
126
|
+
* paired with the key that will sign under it. The caller must have obtained
|
|
127
|
+
* the hosted passkey confirmation through `confirm`; no proof is generated here
|
|
128
|
+
* and no mediated request is sent.
|
|
129
|
+
*/
|
|
130
|
+
export declare function enrollRequestGrant(host: RequestGrantEnrollmentHost, key: RequestSigningKey, enrollment: RequestGrantEnrollment, keys: {
|
|
131
|
+
readonly create: string;
|
|
132
|
+
readonly execute: string;
|
|
133
|
+
}): Promise<RequestGrantResult>;
|
|
134
|
+
/**
|
|
135
|
+
* parseRequestGrantResultEnvelope accepts the `{request_id,result}` protected
|
|
136
|
+
* result envelope and requires the grant to belong to this key and to the
|
|
137
|
+
* enrollment that was submitted.
|
|
138
|
+
*/
|
|
139
|
+
export declare function parseRequestGrantResultEnvelope(payload: unknown, key: RequestSigningKey, enrollment: RequestGrantEnrollment): {
|
|
140
|
+
requestId: string;
|
|
141
|
+
result: RequestGrantResult;
|
|
142
|
+
};
|
|
143
|
+
/** parseRequestGrantResult accepts exactly the closed grant result of source §2.2. */
|
|
144
|
+
export declare function parseRequestGrantResult(payload: unknown): RequestGrantResult;
|
|
145
|
+
/**
|
|
146
|
+
* RequestGrantStore keeps one grant result beside the exact key that owns it,
|
|
147
|
+
* in memory only. It never touches browser storage, never renews a grant and
|
|
148
|
+
* never re-enrolls: a released, absent or expired pairing reports
|
|
149
|
+
* `request_grant_reenrollment_required` so the caller performs an explicit
|
|
150
|
+
* fresh-UV enrollment instead.
|
|
151
|
+
*/
|
|
152
|
+
export declare class RequestGrantStore {
|
|
153
|
+
#private;
|
|
154
|
+
/** pair refuses any grant result that is not this key's own enrolled grant. */
|
|
155
|
+
pair(key: RequestSigningKey, grant: RequestGrantResult, enrollment: RequestGrantEnrollment): PairedRequestGrant;
|
|
156
|
+
/**
|
|
157
|
+
* current returns the live pairing. An absent pairing is a lost key, and an
|
|
158
|
+
* expired grant is over: neither is repaired locally.
|
|
159
|
+
*/
|
|
160
|
+
current(serverNowSeconds: number): PairedRequestGrant;
|
|
161
|
+
/** release forgets the pairing, as logout or a lost key requires. */
|
|
162
|
+
release(): void;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* assertGrantMatches is the local pairing check of source §1.4: the result must
|
|
166
|
+
* carry this key's RFC 7638 thumbprint and the exact bounds and owner the
|
|
167
|
+
* client enrolled. A mismatch is a refusal, never a renegotiated grant.
|
|
168
|
+
*/
|
|
169
|
+
export declare function assertGrantMatches(grant: RequestGrantResult, key: RequestSigningKey, enrollment: RequestGrantEnrollment): void;
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
// Mode C request-signing key and request grant (source §1.4, §2.2, §2.5).
|
|
2
|
+
//
|
|
3
|
+
// The private key is a non-extractable P-256 WebCrypto key held only in this
|
|
4
|
+
// package's memory; only its public kty/crv/x/y JWK is ever enrolled. The
|
|
5
|
+
// protected grant result is retained beside the exact key that produced it: a
|
|
6
|
+
// result paired with another key, an altered thumbprint or a lost key all
|
|
7
|
+
// refuse proof generation locally and require explicit re-enrollment with
|
|
8
|
+
// fresh passkey UV. Nothing here reads or writes browser storage, and nothing
|
|
9
|
+
// here re-enrolls on refresh, reload or login.
|
|
10
|
+
import { ClientFlowError, base64url, canonicalDecimal, integer, opaqueId, record } from "./auth.js";
|
|
11
|
+
import { canonicalJSONBytes } from "./canonicalize.js";
|
|
12
|
+
/** MaxRequestGrantRecipients is the source §2.8 allowlist bound. */
|
|
13
|
+
export const MaxRequestGrantRecipients = 10;
|
|
14
|
+
/** MaxRequestGrantLifetimeSeconds is the source §2.5 ceiling of fifteen minutes. */
|
|
15
|
+
export const MaxRequestGrantLifetimeSeconds = 15 * 60;
|
|
16
|
+
const maxUint256 = (1n << 256n) - 1n;
|
|
17
|
+
const coordinate = /^[A-Za-z0-9_-]{43}$/;
|
|
18
|
+
const grantToken = /^[A-Za-z0-9_-]{43}$/;
|
|
19
|
+
const grantNetwork = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
20
|
+
const grantRecipient = /^[\x21-\x7e]{1,256}$/;
|
|
21
|
+
/**
|
|
22
|
+
* generateRequestSigningKey creates the non-extractable P-256 key of source
|
|
23
|
+
* §2.5. It refuses a runtime that returns an extractable private key or a
|
|
24
|
+
* public JWK carrying any private member, so a weakened WebCrypto cannot
|
|
25
|
+
* silently become custody of a user key.
|
|
26
|
+
*/
|
|
27
|
+
export async function generateRequestSigningKey(crypto) {
|
|
28
|
+
if (!crypto?.subtle || typeof crypto.subtle.generateKey !== "function" || typeof crypto.getRandomValues !== "function") {
|
|
29
|
+
throw new ClientFlowError("runtime_capability_unavailable");
|
|
30
|
+
}
|
|
31
|
+
const algorithm = { name: "ECDSA", namedCurve: "P-256" };
|
|
32
|
+
const pair = (await crypto.subtle.generateKey(algorithm, false, ["sign", "verify"]));
|
|
33
|
+
if (!pair?.privateKey || !pair.publicKey || pair.privateKey.extractable !== false)
|
|
34
|
+
throw new ClientFlowError("request_key_extractable");
|
|
35
|
+
const exported = await crypto.subtle.exportKey("jwk", pair.publicKey);
|
|
36
|
+
const publicJwk = exactPublicJwk(exported);
|
|
37
|
+
return {
|
|
38
|
+
handle: base64url(crypto.getRandomValues(new Uint8Array(16))),
|
|
39
|
+
publicJwk,
|
|
40
|
+
thumbprint: await jwkThumbprint(crypto, publicJwk),
|
|
41
|
+
privateKey: pair.privateKey,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* jwkThumbprint is the RFC 7638 SHA-256 thumbprint in unpadded base64url over
|
|
46
|
+
* exactly the required EC members. The canonicalizer already orders them
|
|
47
|
+
* lexicographically, which is the order RFC 7638 prescribes.
|
|
48
|
+
*/
|
|
49
|
+
export async function jwkThumbprint(crypto, key) {
|
|
50
|
+
if (!crypto?.subtle || typeof crypto.subtle.digest !== "function")
|
|
51
|
+
throw new ClientFlowError("runtime_capability_unavailable");
|
|
52
|
+
const canonical = canonicalJSONBytes({ crv: key.crv, kty: key.kty, x: key.x, y: key.y });
|
|
53
|
+
return base64url(new Uint8Array(await crypto.subtle.digest("SHA-256", canonical)));
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* buildRequestGrantEnrollment returns the closed `session.authorize_requests`
|
|
57
|
+
* OperationCreate. Every bound is validated locally first, so an empty or
|
|
58
|
+
* over-length allowlist, a duplicate or wildcard recipient and a noncanonical
|
|
59
|
+
* or out-of-range ceiling never leave the client.
|
|
60
|
+
*/
|
|
61
|
+
export function buildRequestGrantEnrollment(key, enrollment) {
|
|
62
|
+
const declared = record(enrollment, ["ownerId", "servicePrincipalId", "walletId", "network", "recipientAllowlist", "maxAmountRaw", "maxFeeRaw"]);
|
|
63
|
+
if (!opaqueId(declared.ownerId) || !opaqueId(declared.servicePrincipalId) || !opaqueId(declared.walletId) ||
|
|
64
|
+
typeof declared.network !== "string" || !grantNetwork.test(declared.network) ||
|
|
65
|
+
!rawQuantity(declared.maxAmountRaw, true) || !rawQuantity(declared.maxFeeRaw, false)) {
|
|
66
|
+
throw new ClientFlowError("invalid_request_grant_enrollment");
|
|
67
|
+
}
|
|
68
|
+
const allowlist = exactAllowlist(declared.recipientAllowlist, "invalid_request_grant_enrollment");
|
|
69
|
+
return {
|
|
70
|
+
kind: "session.authorize_requests",
|
|
71
|
+
owner_type: "account",
|
|
72
|
+
owner_id: declared.ownerId,
|
|
73
|
+
payload: {
|
|
74
|
+
public_jwk: { kty: key.publicJwk.kty, crv: key.publicJwk.crv, x: key.publicJwk.x, y: key.publicJwk.y },
|
|
75
|
+
service_principal_id: declared.servicePrincipalId,
|
|
76
|
+
wallet_id: declared.walletId,
|
|
77
|
+
network: declared.network,
|
|
78
|
+
recipient_allowlist: allowlist,
|
|
79
|
+
max_amount_raw: declared.maxAmountRaw,
|
|
80
|
+
max_fee_raw: declared.maxFeeRaw,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* enrollRequestGrant performs one explicit enrollment: it never runs from a
|
|
86
|
+
* refresh, reload or login callback, and it returns the protected grant result
|
|
87
|
+
* paired with the key that will sign under it. The caller must have obtained
|
|
88
|
+
* the hosted passkey confirmation through `confirm`; no proof is generated here
|
|
89
|
+
* and no mediated request is sent.
|
|
90
|
+
*/
|
|
91
|
+
export async function enrollRequestGrant(host, key, enrollment, keys) {
|
|
92
|
+
if (!opaqueId(keys?.create) || !opaqueId(keys?.execute))
|
|
93
|
+
throw new ClientFlowError("invalid_idempotency_key");
|
|
94
|
+
const create = buildRequestGrantEnrollment(key, enrollment);
|
|
95
|
+
const admitted = await host.create(create, keys.create);
|
|
96
|
+
if (!opaqueId(admitted?.operationId))
|
|
97
|
+
throw new ClientFlowError("invalid_server_response");
|
|
98
|
+
const confirmed = await host.confirm(admitted.operationId);
|
|
99
|
+
if (confirmed?.operationId !== admitted.operationId || !canonicalDecimal(confirmed.version))
|
|
100
|
+
throw new ClientFlowError("invalid_server_response");
|
|
101
|
+
await host.execute(admitted.operationId, confirmed.version, keys.execute);
|
|
102
|
+
return parseRequestGrantResultEnvelope(await host.result(admitted.operationId), key, enrollment).result;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* parseRequestGrantResultEnvelope accepts the `{request_id,result}` protected
|
|
106
|
+
* result envelope and requires the grant to belong to this key and to the
|
|
107
|
+
* enrollment that was submitted.
|
|
108
|
+
*/
|
|
109
|
+
export function parseRequestGrantResultEnvelope(payload, key, enrollment) {
|
|
110
|
+
const root = record(payload, ["request_id", "result"]);
|
|
111
|
+
if (!opaqueId(root.request_id))
|
|
112
|
+
throw new ClientFlowError("invalid_server_response");
|
|
113
|
+
const result = parseRequestGrantResult(root.result);
|
|
114
|
+
assertGrantMatches(result, key, enrollment);
|
|
115
|
+
return { requestId: root.request_id, result };
|
|
116
|
+
}
|
|
117
|
+
/** parseRequestGrantResult accepts exactly the closed grant result of source §2.2. */
|
|
118
|
+
export function parseRequestGrantResult(payload) {
|
|
119
|
+
const root = record(payload, ["request_grant_id", "public_key_thumbprint", "server_time", "expires_at", "binding", "bounds"]);
|
|
120
|
+
if (typeof root.request_grant_id !== "string" || !grantToken.test(root.request_grant_id) ||
|
|
121
|
+
typeof root.public_key_thumbprint !== "string" || !grantToken.test(root.public_key_thumbprint) ||
|
|
122
|
+
!integer(root.server_time) || !integer(root.expires_at) || root.expires_at <= root.server_time ||
|
|
123
|
+
root.expires_at - root.server_time > MaxRequestGrantLifetimeSeconds) {
|
|
124
|
+
throw new ClientFlowError("invalid_request_grant_result");
|
|
125
|
+
}
|
|
126
|
+
const raw = record(root.binding, ["tenant_id", "application_id", "environment_id", "client_id", "account_id", "session_id", "service_principal_id", "wallet_id", "wallet_generation", "mapping_version"]);
|
|
127
|
+
const binding = {
|
|
128
|
+
tenantId: boundIdentifier(raw.tenant_id), applicationId: boundIdentifier(raw.application_id), environmentId: boundIdentifier(raw.environment_id),
|
|
129
|
+
clientId: boundIdentifier(raw.client_id), accountId: boundIdentifier(raw.account_id), sessionId: boundIdentifier(raw.session_id),
|
|
130
|
+
servicePrincipalId: boundIdentifier(raw.service_principal_id), walletId: boundIdentifier(raw.wallet_id),
|
|
131
|
+
walletGeneration: boundVersion(raw.wallet_generation), mappingVersion: boundVersion(raw.mapping_version),
|
|
132
|
+
};
|
|
133
|
+
const bounds = record(root.bounds, ["network", "recipient_allowlist", "max_amount_raw", "max_fee_raw"]);
|
|
134
|
+
if (typeof bounds.network !== "string" || !grantNetwork.test(bounds.network) ||
|
|
135
|
+
!rawQuantity(bounds.max_amount_raw, true) || !rawQuantity(bounds.max_fee_raw, false)) {
|
|
136
|
+
throw new ClientFlowError("invalid_request_grant_result");
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
requestGrantId: root.request_grant_id,
|
|
140
|
+
publicKeyThumbprint: root.public_key_thumbprint,
|
|
141
|
+
serverTime: root.server_time,
|
|
142
|
+
expiresAt: root.expires_at,
|
|
143
|
+
binding,
|
|
144
|
+
bounds: {
|
|
145
|
+
network: bounds.network, recipientAllowlist: exactAllowlist(bounds.recipient_allowlist, "invalid_request_grant_result"),
|
|
146
|
+
maxAmountRaw: bounds.max_amount_raw, maxFeeRaw: bounds.max_fee_raw,
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* RequestGrantStore keeps one grant result beside the exact key that owns it,
|
|
152
|
+
* in memory only. It never touches browser storage, never renews a grant and
|
|
153
|
+
* never re-enrolls: a released, absent or expired pairing reports
|
|
154
|
+
* `request_grant_reenrollment_required` so the caller performs an explicit
|
|
155
|
+
* fresh-UV enrollment instead.
|
|
156
|
+
*/
|
|
157
|
+
export class RequestGrantStore {
|
|
158
|
+
#paired;
|
|
159
|
+
/** pair refuses any grant result that is not this key's own enrolled grant. */
|
|
160
|
+
pair(key, grant, enrollment) {
|
|
161
|
+
assertGrantMatches(grant, key, enrollment);
|
|
162
|
+
const paired = Object.freeze({ grant, key });
|
|
163
|
+
this.#paired = paired;
|
|
164
|
+
return paired;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* current returns the live pairing. An absent pairing is a lost key, and an
|
|
168
|
+
* expired grant is over: neither is repaired locally.
|
|
169
|
+
*/
|
|
170
|
+
current(serverNowSeconds) {
|
|
171
|
+
const paired = this.#paired;
|
|
172
|
+
if (!paired)
|
|
173
|
+
throw new ClientFlowError("request_grant_reenrollment_required");
|
|
174
|
+
if (!integer(serverNowSeconds) || serverNowSeconds >= paired.grant.expiresAt)
|
|
175
|
+
throw new ClientFlowError("request_grant_reenrollment_required");
|
|
176
|
+
return paired;
|
|
177
|
+
}
|
|
178
|
+
/** release forgets the pairing, as logout or a lost key requires. */
|
|
179
|
+
release() {
|
|
180
|
+
this.#paired = undefined;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* assertGrantMatches is the local pairing check of source §1.4: the result must
|
|
185
|
+
* carry this key's RFC 7638 thumbprint and the exact bounds and owner the
|
|
186
|
+
* client enrolled. A mismatch is a refusal, never a renegotiated grant.
|
|
187
|
+
*/
|
|
188
|
+
export function assertGrantMatches(grant, key, enrollment) {
|
|
189
|
+
if (grant.publicKeyThumbprint !== key.thumbprint)
|
|
190
|
+
throw new ClientFlowError("request_grant_mismatch");
|
|
191
|
+
if (grant.binding.accountId !== enrollment.ownerId || grant.binding.servicePrincipalId !== enrollment.servicePrincipalId ||
|
|
192
|
+
grant.binding.walletId !== enrollment.walletId || grant.bounds.network !== enrollment.network ||
|
|
193
|
+
grant.bounds.maxAmountRaw !== enrollment.maxAmountRaw || grant.bounds.maxFeeRaw !== enrollment.maxFeeRaw) {
|
|
194
|
+
throw new ClientFlowError("request_grant_mismatch");
|
|
195
|
+
}
|
|
196
|
+
if (grant.bounds.recipientAllowlist.length !== enrollment.recipientAllowlist.length ||
|
|
197
|
+
grant.bounds.recipientAllowlist.some((recipient, index) => recipient !== enrollment.recipientAllowlist[index])) {
|
|
198
|
+
throw new ClientFlowError("request_grant_mismatch");
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function exactPublicJwk(value) {
|
|
202
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
203
|
+
throw new ClientFlowError("invalid_request_key");
|
|
204
|
+
const jwk = value;
|
|
205
|
+
// A WebCrypto export carries advertised usage metadata; every other member,
|
|
206
|
+
// and any private member, refuses the key outright.
|
|
207
|
+
const allowed = new Set(["kty", "crv", "x", "y", "ext", "key_ops"]);
|
|
208
|
+
if (Object.keys(jwk).some((member) => !allowed.has(member)))
|
|
209
|
+
throw new ClientFlowError("invalid_request_key");
|
|
210
|
+
if (jwk.kty !== "EC" || jwk.crv !== "P-256" || typeof jwk.x !== "string" || typeof jwk.y !== "string" ||
|
|
211
|
+
!coordinate.test(jwk.x) || !coordinate.test(jwk.y)) {
|
|
212
|
+
throw new ClientFlowError("invalid_request_key");
|
|
213
|
+
}
|
|
214
|
+
return Object.freeze({ kty: "EC", crv: "P-256", x: jwk.x, y: jwk.y });
|
|
215
|
+
}
|
|
216
|
+
function exactAllowlist(value, code) {
|
|
217
|
+
if (!Array.isArray(value) || value.length < 1 || value.length > MaxRequestGrantRecipients)
|
|
218
|
+
throw new ClientFlowError(code);
|
|
219
|
+
const allowlist = [];
|
|
220
|
+
for (const recipient of value) {
|
|
221
|
+
if (typeof recipient !== "string" || !grantRecipient.test(recipient) || allowlist.includes(recipient))
|
|
222
|
+
throw new ClientFlowError(code);
|
|
223
|
+
allowlist.push(recipient);
|
|
224
|
+
}
|
|
225
|
+
return Object.freeze(allowlist);
|
|
226
|
+
}
|
|
227
|
+
function rawQuantity(value, positive) {
|
|
228
|
+
if (!canonicalDecimal(value))
|
|
229
|
+
return false;
|
|
230
|
+
const quantity = BigInt(value);
|
|
231
|
+
return quantity <= maxUint256 && (!positive || quantity > 0n);
|
|
232
|
+
}
|
|
233
|
+
function boundIdentifier(value) {
|
|
234
|
+
if (!opaqueId(value))
|
|
235
|
+
throw new ClientFlowError("invalid_request_grant_result");
|
|
236
|
+
return value;
|
|
237
|
+
}
|
|
238
|
+
/** boundVersion accepts the canonical decimal versions of source §2.2; zero is never a live version. */
|
|
239
|
+
function boundVersion(value) {
|
|
240
|
+
if (!canonicalDecimal(value) || value === "0")
|
|
241
|
+
throw new ClientFlowError("invalid_request_grant_result");
|
|
242
|
+
return value;
|
|
243
|
+
}
|
package/dist/sends.d.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { type PageOptions } from './pages.ts';
|
|
2
|
+
import { type ReplaceAction, type UserWalletEffectState, type UserWalletState } from './walletOperations.ts';
|
|
3
|
+
import type { WalletEvent } from './walletEvents.ts';
|
|
4
|
+
/** The recorded final fate of a send's transfer. It never changes. */
|
|
5
|
+
export interface TransferOutcome {
|
|
6
|
+
readonly state: 'confirmed' | 'failed' | 'expired';
|
|
7
|
+
/** When the provider recorded the own-wallet system's report. */
|
|
8
|
+
readonly observedAt: string;
|
|
9
|
+
readonly transactionHash?: string;
|
|
10
|
+
readonly failureReason?: string;
|
|
11
|
+
/** The account's own send whose transaction the network included in this transfer's place; only with failureReason nonce_used. */
|
|
12
|
+
readonly replacedByOperationId?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* One send of the history. network, asset, recipient and amountRaw are absent
|
|
16
|
+
* once the operation's material retention expired (materialPurged): the send
|
|
17
|
+
* stays listed with its state and its transfer, and without details.
|
|
18
|
+
*/
|
|
19
|
+
export interface ListedSend {
|
|
20
|
+
readonly operationId: string;
|
|
21
|
+
readonly state: UserWalletState;
|
|
22
|
+
readonly effectState: UserWalletEffectState;
|
|
23
|
+
/**
|
|
24
|
+
* session_authorized: the account's session authorized a send from a linked
|
|
25
|
+
* wallet (no passkey, since 14 October 2026); user_confirmed: the passkey
|
|
26
|
+
* confirmed it; user_request_signed: a service sent it on the user's signed
|
|
27
|
+
* request; "" while it waits for a passkey.
|
|
28
|
+
*/
|
|
29
|
+
readonly authorizationBasis: '' | 'user_confirmed' | 'session_authorized' | 'user_request_signed';
|
|
30
|
+
readonly createdAt: string;
|
|
31
|
+
readonly expiresAt: string;
|
|
32
|
+
readonly materialPurged: boolean;
|
|
33
|
+
/** Present for a send from a linked wallet. */
|
|
34
|
+
readonly linkedWalletId?: string;
|
|
35
|
+
/** Present for a send from a wallet bound in the provider. */
|
|
36
|
+
readonly walletId?: string;
|
|
37
|
+
readonly network?: string;
|
|
38
|
+
readonly asset?: string;
|
|
39
|
+
readonly recipient?: string;
|
|
40
|
+
readonly amountRaw?: string;
|
|
41
|
+
/** The sending address, once the own-wallet system prepared the transfer of a linked wallet. */
|
|
42
|
+
readonly address?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Present for a send that replaces an earlier one the network had not
|
|
45
|
+
* included: that send's operation, and "speed_up" or "cancel". network,
|
|
46
|
+
* asset, recipient and amountRaw are then what the replacing transaction
|
|
47
|
+
* itself moves, for a cancel nothing ("0") to the wallet's own address.
|
|
48
|
+
*/
|
|
49
|
+
readonly replacesOperationId?: string;
|
|
50
|
+
readonly replaceAction?: ReplaceAction;
|
|
51
|
+
/** The text the transfer carries to its recipient, on TON. */
|
|
52
|
+
readonly comment?: string;
|
|
53
|
+
/** The network's hash of a send from a linked wallet the own-wallet system accepted. */
|
|
54
|
+
readonly transactionHash?: string;
|
|
55
|
+
/** The final fate of that send's transfer; absent until transferStatus() read a final state. */
|
|
56
|
+
readonly transfer?: TransferOutcome;
|
|
57
|
+
}
|
|
58
|
+
export interface SendPage {
|
|
59
|
+
readonly requestId: string;
|
|
60
|
+
readonly items: readonly ListedSend[];
|
|
61
|
+
readonly hasMore: boolean;
|
|
62
|
+
readonly nextCursor?: string;
|
|
63
|
+
}
|
|
64
|
+
/** The path of one page of the send history. */
|
|
65
|
+
export declare function sendsPath(options?: PageOptions): {
|
|
66
|
+
path: string;
|
|
67
|
+
signal?: AbortSignal;
|
|
68
|
+
};
|
|
69
|
+
/** Parses one closed page of the send history. */
|
|
70
|
+
export declare function parseSendPage(payload: unknown): SendPage;
|
|
71
|
+
/**
|
|
72
|
+
* Says whether a wallet event is the network's record of the transaction with
|
|
73
|
+
* this hash on this network: true or false where an event's sourceReference
|
|
74
|
+
* is the transaction hash, and undefined on every other network, where
|
|
75
|
+
* nothing ties an event to a hash yet. On the EVM networks and TRON the two
|
|
76
|
+
* are one hexadecimal hash and differ at most in a 0x prefix and letter case.
|
|
77
|
+
* On Solana both are the transaction's signature in base58, where case is
|
|
78
|
+
* part of the value. On TON the event names the transaction the network made
|
|
79
|
+
* and the own-wallet system names the message it sent; the two hashes differ.
|
|
80
|
+
*/
|
|
81
|
+
export declare function eventOfTransaction(event: WalletEvent, networkId: string, transactionHash: string): boolean | undefined;
|
|
82
|
+
/**
|
|
83
|
+
* The wallet events that record one listed send on its network: the
|
|
84
|
+
* withdrawal from the sending wallet and any activity of the same
|
|
85
|
+
* transaction. It returns undefined where events cannot be tied to the send
|
|
86
|
+
* (no hash yet, details purged, or a network without that tie), and an empty
|
|
87
|
+
* list where they can and none of the given events is of this send.
|
|
88
|
+
*/
|
|
89
|
+
export declare function sendEvents(send: ListedSend, events: readonly WalletEvent[]): readonly WalletEvent[] | undefined;
|