@shuttle.technology/authorization 0.0.0-stage → 0.1.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/README.md +708 -2
- package/dist/aesGcmSiv.d.ts +14 -0
- package/dist/aesGcmSiv.js +160 -0
- package/dist/auth.d.ts +68 -0
- package/dist/auth.js +215 -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 +264 -0
- package/dist/client.js +754 -0
- package/dist/deviceKey.d.ts +13 -0
- package/dist/deviceKey.js +28 -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/linkedChecks.d.ts +17 -0
- package/dist/linkedChecks.js +31 -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 +373 -0
- package/dist/linkedWallets.js +471 -0
- package/dist/mediatedOperation.d.ts +27 -0
- package/dist/mediatedOperation.js +68 -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/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/slice4.d.ts +173 -0
- package/dist/slice4.js +285 -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/walletDisplayParts.d.ts +15 -0
- package/dist/walletDisplayParts.js +43 -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 +192 -0
- package/dist/walletOperations.js +308 -0
- package/package.json +64 -4
package/dist/modeC.d.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { type PairedRequestGrant } from "./requestGrant.ts";
|
|
2
|
+
export { canonicalJSON, canonicalJSONBytes, sha256HexLower, MaxCanonicalDepth, type CanonicalValue, } from "./canonicalize.ts";
|
|
3
|
+
export { MaxRequestGrantLifetimeSeconds, MaxRequestGrantRecipients, RequestGrantStore, assertGrantMatches, buildRequestGrantEnrollment, enrollRequestGrant, generateRequestSigningKey, jwkThumbprint, parseRequestGrantResult, parseRequestGrantResultEnvelope, type PairedRequestGrant, type RequestGrantBinding, type RequestGrantBounds, type RequestGrantEnrollment, type RequestGrantEnrollmentCreate, type RequestGrantEnrollmentHost, type RequestGrantResult, type RequestPublicJwk, type RequestSigningKey, } from "./requestGrant.ts";
|
|
4
|
+
/** ModeCProofVersion, Audience, Type and Kind are the closed proof constants of source §2.5. */
|
|
5
|
+
export declare const ModeCProofVersion = "1.0";
|
|
6
|
+
export declare const ModeCProofAudience = "authorization-api:mode-c";
|
|
7
|
+
export declare const ModeCProofType = "auth-user-request+jws";
|
|
8
|
+
export declare const ModeCProofKind = "wallet.send";
|
|
9
|
+
/** ModeCProofPayloadMembers is the exact member count of the signed payload. */
|
|
10
|
+
export declare const ModeCProofPayloadMembers = 17;
|
|
11
|
+
/** MaxModeCProofLifetimeSeconds bounds exp-iat. No grace extends it. */
|
|
12
|
+
export declare const MaxModeCProofLifetimeSeconds = 60;
|
|
13
|
+
/** ModeCJtiBytes is the 256-bit jti entropy that is also the Idempotency-Key. */
|
|
14
|
+
export declare const ModeCJtiBytes = 32;
|
|
15
|
+
/**
|
|
16
|
+
* MediatedTransferIntent is everything the user chooses. The owner, wallet and
|
|
17
|
+
* network are never caller-supplied: they come from the grant binding, so a
|
|
18
|
+
* backend cannot redirect the transfer by naming another wallet.
|
|
19
|
+
*/
|
|
20
|
+
export interface MediatedTransferIntent {
|
|
21
|
+
readonly recipient: string;
|
|
22
|
+
readonly amountRaw: string;
|
|
23
|
+
readonly maxFeeRaw: string;
|
|
24
|
+
}
|
|
25
|
+
/** MediatedTransferDisplay is the exact typed intent that must be rendered before signing. */
|
|
26
|
+
export interface MediatedTransferDisplay {
|
|
27
|
+
readonly kind: "wallet.send";
|
|
28
|
+
readonly ownerId: string;
|
|
29
|
+
readonly walletId: string;
|
|
30
|
+
readonly network: string;
|
|
31
|
+
readonly asset: "native";
|
|
32
|
+
readonly recipient: string;
|
|
33
|
+
readonly amountRaw: string;
|
|
34
|
+
readonly maxFeeRaw: string;
|
|
35
|
+
}
|
|
36
|
+
/** MediatedOperationCreate is the closed mediated OperationCreate of source §2.2. */
|
|
37
|
+
export interface MediatedOperationCreate {
|
|
38
|
+
readonly kind: "wallet.send";
|
|
39
|
+
readonly owner_type: "account";
|
|
40
|
+
readonly owner_id: string;
|
|
41
|
+
readonly wallet_id: string;
|
|
42
|
+
readonly payload: {
|
|
43
|
+
readonly network: string;
|
|
44
|
+
readonly asset: "native";
|
|
45
|
+
readonly recipient: string;
|
|
46
|
+
readonly amount_raw: string;
|
|
47
|
+
readonly max_fee_raw: string;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** PreparedMediatedTransfer is one displayed intent and the exact body it will sign. */
|
|
51
|
+
export interface PreparedMediatedTransfer {
|
|
52
|
+
readonly display: MediatedTransferDisplay;
|
|
53
|
+
readonly operation: MediatedOperationCreate;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* SignedMediatedRequest is everything the caller hands to its own customer
|
|
57
|
+
* backend. `idempotencyKey` is the payload `jti` itself, so no other request
|
|
58
|
+
* identity can be bound to this proof.
|
|
59
|
+
*/
|
|
60
|
+
export interface SignedMediatedRequest {
|
|
61
|
+
readonly display: MediatedTransferDisplay;
|
|
62
|
+
readonly operation: MediatedOperationCreate;
|
|
63
|
+
readonly userProof: string;
|
|
64
|
+
readonly idempotencyKey: string;
|
|
65
|
+
readonly jti: string;
|
|
66
|
+
readonly requestHash: string;
|
|
67
|
+
readonly issuedAt: number;
|
|
68
|
+
readonly expiresAt: number;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* prepareMediatedTransfer derives the displayed intent and the exact closed
|
|
72
|
+
* OperationCreate from the paired grant. It refuses locally before any request
|
|
73
|
+
* or signature: a recipient outside the frozen allowlist, a noncanonical or
|
|
74
|
+
* over-ceiling quantity and a zero amount all stop here.
|
|
75
|
+
*/
|
|
76
|
+
export declare function prepareMediatedTransfer(paired: PairedRequestGrant, intent: MediatedTransferIntent): PreparedMediatedTransfer;
|
|
77
|
+
/**
|
|
78
|
+
* canonicalMediatedRequest is the RFC 8785 serialization the request hash
|
|
79
|
+
* covers. It is produced from the SDK's own closed body, never from transport
|
|
80
|
+
* bytes.
|
|
81
|
+
*/
|
|
82
|
+
export declare function canonicalMediatedRequest(operation: MediatedOperationCreate): string;
|
|
83
|
+
/** mediatedRequestHash is the lowercase hex SHA-256 of the canonical request bytes. */
|
|
84
|
+
export declare function mediatedRequestHash(crypto: Crypto, operation: MediatedOperationCreate): Promise<string>;
|
|
85
|
+
/**
|
|
86
|
+
* modeCProtectedHeader is the closed protected header of source §2.5 in its
|
|
87
|
+
* exact member order. The header is not canonicalized: `alg`, `typ` and `kid`
|
|
88
|
+
* are emitted in that order with no whitespace, which is the byte sequence the
|
|
89
|
+
* server's golden vector pins.
|
|
90
|
+
*/
|
|
91
|
+
export declare function modeCProtectedHeader(requestGrantId: string): string;
|
|
92
|
+
/**
|
|
93
|
+
* ModeCServerClock carries the authenticated server time of source §2.5. It is
|
|
94
|
+
* seeded from the grant result's `server_time` and refreshed only from a
|
|
95
|
+
* current authenticated `/v1/me` response; it installs no local clock override
|
|
96
|
+
* and no tolerance.
|
|
97
|
+
*/
|
|
98
|
+
export declare class ModeCServerClock {
|
|
99
|
+
#private;
|
|
100
|
+
constructor(serverTimeSeconds: number, localNowMs: number);
|
|
101
|
+
/** adopt accepts one authenticated integer NumericDate reading. */
|
|
102
|
+
adopt(serverTimeSeconds: number, localNowMs: number): void;
|
|
103
|
+
/** nowSeconds projects the adopted server reading onto the current local instant. */
|
|
104
|
+
nowSeconds(localNowMs: number): number;
|
|
105
|
+
}
|
|
106
|
+
/** ModeCSigningRequest is the closed input of `signMediatedRequest`. */
|
|
107
|
+
export interface ModeCSigningRequest {
|
|
108
|
+
readonly grant: PairedRequestGrant;
|
|
109
|
+
readonly prepared: PreparedMediatedTransfer;
|
|
110
|
+
readonly crypto: Crypto;
|
|
111
|
+
readonly serverNowSeconds: number;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* signMediatedRequest produces the compact ES256 proof over the exact
|
|
115
|
+
* displayed intent.
|
|
116
|
+
*
|
|
117
|
+
* The order is fixed so that every refusal happens before the signing key is
|
|
118
|
+
* touched: the closed input is checked, the displayed intent is independently
|
|
119
|
+
* rederived from the grant and compared, the grant window is checked, and only
|
|
120
|
+
* then are the digest, the 256-bit jti and the signature produced. A caller
|
|
121
|
+
* that hands this function a string, a blob or any backend-supplied payload is
|
|
122
|
+
* refused with `opaque_signing_input_refused` and no crypto call occurs.
|
|
123
|
+
*/
|
|
124
|
+
export declare function signMediatedRequest(request: ModeCSigningRequest): Promise<SignedMediatedRequest>;
|
|
125
|
+
/**
|
|
126
|
+
* assertDisplayedIntent is the display-to-intent consistency check of source
|
|
127
|
+
* §1.4. The operation is rederived from the grant binding and the displayed
|
|
128
|
+
* recipient, amount and fee, so a display field changed after preparation and
|
|
129
|
+
* an operation field changed after display are both refused before signing.
|
|
130
|
+
*/
|
|
131
|
+
export declare function assertDisplayedIntent(grant: PairedRequestGrant, prepared: PreparedMediatedTransfer): void;
|
package/dist/modeC.js
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
// Mode C typed transfer intent and compact ES256 request proof (source §1.4,
|
|
2
|
+
// §2.2, §2.5).
|
|
3
|
+
//
|
|
4
|
+
// This module owns exactly two client responsibilities: display the typed
|
|
5
|
+
// native wallet.send intent, and sign the canonical bytes of that displayed
|
|
6
|
+
// intent with the enrolled non-extractable key. It performs no HTTP and it
|
|
7
|
+
// exports no forward, approve, refuse or backend-signing helper: forwarding and
|
|
8
|
+
// refusal belong to the customer backend, and operation status and result reads
|
|
9
|
+
// use the generic helpers in `client.ts` (source §1.4, AUTH-009-A05).
|
|
10
|
+
//
|
|
11
|
+
// Every locally detectable fault is refused before any crypto call, so an
|
|
12
|
+
// arbitrary or backend-supplied blob never reaches the signing key.
|
|
13
|
+
import { ClientFlowError, base64url, canonicalDecimal, integer } from "./auth.js";
|
|
14
|
+
import { canonicalJSON, canonicalJSONBytes, sha256HexLower } from "./canonicalize.js";
|
|
15
|
+
import {} from "./requestGrant.js";
|
|
16
|
+
export { canonicalJSON, canonicalJSONBytes, sha256HexLower, MaxCanonicalDepth, } from "./canonicalize.js";
|
|
17
|
+
export { MaxRequestGrantLifetimeSeconds, MaxRequestGrantRecipients, RequestGrantStore, assertGrantMatches, buildRequestGrantEnrollment, enrollRequestGrant, generateRequestSigningKey, jwkThumbprint, parseRequestGrantResult, parseRequestGrantResultEnvelope, } from "./requestGrant.js";
|
|
18
|
+
/** ModeCProofVersion, Audience, Type and Kind are the closed proof constants of source §2.5. */
|
|
19
|
+
export const ModeCProofVersion = "1.0";
|
|
20
|
+
export const ModeCProofAudience = "authorization-api:mode-c";
|
|
21
|
+
export const ModeCProofType = "auth-user-request+jws";
|
|
22
|
+
export const ModeCProofKind = "wallet.send";
|
|
23
|
+
/** ModeCProofPayloadMembers is the exact member count of the signed payload. */
|
|
24
|
+
export const ModeCProofPayloadMembers = 17;
|
|
25
|
+
/** MaxModeCProofLifetimeSeconds bounds exp-iat. No grace extends it. */
|
|
26
|
+
export const MaxModeCProofLifetimeSeconds = 60;
|
|
27
|
+
/** ModeCJtiBytes is the 256-bit jti entropy that is also the Idempotency-Key. */
|
|
28
|
+
export const ModeCJtiBytes = 32;
|
|
29
|
+
const maxUint256 = (1n << 256n) - 1n;
|
|
30
|
+
const printableRecipient = /^[\x21-\x7e]{1,256}$/;
|
|
31
|
+
/**
|
|
32
|
+
* prepareMediatedTransfer derives the displayed intent and the exact closed
|
|
33
|
+
* OperationCreate from the paired grant. It refuses locally before any request
|
|
34
|
+
* or signature: a recipient outside the frozen allowlist, a noncanonical or
|
|
35
|
+
* over-ceiling quantity and a zero amount all stop here.
|
|
36
|
+
*/
|
|
37
|
+
export function prepareMediatedTransfer(paired, intent) {
|
|
38
|
+
const declared = closed(intent, ["recipient", "amountRaw", "maxFeeRaw"]);
|
|
39
|
+
const recipient = declared.recipient;
|
|
40
|
+
const amountRaw = declared.amountRaw;
|
|
41
|
+
const maxFeeRaw = declared.maxFeeRaw;
|
|
42
|
+
if (typeof recipient !== "string" || !printableRecipient.test(recipient) || !paired.grant.bounds.recipientAllowlist.includes(recipient)) {
|
|
43
|
+
throw new ClientFlowError("invalid_transfer_intent");
|
|
44
|
+
}
|
|
45
|
+
if (!rawQuantity(amountRaw, true) || !rawQuantity(maxFeeRaw, false) ||
|
|
46
|
+
BigInt(amountRaw) > BigInt(paired.grant.bounds.maxAmountRaw) || BigInt(maxFeeRaw) > BigInt(paired.grant.bounds.maxFeeRaw)) {
|
|
47
|
+
throw new ClientFlowError("invalid_transfer_intent");
|
|
48
|
+
}
|
|
49
|
+
const binding = paired.grant.binding;
|
|
50
|
+
const network = paired.grant.bounds.network;
|
|
51
|
+
return Object.freeze({
|
|
52
|
+
display: Object.freeze({
|
|
53
|
+
kind: "wallet.send", ownerId: binding.accountId, walletId: binding.walletId, network,
|
|
54
|
+
asset: "native", recipient, amountRaw, maxFeeRaw,
|
|
55
|
+
}),
|
|
56
|
+
operation: Object.freeze({
|
|
57
|
+
kind: "wallet.send", owner_type: "account", owner_id: binding.accountId, wallet_id: binding.walletId,
|
|
58
|
+
payload: Object.freeze({ network, asset: "native", recipient, amount_raw: amountRaw, max_fee_raw: maxFeeRaw }),
|
|
59
|
+
}),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* canonicalMediatedRequest is the RFC 8785 serialization the request hash
|
|
64
|
+
* covers. It is produced from the SDK's own closed body, never from transport
|
|
65
|
+
* bytes.
|
|
66
|
+
*/
|
|
67
|
+
export function canonicalMediatedRequest(operation) {
|
|
68
|
+
const closedOperation = closed(operation, ["kind", "owner_type", "owner_id", "wallet_id", "payload"]);
|
|
69
|
+
const payload = closed(closedOperation.payload, ["network", "asset", "recipient", "amount_raw", "max_fee_raw"]);
|
|
70
|
+
if (closedOperation.kind !== ModeCProofKind || closedOperation.owner_type !== "account" || payload.asset !== "native") {
|
|
71
|
+
throw new ClientFlowError("invalid_transfer_intent");
|
|
72
|
+
}
|
|
73
|
+
return canonicalJSON({
|
|
74
|
+
kind: closedOperation.kind,
|
|
75
|
+
owner_type: closedOperation.owner_type,
|
|
76
|
+
owner_id: closedOperation.owner_id,
|
|
77
|
+
wallet_id: closedOperation.wallet_id,
|
|
78
|
+
payload: {
|
|
79
|
+
network: payload.network,
|
|
80
|
+
asset: payload.asset,
|
|
81
|
+
recipient: payload.recipient,
|
|
82
|
+
amount_raw: payload.amount_raw,
|
|
83
|
+
max_fee_raw: payload.max_fee_raw,
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
/** mediatedRequestHash is the lowercase hex SHA-256 of the canonical request bytes. */
|
|
88
|
+
export function mediatedRequestHash(crypto, operation) {
|
|
89
|
+
return sha256HexLower(crypto, new TextEncoder().encode(canonicalMediatedRequest(operation)));
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* modeCProtectedHeader is the closed protected header of source §2.5 in its
|
|
93
|
+
* exact member order. The header is not canonicalized: `alg`, `typ` and `kid`
|
|
94
|
+
* are emitted in that order with no whitespace, which is the byte sequence the
|
|
95
|
+
* server's golden vector pins.
|
|
96
|
+
*/
|
|
97
|
+
export function modeCProtectedHeader(requestGrantId) {
|
|
98
|
+
return `{"alg":"ES256","typ":${JSON.stringify(ModeCProofType)},"kid":${JSON.stringify(requestGrantId)}}`;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* ModeCServerClock carries the authenticated server time of source §2.5. It is
|
|
102
|
+
* seeded from the grant result's `server_time` and refreshed only from a
|
|
103
|
+
* current authenticated `/v1/me` response; it installs no local clock override
|
|
104
|
+
* and no tolerance.
|
|
105
|
+
*/
|
|
106
|
+
export class ModeCServerClock {
|
|
107
|
+
#serverTime;
|
|
108
|
+
#observedMs;
|
|
109
|
+
constructor(serverTimeSeconds, localNowMs) {
|
|
110
|
+
this.#serverTime = 0;
|
|
111
|
+
this.#observedMs = 0;
|
|
112
|
+
this.adopt(serverTimeSeconds, localNowMs);
|
|
113
|
+
}
|
|
114
|
+
/** adopt accepts one authenticated integer NumericDate reading. */
|
|
115
|
+
adopt(serverTimeSeconds, localNowMs) {
|
|
116
|
+
if (!integer(serverTimeSeconds) || serverTimeSeconds <= 0 || !Number.isFinite(localNowMs))
|
|
117
|
+
throw new ClientFlowError("invalid_server_time");
|
|
118
|
+
this.#serverTime = serverTimeSeconds;
|
|
119
|
+
this.#observedMs = localNowMs;
|
|
120
|
+
}
|
|
121
|
+
/** nowSeconds projects the adopted server reading onto the current local instant. */
|
|
122
|
+
nowSeconds(localNowMs) {
|
|
123
|
+
if (!Number.isFinite(localNowMs))
|
|
124
|
+
throw new ClientFlowError("invalid_server_time");
|
|
125
|
+
const elapsed = Math.floor((localNowMs - this.#observedMs) / 1_000);
|
|
126
|
+
const now = this.#serverTime + elapsed;
|
|
127
|
+
if (!integer(now) || now <= 0)
|
|
128
|
+
throw new ClientFlowError("invalid_server_time");
|
|
129
|
+
return now;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* signMediatedRequest produces the compact ES256 proof over the exact
|
|
134
|
+
* displayed intent.
|
|
135
|
+
*
|
|
136
|
+
* The order is fixed so that every refusal happens before the signing key is
|
|
137
|
+
* touched: the closed input is checked, the displayed intent is independently
|
|
138
|
+
* rederived from the grant and compared, the grant window is checked, and only
|
|
139
|
+
* then are the digest, the 256-bit jti and the signature produced. A caller
|
|
140
|
+
* that hands this function a string, a blob or any backend-supplied payload is
|
|
141
|
+
* refused with `opaque_signing_input_refused` and no crypto call occurs.
|
|
142
|
+
*/
|
|
143
|
+
export async function signMediatedRequest(request) {
|
|
144
|
+
const input = closed(request, ["grant", "prepared", "crypto", "serverNowSeconds"]);
|
|
145
|
+
const grant = input.grant;
|
|
146
|
+
const prepared = input.prepared;
|
|
147
|
+
const crypto = input.crypto;
|
|
148
|
+
const serverNowSeconds = input.serverNowSeconds;
|
|
149
|
+
if (!grant?.grant || !grant.key?.privateKey || !prepared?.display || !prepared.operation)
|
|
150
|
+
throw new ClientFlowError("opaque_signing_input_refused");
|
|
151
|
+
if (!crypto?.subtle || typeof crypto.subtle.sign !== "function" || typeof crypto.subtle.digest !== "function" || typeof crypto.getRandomValues !== "function") {
|
|
152
|
+
throw new ClientFlowError("runtime_capability_unavailable");
|
|
153
|
+
}
|
|
154
|
+
assertDisplayedIntent(grant, prepared);
|
|
155
|
+
if (!integer(serverNowSeconds) || serverNowSeconds <= 0 || serverNowSeconds >= grant.grant.expiresAt) {
|
|
156
|
+
throw new ClientFlowError("request_grant_reenrollment_required");
|
|
157
|
+
}
|
|
158
|
+
const issuedAt = serverNowSeconds;
|
|
159
|
+
const expiresAt = Math.min(issuedAt + MaxModeCProofLifetimeSeconds, grant.grant.expiresAt);
|
|
160
|
+
if (expiresAt <= issuedAt || expiresAt - issuedAt > MaxModeCProofLifetimeSeconds)
|
|
161
|
+
throw new ClientFlowError("request_grant_reenrollment_required");
|
|
162
|
+
const requestHash = await mediatedRequestHash(crypto, prepared.operation);
|
|
163
|
+
const jti = base64url(crypto.getRandomValues(new Uint8Array(ModeCJtiBytes)));
|
|
164
|
+
const binding = grant.grant.binding;
|
|
165
|
+
const payload = canonicalJSONBytes({
|
|
166
|
+
version: ModeCProofVersion,
|
|
167
|
+
aud: ModeCProofAudience,
|
|
168
|
+
tenant_id: binding.tenantId,
|
|
169
|
+
application_id: binding.applicationId,
|
|
170
|
+
environment_id: binding.environmentId,
|
|
171
|
+
client_id: binding.clientId,
|
|
172
|
+
account_id: binding.accountId,
|
|
173
|
+
session_id: binding.sessionId,
|
|
174
|
+
service_principal_id: binding.servicePrincipalId,
|
|
175
|
+
wallet_id: binding.walletId,
|
|
176
|
+
wallet_generation: binding.walletGeneration,
|
|
177
|
+
mapping_version: binding.mappingVersion,
|
|
178
|
+
kind: ModeCProofKind,
|
|
179
|
+
request_hash: requestHash,
|
|
180
|
+
jti,
|
|
181
|
+
iat: issuedAt,
|
|
182
|
+
exp: expiresAt,
|
|
183
|
+
});
|
|
184
|
+
const header = new TextEncoder().encode(modeCProtectedHeader(grant.grant.requestGrantId));
|
|
185
|
+
const signingInput = `${base64url(header)}.${base64url(payload)}`;
|
|
186
|
+
const signature = new Uint8Array(await crypto.subtle.sign({ name: "ECDSA", hash: { name: "SHA-256" } }, grant.key.privateKey, new TextEncoder().encode(signingInput)));
|
|
187
|
+
// WebCrypto ECDSA already emits the 64-byte R||S encoding JWS requires; a
|
|
188
|
+
// runtime that returns DER or another length is refused, never reshaped.
|
|
189
|
+
if (signature.length !== 64)
|
|
190
|
+
throw new ClientFlowError("invalid_request_signature");
|
|
191
|
+
return Object.freeze({
|
|
192
|
+
display: prepared.display,
|
|
193
|
+
operation: prepared.operation,
|
|
194
|
+
userProof: `${signingInput}.${base64url(signature)}`,
|
|
195
|
+
idempotencyKey: jti,
|
|
196
|
+
jti,
|
|
197
|
+
requestHash,
|
|
198
|
+
issuedAt,
|
|
199
|
+
expiresAt,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* assertDisplayedIntent is the display-to-intent consistency check of source
|
|
204
|
+
* §1.4. The operation is rederived from the grant binding and the displayed
|
|
205
|
+
* recipient, amount and fee, so a display field changed after preparation and
|
|
206
|
+
* an operation field changed after display are both refused before signing.
|
|
207
|
+
*/
|
|
208
|
+
export function assertDisplayedIntent(grant, prepared) {
|
|
209
|
+
const display = closed(prepared.display, ["kind", "ownerId", "walletId", "network", "asset", "recipient", "amountRaw", "maxFeeRaw"]);
|
|
210
|
+
let rederived;
|
|
211
|
+
try {
|
|
212
|
+
rederived = prepareMediatedTransfer(grant, {
|
|
213
|
+
recipient: display.recipient,
|
|
214
|
+
amountRaw: display.amountRaw,
|
|
215
|
+
maxFeeRaw: display.maxFeeRaw,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
throw new ClientFlowError("display_intent_mismatch");
|
|
220
|
+
}
|
|
221
|
+
const sameDisplay = ["kind", "ownerId", "walletId", "network", "asset", "recipient", "amountRaw", "maxFeeRaw"]
|
|
222
|
+
.every((member) => display[member] === rederived.display[member]);
|
|
223
|
+
if (!sameDisplay || canonicalMediatedRequest(prepared.operation) !== canonicalMediatedRequest(rederived.operation)) {
|
|
224
|
+
throw new ClientFlowError("display_intent_mismatch");
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* closed accepts exactly the named members of a plain object. A string, a
|
|
229
|
+
* binary blob, an array and any extra or missing member are refused, which is
|
|
230
|
+
* how arbitrary backend-supplied bytes are rejected before crypto.
|
|
231
|
+
*/
|
|
232
|
+
function closed(value, required) {
|
|
233
|
+
if (value === null || typeof value !== "object" || Array.isArray(value) || ArrayBuffer.isView(value) || value instanceof ArrayBuffer) {
|
|
234
|
+
throw new ClientFlowError("opaque_signing_input_refused");
|
|
235
|
+
}
|
|
236
|
+
const result = value;
|
|
237
|
+
const allowed = new Set(required);
|
|
238
|
+
if (required.some((member) => !(member in result)) || Object.keys(result).some((member) => !allowed.has(member))) {
|
|
239
|
+
throw new ClientFlowError("opaque_signing_input_refused");
|
|
240
|
+
}
|
|
241
|
+
return result;
|
|
242
|
+
}
|
|
243
|
+
function rawQuantity(value, positive) {
|
|
244
|
+
if (!canonicalDecimal(value))
|
|
245
|
+
return false;
|
|
246
|
+
const quantity = BigInt(value);
|
|
247
|
+
return quantity <= maxUint256 && (!positive || quantity > 0n);
|
|
248
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { type RequestSigningKey } from "./requestGrant.ts";
|
|
2
|
+
import type { DeviceSharePackage } from "./walletOperations.ts";
|
|
3
|
+
export { ModeCServerClock, MaxModeCProofLifetimeSeconds } from "./modeC.ts";
|
|
4
|
+
export { generateRequestSigningKey, type RequestSigningKey } from "./requestGrant.ts";
|
|
5
|
+
/** ModeCLinkedProofVersion is the proof of a transfer from a linked wallet. */
|
|
6
|
+
export declare const ModeCLinkedProofVersion = "2.0";
|
|
7
|
+
/** The assets a linked grant may name. */
|
|
8
|
+
export declare const LinkedModeCAssets: readonly ["native", "usdt", "usdc"];
|
|
9
|
+
export type LinkedModeCAsset = (typeof LinkedModeCAssets)[number];
|
|
10
|
+
export interface LinkedAssetCeiling {
|
|
11
|
+
readonly asset: LinkedModeCAsset;
|
|
12
|
+
/** The most one transfer of the asset may move, in its minimal units. */
|
|
13
|
+
readonly maxAmountRaw: string;
|
|
14
|
+
}
|
|
15
|
+
/** LinkedRequestGrantEnrollment is what the person sets and confirms with the passkey. */
|
|
16
|
+
export interface LinkedRequestGrantEnrollment {
|
|
17
|
+
readonly ownerId: string;
|
|
18
|
+
readonly servicePrincipalId: string;
|
|
19
|
+
readonly linkedWalletId: string;
|
|
20
|
+
/** The network the transfers are made on: the wallet's own, or for an EVM wallet another EVM network. */
|
|
21
|
+
readonly network: string;
|
|
22
|
+
readonly assets: readonly LinkedAssetCeiling[];
|
|
23
|
+
/** Zero to ten recipients; none means any recipient. */
|
|
24
|
+
readonly recipientAllowlist: readonly string[];
|
|
25
|
+
/** The most the network fee may be, in minimal units of the network's own coin. */
|
|
26
|
+
readonly maxFeeRaw: string;
|
|
27
|
+
}
|
|
28
|
+
/** The closed session.authorize_requests OperationCreate of a linked wallet. */
|
|
29
|
+
export interface LinkedRequestGrantEnrollmentCreate {
|
|
30
|
+
readonly kind: "session.authorize_requests";
|
|
31
|
+
readonly owner_type: "account";
|
|
32
|
+
readonly owner_id: string;
|
|
33
|
+
/** Who may read the grant; absent, the account itself. */
|
|
34
|
+
readonly result_recipient?: {
|
|
35
|
+
readonly type: "account" | "service";
|
|
36
|
+
readonly id: string;
|
|
37
|
+
};
|
|
38
|
+
readonly payload: {
|
|
39
|
+
readonly public_jwk: {
|
|
40
|
+
readonly kty: "EC";
|
|
41
|
+
readonly crv: "P-256";
|
|
42
|
+
readonly x: string;
|
|
43
|
+
readonly y: string;
|
|
44
|
+
};
|
|
45
|
+
readonly service_principal_id: string;
|
|
46
|
+
readonly linked_wallet_id: string;
|
|
47
|
+
readonly network: string;
|
|
48
|
+
readonly assets: readonly {
|
|
49
|
+
readonly asset: LinkedModeCAsset;
|
|
50
|
+
readonly max_amount_raw: string;
|
|
51
|
+
}[];
|
|
52
|
+
readonly recipient_allowlist: readonly string[];
|
|
53
|
+
readonly max_fee_raw: string;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/** The enrollment as the person confirms it, read back with the operation. */
|
|
57
|
+
export interface LinkedEnrollmentDisplay {
|
|
58
|
+
kind: "session.authorize_requests";
|
|
59
|
+
ownerId: string;
|
|
60
|
+
requesterId: string;
|
|
61
|
+
servicePrincipalId: string;
|
|
62
|
+
linkedWalletId: string;
|
|
63
|
+
address: string;
|
|
64
|
+
network: string;
|
|
65
|
+
assets: readonly LinkedAssetCeiling[];
|
|
66
|
+
recipientAllowlist: readonly string[];
|
|
67
|
+
maxFeeRaw: string;
|
|
68
|
+
publicKeyThumbprint: string;
|
|
69
|
+
}
|
|
70
|
+
/** The grant the enrollment issued: its identity, binding and bounds. Times are NumericDate seconds. */
|
|
71
|
+
export interface LinkedRequestGrantResult {
|
|
72
|
+
readonly requestGrantId: string;
|
|
73
|
+
readonly publicKeyThumbprint: string;
|
|
74
|
+
readonly serverTime: number;
|
|
75
|
+
readonly expiresAt: number;
|
|
76
|
+
readonly binding: {
|
|
77
|
+
readonly tenantId: string;
|
|
78
|
+
readonly applicationId: string;
|
|
79
|
+
readonly environmentId: string;
|
|
80
|
+
readonly clientId: string;
|
|
81
|
+
readonly accountId: string;
|
|
82
|
+
readonly sessionId: string;
|
|
83
|
+
readonly servicePrincipalId: string;
|
|
84
|
+
readonly linkedWalletId: string;
|
|
85
|
+
readonly bindingRevision: string;
|
|
86
|
+
};
|
|
87
|
+
readonly bounds: {
|
|
88
|
+
readonly network: string;
|
|
89
|
+
readonly recipientAllowlist: readonly string[];
|
|
90
|
+
readonly assets: readonly LinkedAssetCeiling[];
|
|
91
|
+
readonly maxFeeRaw: string;
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/** One linked grant beside the exact key that signs under it. */
|
|
95
|
+
export interface PairedLinkedRequestGrant {
|
|
96
|
+
readonly grant: LinkedRequestGrantResult;
|
|
97
|
+
readonly key: RequestSigningKey;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* canonicalAddress is a recipient in the form the network compares addresses
|
|
101
|
+
* by, as the provider keeps it: an EVM address in lower case, a TON address
|
|
102
|
+
* raw (workchain:hash in lower-case hex, from the user-friendly form when its
|
|
103
|
+
* checksum holds), anything else as given. An address that is not well formed
|
|
104
|
+
* for its network is returned unchanged, as the provider does.
|
|
105
|
+
*/
|
|
106
|
+
export declare function canonicalAddress(network: string, address: string): string;
|
|
107
|
+
/** sameAddress compares two recipients as the network compares addresses. */
|
|
108
|
+
export declare function sameAddress(network: string, a: string, b: string): boolean;
|
|
109
|
+
/** buildLinkedRequestGrantEnrollment checks every bound locally and returns the closed create body. */
|
|
110
|
+
export declare function buildLinkedRequestGrantEnrollment(key: RequestSigningKey, enrollment: LinkedRequestGrantEnrollment): LinkedRequestGrantEnrollmentCreate;
|
|
111
|
+
/** validateLinkedEnrollmentCreate is the client's createOperation check of the body above. */
|
|
112
|
+
export declare function validateLinkedEnrollmentCreate(value: unknown, expectedOwnerId: string): void;
|
|
113
|
+
/** parseLinkedEnrollmentDisplay reads the enrollment display of a linked wallet. */
|
|
114
|
+
export declare function parseLinkedEnrollmentDisplay(value: unknown): LinkedEnrollmentDisplay;
|
|
115
|
+
/** parseLinkedRequestGrantResult accepts exactly the protected result of a linked grant. */
|
|
116
|
+
export declare function parseLinkedRequestGrantResult(payload: unknown): LinkedRequestGrantResult;
|
|
117
|
+
/**
|
|
118
|
+
* pairLinkedRequestGrant keeps a grant beside the key that enrolled it, and
|
|
119
|
+
* refuses one that is not this key's grant on these bounds: another key, owner,
|
|
120
|
+
* service, wallet, network, asset ceiling, fee ceiling or recipient list.
|
|
121
|
+
*/
|
|
122
|
+
export declare function pairLinkedRequestGrant(key: RequestSigningKey, grant: LinkedRequestGrantResult, enrollment: LinkedRequestGrantEnrollment): PairedLinkedRequestGrant;
|
|
123
|
+
/** What the person chooses for one transfer. A comment is carried on TON only. */
|
|
124
|
+
export interface LinkedTransferIntent {
|
|
125
|
+
readonly asset: LinkedModeCAsset;
|
|
126
|
+
readonly recipient: string;
|
|
127
|
+
readonly amountRaw: string;
|
|
128
|
+
readonly comment?: string;
|
|
129
|
+
}
|
|
130
|
+
/** The closed OperationCreate a linked transfer's request hash covers. */
|
|
131
|
+
export interface LinkedMediatedOperationCreate {
|
|
132
|
+
readonly kind: "wallet.send";
|
|
133
|
+
readonly owner_type: "account";
|
|
134
|
+
readonly owner_id: string;
|
|
135
|
+
readonly linked_wallet_id: string;
|
|
136
|
+
readonly payload: {
|
|
137
|
+
readonly network: string;
|
|
138
|
+
readonly asset: LinkedModeCAsset;
|
|
139
|
+
readonly recipient: string;
|
|
140
|
+
readonly amount_raw: string;
|
|
141
|
+
readonly comment?: string;
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/** The transfer exactly as the app shows it before signing. */
|
|
145
|
+
export interface LinkedTransferDisplay {
|
|
146
|
+
readonly ownerId: string;
|
|
147
|
+
readonly linkedWalletId: string;
|
|
148
|
+
readonly servicePrincipalId: string;
|
|
149
|
+
readonly network: string;
|
|
150
|
+
readonly asset: LinkedModeCAsset;
|
|
151
|
+
readonly recipient: string;
|
|
152
|
+
readonly amountRaw: string;
|
|
153
|
+
readonly comment?: string;
|
|
154
|
+
readonly maxFeeRaw: string;
|
|
155
|
+
}
|
|
156
|
+
export interface PreparedLinkedTransfer {
|
|
157
|
+
readonly display: LinkedTransferDisplay;
|
|
158
|
+
readonly operation: LinkedMediatedOperationCreate;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* prepareLinkedMediatedTransfer derives the display and the closed body from
|
|
162
|
+
* the grant and the person's choice, and refuses before any signature what the
|
|
163
|
+
* grant does not cover: another asset, an amount over its ceiling, a recipient
|
|
164
|
+
* off a non-empty list, a comment off TON.
|
|
165
|
+
*/
|
|
166
|
+
export declare function prepareLinkedMediatedTransfer(paired: PairedLinkedRequestGrant, intent: LinkedTransferIntent): PreparedLinkedTransfer;
|
|
167
|
+
/** canonicalLinkedMediatedRequest is the RFC 8785 serialization the request hash covers. */
|
|
168
|
+
export declare function canonicalLinkedMediatedRequest(operation: LinkedMediatedOperationCreate): string;
|
|
169
|
+
/** The device share as the request carries it, snake_case. */
|
|
170
|
+
export interface WireDeviceShare {
|
|
171
|
+
readonly ciphertext: string;
|
|
172
|
+
readonly nonce: string;
|
|
173
|
+
readonly ephemeral_public_key: string;
|
|
174
|
+
readonly encryption_algorithm: string;
|
|
175
|
+
readonly encryption_context: string;
|
|
176
|
+
}
|
|
177
|
+
export declare function wireDeviceShare(share: DeviceSharePackage): WireDeviceShare;
|
|
178
|
+
/** deviceShareDigest is the lowercase hex SHA-256 of the RFC 8785 JSON of the share as the request carries it. */
|
|
179
|
+
export declare function deviceShareDigest(crypto: Crypto, share: WireDeviceShare): Promise<string>;
|
|
180
|
+
/** The request the backend forwards, unchanged, with Idempotency-Key = idempotencyKey. */
|
|
181
|
+
export interface LinkedMediatedRequest {
|
|
182
|
+
readonly operation: LinkedMediatedOperationCreate;
|
|
183
|
+
readonly user_proof: string;
|
|
184
|
+
readonly device_share: WireDeviceShare;
|
|
185
|
+
}
|
|
186
|
+
export interface SignedLinkedMediatedRequest {
|
|
187
|
+
readonly display: LinkedTransferDisplay;
|
|
188
|
+
readonly request: LinkedMediatedRequest;
|
|
189
|
+
readonly idempotencyKey: string;
|
|
190
|
+
readonly requestHash: string;
|
|
191
|
+
readonly issuedAt: number;
|
|
192
|
+
readonly expiresAt: number;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* signLinkedMediatedRequest signs the shown transfer and the device share with
|
|
196
|
+
* the enrolled key as the 2.0 proof. The share is the one sealed to the Wallet
|
|
197
|
+
* for this transfer (resealDeviceShare); its digest is signed, so the backend
|
|
198
|
+
* can neither swap it nor change the transfer. The request lives at most 60
|
|
199
|
+
* seconds and never past the grant.
|
|
200
|
+
*/
|
|
201
|
+
export declare function signLinkedMediatedRequest(input: {
|
|
202
|
+
grant: PairedLinkedRequestGrant;
|
|
203
|
+
prepared: PreparedLinkedTransfer;
|
|
204
|
+
deviceShare: DeviceSharePackage;
|
|
205
|
+
crypto: Crypto;
|
|
206
|
+
serverNowSeconds: number;
|
|
207
|
+
}): Promise<SignedLinkedMediatedRequest>;
|