@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
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
// Mode C on the person's own linked wallet (owner decision, 14 October 2026).
|
|
2
|
+
//
|
|
3
|
+
// The person enrolls, once and with a passkey, a request key bounded to one of
|
|
4
|
+
// their linked wallets: the network the transfers are made on, a ceiling for
|
|
5
|
+
// one transfer of each asset, a ceiling on the network fee and the recipients
|
|
6
|
+
// (none: any). Each later transfer goes with no passkey: the app shows the
|
|
7
|
+
// transfer, signs it and the device share it sealed for the Wallet with the
|
|
8
|
+
// enrolled key (proof 2.0), and hands the request to its own backend, which
|
|
9
|
+
// forwards it unchanged with its service key. The service key never reaches
|
|
10
|
+
// this code, and nothing here sends the request: forwarding belongs to the
|
|
11
|
+
// backend. Every locally detectable fault is refused before the signing key
|
|
12
|
+
// is touched.
|
|
13
|
+
import { ClientFlowError, base64url, canonicalDecimal, integer, opaqueId, record } from "./auth.js";
|
|
14
|
+
import { canonicalJSON, canonicalJSONBytes, sha256HexLower } from "./canonicalize.js";
|
|
15
|
+
import { MaxModeCProofLifetimeSeconds, ModeCJtiBytes, ModeCProofAudience, ModeCProofKind, modeCProtectedHeader } from "./modeC.js";
|
|
16
|
+
import { MaxRequestGrantLifetimeSeconds, MaxRequestGrantRecipients } from "./requestGrant.js";
|
|
17
|
+
export { ModeCServerClock, MaxModeCProofLifetimeSeconds } from "./modeC.js";
|
|
18
|
+
export { generateRequestSigningKey } from "./requestGrant.js";
|
|
19
|
+
/** ModeCLinkedProofVersion is the proof of a transfer from a linked wallet. */
|
|
20
|
+
export const ModeCLinkedProofVersion = "2.0";
|
|
21
|
+
/** The assets a linked grant may name. */
|
|
22
|
+
export const LinkedModeCAssets = ["native", "usdt", "usdc"];
|
|
23
|
+
const maxUint256 = (1n << 256n) - 1n;
|
|
24
|
+
const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
25
|
+
const linkedNetwork = /^[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32}$/;
|
|
26
|
+
const printable = /^[\x21-\x7e]{1,256}$/;
|
|
27
|
+
const grantToken = /^[A-Za-z0-9_-]{43}$/;
|
|
28
|
+
const fault = (code) => { throw new ClientFlowError(code); };
|
|
29
|
+
function quantity(value, positive) {
|
|
30
|
+
return canonicalDecimal(value) && BigInt(value) <= maxUint256 && (!positive || BigInt(value) > 0n);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* canonicalAddress is a recipient in the form the network compares addresses
|
|
34
|
+
* by, as the provider keeps it: an EVM address in lower case, a TON address
|
|
35
|
+
* raw (workchain:hash in lower-case hex, from the user-friendly form when its
|
|
36
|
+
* checksum holds), anything else as given. An address that is not well formed
|
|
37
|
+
* for its network is returned unchanged, as the provider does.
|
|
38
|
+
*/
|
|
39
|
+
export function canonicalAddress(network, address) {
|
|
40
|
+
if (network.startsWith("ton:"))
|
|
41
|
+
return canonicalTONAddress(network, address);
|
|
42
|
+
if (!network.startsWith("eip155:") || !/^0[xX][0-9a-fA-F]{40}$/.test(address))
|
|
43
|
+
return address;
|
|
44
|
+
return "0x" + address.slice(2).toLowerCase();
|
|
45
|
+
}
|
|
46
|
+
function canonicalTONAddress(network, address) {
|
|
47
|
+
const raw = /^(0|-1):([0-9a-fA-F]{64})$/.exec(address);
|
|
48
|
+
if (raw)
|
|
49
|
+
return `${raw[1]}:${raw[2].toLowerCase()}`;
|
|
50
|
+
if (address.includes(":") || address.length !== 48 || !/^[A-Za-z0-9+/_-]{48}$/.test(address))
|
|
51
|
+
return address;
|
|
52
|
+
const bytes = Uint8Array.from(atob(address.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0));
|
|
53
|
+
if (bytes.length !== 36 || (network === "ton:mainnet" && (bytes[0] & 0x80) !== 0) || ((bytes[0] & 0x7f) !== 0x11 && (bytes[0] & 0x7f) !== 0x51))
|
|
54
|
+
return address;
|
|
55
|
+
let crc = 0;
|
|
56
|
+
for (const byte of bytes.subarray(0, 34)) {
|
|
57
|
+
crc ^= byte << 8;
|
|
58
|
+
for (let bit = 0; bit < 8; bit++)
|
|
59
|
+
crc = crc & 0x8000 ? ((crc << 1) ^ 0x1021) & 0xffff : (crc << 1) & 0xffff;
|
|
60
|
+
}
|
|
61
|
+
if (((bytes[34] << 8) | bytes[35]) !== crc)
|
|
62
|
+
return address;
|
|
63
|
+
const workchain = bytes[1] > 127 ? bytes[1] - 256 : bytes[1];
|
|
64
|
+
return `${workchain}:${Array.from(bytes.subarray(2, 34), (b) => b.toString(16).padStart(2, "0")).join("")}`;
|
|
65
|
+
}
|
|
66
|
+
/** sameAddress compares two recipients as the network compares addresses. */
|
|
67
|
+
export function sameAddress(network, a, b) {
|
|
68
|
+
return canonicalAddress(network, a) === canonicalAddress(network, b);
|
|
69
|
+
}
|
|
70
|
+
function assets(value, code) {
|
|
71
|
+
if (!Array.isArray(value) || value.length < 1 || value.length > LinkedModeCAssets.length)
|
|
72
|
+
fault(code);
|
|
73
|
+
const out = [];
|
|
74
|
+
for (const item of value) {
|
|
75
|
+
const r = record(item, ["asset", "max_amount_raw"]);
|
|
76
|
+
if (!LinkedModeCAssets.includes(r.asset) || !quantity(r.max_amount_raw, true) || out.some((a) => a.asset === r.asset))
|
|
77
|
+
fault(code);
|
|
78
|
+
out.push(Object.freeze({ asset: r.asset, maxAmountRaw: r.max_amount_raw }));
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
function recipients(network, value, code) {
|
|
83
|
+
if (!Array.isArray(value) || value.length > MaxRequestGrantRecipients)
|
|
84
|
+
fault(code);
|
|
85
|
+
const out = [];
|
|
86
|
+
for (const item of value) {
|
|
87
|
+
if (typeof item !== "string" || !printable.test(item) || out.some((seen) => sameAddress(network, seen, item)))
|
|
88
|
+
fault(code);
|
|
89
|
+
out.push(canonicalAddress(network, item));
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
/** buildLinkedRequestGrantEnrollment checks every bound locally and returns the closed create body. */
|
|
94
|
+
export function buildLinkedRequestGrantEnrollment(key, enrollment) {
|
|
95
|
+
const code = "invalid_request_grant_enrollment";
|
|
96
|
+
const e = record(enrollment, ["ownerId", "servicePrincipalId", "linkedWalletId", "network", "assets", "recipientAllowlist", "maxFeeRaw"]);
|
|
97
|
+
if (!opaqueId(e.ownerId) || !opaqueId(e.servicePrincipalId) || typeof e.linkedWalletId !== "string" || !uuid.test(e.linkedWalletId) ||
|
|
98
|
+
typeof e.network !== "string" || !linkedNetwork.test(e.network) || !quantity(e.maxFeeRaw, false) || !key?.publicJwk)
|
|
99
|
+
fault(code);
|
|
100
|
+
const network = e.network;
|
|
101
|
+
const ceilings = assets(e.assets?.map((a) => ({ asset: a?.asset, max_amount_raw: a?.maxAmountRaw })), code);
|
|
102
|
+
return {
|
|
103
|
+
kind: "session.authorize_requests", owner_type: "account", owner_id: e.ownerId,
|
|
104
|
+
payload: {
|
|
105
|
+
public_jwk: { kty: key.publicJwk.kty, crv: key.publicJwk.crv, x: key.publicJwk.x, y: key.publicJwk.y },
|
|
106
|
+
service_principal_id: e.servicePrincipalId, linked_wallet_id: e.linkedWalletId, network,
|
|
107
|
+
assets: ceilings.map((a) => ({ asset: a.asset, max_amount_raw: a.maxAmountRaw })),
|
|
108
|
+
recipient_allowlist: recipients(network, e.recipientAllowlist, code), max_fee_raw: e.maxFeeRaw,
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/** validateLinkedEnrollmentCreate is the client's createOperation check of the body above. */
|
|
113
|
+
export function validateLinkedEnrollmentCreate(value, expectedOwnerId) {
|
|
114
|
+
const code = "invalid_operation_create";
|
|
115
|
+
const create = record(value, ["kind", "owner_type", "owner_id", "payload"], ["result_recipient"]);
|
|
116
|
+
const p = record(create.payload, ["public_jwk", "service_principal_id", "linked_wallet_id", "network", "assets", "recipient_allowlist", "max_fee_raw"]);
|
|
117
|
+
const jwk = record(p.public_jwk, ["kty", "crv", "x", "y"]);
|
|
118
|
+
if (create.kind !== "session.authorize_requests" || create.owner_type !== "account" || create.owner_id !== expectedOwnerId || !opaqueId(create.owner_id) ||
|
|
119
|
+
jwk.kty !== "EC" || jwk.crv !== "P-256" || typeof jwk.x !== "string" || !grantToken.test(jwk.x) || typeof jwk.y !== "string" || !grantToken.test(jwk.y) ||
|
|
120
|
+
!opaqueId(p.service_principal_id) || typeof p.linked_wallet_id !== "string" || !uuid.test(p.linked_wallet_id) || typeof p.network !== "string" ||
|
|
121
|
+
!linkedNetwork.test(p.network) || !quantity(p.max_fee_raw, false))
|
|
122
|
+
fault(code);
|
|
123
|
+
assets(p.assets, code);
|
|
124
|
+
recipients(p.network, p.recipient_allowlist, code);
|
|
125
|
+
}
|
|
126
|
+
/** parseLinkedEnrollmentDisplay reads the enrollment display of a linked wallet. */
|
|
127
|
+
export function parseLinkedEnrollmentDisplay(value) {
|
|
128
|
+
const code = "invalid_server_response";
|
|
129
|
+
const d = record(value, ["address", "assets", "kind", "linked_wallet_id", "max_fee_raw", "network", "owner_id", "public_key_thumbprint", "recipient_allowlist", "requester_id", "service_principal_id"]);
|
|
130
|
+
if (d.kind !== "session.authorize_requests" || !opaqueId(d.owner_id) || !opaqueId(d.requester_id) || !opaqueId(d.service_principal_id) ||
|
|
131
|
+
typeof d.linked_wallet_id !== "string" || !uuid.test(d.linked_wallet_id) || typeof d.address !== "string" || !printable.test(d.address) ||
|
|
132
|
+
typeof d.network !== "string" || !linkedNetwork.test(d.network) || !quantity(d.max_fee_raw, false) || typeof d.public_key_thumbprint !== "string" || !grantToken.test(d.public_key_thumbprint))
|
|
133
|
+
fault(code);
|
|
134
|
+
return {
|
|
135
|
+
kind: "session.authorize_requests", ownerId: d.owner_id, requesterId: d.requester_id, servicePrincipalId: d.service_principal_id,
|
|
136
|
+
linkedWalletId: d.linked_wallet_id, address: d.address, network: d.network, assets: assets(d.assets, code),
|
|
137
|
+
recipientAllowlist: recipients(d.network, d.recipient_allowlist, code), maxFeeRaw: d.max_fee_raw, publicKeyThumbprint: d.public_key_thumbprint,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/** parseLinkedRequestGrantResult accepts exactly the protected result of a linked grant. */
|
|
141
|
+
export function parseLinkedRequestGrantResult(payload) {
|
|
142
|
+
const code = "invalid_request_grant_result";
|
|
143
|
+
const root = record(payload, ["request_grant_id", "public_key_thumbprint", "server_time", "expires_at", "binding", "bounds"]);
|
|
144
|
+
if (typeof root.request_grant_id !== "string" || !grantToken.test(root.request_grant_id) || typeof root.public_key_thumbprint !== "string" ||
|
|
145
|
+
!grantToken.test(root.public_key_thumbprint) || !integer(root.server_time) || !integer(root.expires_at) || root.expires_at <= root.server_time ||
|
|
146
|
+
root.expires_at - root.server_time > MaxRequestGrantLifetimeSeconds)
|
|
147
|
+
fault(code);
|
|
148
|
+
const b = record(root.binding, ["tenant_id", "application_id", "environment_id", "client_id", "account_id", "session_id", "service_principal_id", "linked_wallet_id", "binding_revision"]);
|
|
149
|
+
if (![b.tenant_id, b.application_id, b.environment_id, b.client_id, b.account_id, b.session_id, b.service_principal_id].every(opaqueId) ||
|
|
150
|
+
typeof b.linked_wallet_id !== "string" || !uuid.test(b.linked_wallet_id) || !canonicalDecimal(b.binding_revision) || b.binding_revision === "0")
|
|
151
|
+
fault(code);
|
|
152
|
+
const bounds = record(root.bounds, ["network", "recipient_allowlist", "assets", "max_fee_raw"]);
|
|
153
|
+
if (typeof bounds.network !== "string" || !linkedNetwork.test(bounds.network) || !quantity(bounds.max_fee_raw, false))
|
|
154
|
+
fault(code);
|
|
155
|
+
return Object.freeze({
|
|
156
|
+
requestGrantId: root.request_grant_id, publicKeyThumbprint: root.public_key_thumbprint,
|
|
157
|
+
serverTime: root.server_time, expiresAt: root.expires_at,
|
|
158
|
+
binding: Object.freeze({ tenantId: b.tenant_id, applicationId: b.application_id, environmentId: b.environment_id,
|
|
159
|
+
clientId: b.client_id, accountId: b.account_id, sessionId: b.session_id, servicePrincipalId: b.service_principal_id,
|
|
160
|
+
linkedWalletId: b.linked_wallet_id, bindingRevision: b.binding_revision }),
|
|
161
|
+
bounds: Object.freeze({ network: bounds.network, recipientAllowlist: Object.freeze(recipients(bounds.network, bounds.recipient_allowlist, code)),
|
|
162
|
+
assets: Object.freeze(assets(bounds.assets, code)), maxFeeRaw: bounds.max_fee_raw }),
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* pairLinkedRequestGrant keeps a grant beside the key that enrolled it, and
|
|
167
|
+
* refuses one that is not this key's grant on these bounds: another key, owner,
|
|
168
|
+
* service, wallet, network, asset ceiling, fee ceiling or recipient list.
|
|
169
|
+
*/
|
|
170
|
+
export function pairLinkedRequestGrant(key, grant, enrollment) {
|
|
171
|
+
const mismatch = () => fault("request_grant_mismatch");
|
|
172
|
+
const b = grant.binding, n = grant.bounds;
|
|
173
|
+
if (grant.publicKeyThumbprint !== key.thumbprint || b.accountId !== enrollment.ownerId || b.servicePrincipalId !== enrollment.servicePrincipalId ||
|
|
174
|
+
b.linkedWalletId !== enrollment.linkedWalletId || n.network !== enrollment.network || n.maxFeeRaw !== enrollment.maxFeeRaw ||
|
|
175
|
+
n.assets.length !== enrollment.assets.length || n.assets.some((a, i) => a.asset !== enrollment.assets[i].asset || a.maxAmountRaw !== enrollment.assets[i].maxAmountRaw) ||
|
|
176
|
+
n.recipientAllowlist.length !== enrollment.recipientAllowlist.length ||
|
|
177
|
+
n.recipientAllowlist.some((r, i) => !sameAddress(n.network, r, enrollment.recipientAllowlist[i])))
|
|
178
|
+
mismatch();
|
|
179
|
+
return Object.freeze({ grant, key });
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* prepareLinkedMediatedTransfer derives the display and the closed body from
|
|
183
|
+
* the grant and the person's choice, and refuses before any signature what the
|
|
184
|
+
* grant does not cover: another asset, an amount over its ceiling, a recipient
|
|
185
|
+
* off a non-empty list, a comment off TON.
|
|
186
|
+
*/
|
|
187
|
+
export function prepareLinkedMediatedTransfer(paired, intent) {
|
|
188
|
+
const code = "invalid_transfer_intent";
|
|
189
|
+
const i = record(intent, ["asset", "recipient", "amountRaw"], ["comment"]);
|
|
190
|
+
const { grant } = paired;
|
|
191
|
+
const network = grant.bounds.network;
|
|
192
|
+
const ceiling = grant.bounds.assets.find((a) => a.asset === i.asset);
|
|
193
|
+
if (!ceiling || typeof i.recipient !== "string" || !printable.test(i.recipient) || !quantity(i.amountRaw, true) || BigInt(i.amountRaw) > BigInt(ceiling.maxAmountRaw))
|
|
194
|
+
fault(code);
|
|
195
|
+
const allowed = grant.bounds.recipientAllowlist;
|
|
196
|
+
if (allowed.length !== 0 && !allowed.some((r) => sameAddress(network, r, i.recipient)))
|
|
197
|
+
fault(code);
|
|
198
|
+
if (i.comment !== undefined && (typeof i.comment !== "string" || i.comment === "" || new TextEncoder().encode(i.comment).length > 120 || !network.startsWith("ton:")))
|
|
199
|
+
fault(code);
|
|
200
|
+
const payload = { network, asset: ceiling.asset, recipient: i.recipient, amount_raw: i.amountRaw, ...(i.comment !== undefined ? { comment: i.comment } : {}) };
|
|
201
|
+
const b = grant.binding;
|
|
202
|
+
return Object.freeze({
|
|
203
|
+
display: Object.freeze({ ownerId: b.accountId, linkedWalletId: b.linkedWalletId, servicePrincipalId: b.servicePrincipalId, network, asset: payload.asset,
|
|
204
|
+
recipient: payload.recipient, amountRaw: payload.amount_raw, ...(payload.comment !== undefined ? { comment: payload.comment } : {}), maxFeeRaw: grant.bounds.maxFeeRaw }),
|
|
205
|
+
operation: Object.freeze({ kind: "wallet.send", owner_type: "account", owner_id: b.accountId, linked_wallet_id: b.linkedWalletId, payload: Object.freeze(payload) }),
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
/** canonicalLinkedMediatedRequest is the RFC 8785 serialization the request hash covers. */
|
|
209
|
+
export function canonicalLinkedMediatedRequest(operation) {
|
|
210
|
+
const o = record(operation, ["kind", "owner_type", "owner_id", "linked_wallet_id", "payload"]);
|
|
211
|
+
const p = record(o.payload, ["network", "asset", "recipient", "amount_raw"], ["comment"]);
|
|
212
|
+
const payload = { network: p.network, asset: p.asset, recipient: p.recipient, amount_raw: p.amount_raw };
|
|
213
|
+
if (p.comment !== undefined)
|
|
214
|
+
payload.comment = p.comment;
|
|
215
|
+
return canonicalJSON({ kind: o.kind, owner_type: o.owner_type, owner_id: o.owner_id, linked_wallet_id: o.linked_wallet_id, payload });
|
|
216
|
+
}
|
|
217
|
+
export function wireDeviceShare(share) {
|
|
218
|
+
const s = record(share, ["ciphertext", "nonce", "ephemeralPublicKey", "encryptionAlgorithm", "encryptionContext"]);
|
|
219
|
+
if (![s.ciphertext, s.nonce, s.ephemeralPublicKey, s.encryptionAlgorithm, s.encryptionContext].every((v) => typeof v === "string" && v !== ""))
|
|
220
|
+
fault("device_share_unavailable");
|
|
221
|
+
return Object.freeze({ ciphertext: s.ciphertext, nonce: s.nonce, ephemeral_public_key: s.ephemeralPublicKey,
|
|
222
|
+
encryption_algorithm: s.encryptionAlgorithm, encryption_context: s.encryptionContext });
|
|
223
|
+
}
|
|
224
|
+
/** deviceShareDigest is the lowercase hex SHA-256 of the RFC 8785 JSON of the share as the request carries it. */
|
|
225
|
+
export function deviceShareDigest(crypto, share) {
|
|
226
|
+
return sha256HexLower(crypto, canonicalJSONBytes({ ...share }));
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* signLinkedMediatedRequest signs the shown transfer and the device share with
|
|
230
|
+
* the enrolled key as the 2.0 proof. The share is the one sealed to the Wallet
|
|
231
|
+
* for this transfer (resealDeviceShare); its digest is signed, so the backend
|
|
232
|
+
* can neither swap it nor change the transfer. The request lives at most 60
|
|
233
|
+
* seconds and never past the grant.
|
|
234
|
+
*/
|
|
235
|
+
export async function signLinkedMediatedRequest(input) {
|
|
236
|
+
const { grant, prepared, crypto, serverNowSeconds } = record(input, ["grant", "prepared", "deviceShare", "crypto", "serverNowSeconds"]);
|
|
237
|
+
if (!grant?.grant || !grant.key?.privateKey || !prepared?.operation || !prepared.display)
|
|
238
|
+
fault("opaque_signing_input_refused");
|
|
239
|
+
if (!crypto?.subtle || typeof crypto.getRandomValues !== "function")
|
|
240
|
+
fault("runtime_capability_unavailable");
|
|
241
|
+
const again = prepareLinkedMediatedTransfer(grant, { asset: prepared.display.asset, recipient: prepared.display.recipient, amountRaw: prepared.display.amountRaw,
|
|
242
|
+
...(prepared.display.comment !== undefined ? { comment: prepared.display.comment } : {}) });
|
|
243
|
+
// Everything the person was shown is what the grant names and what is signed.
|
|
244
|
+
const shown = record(prepared.display, ["ownerId", "linkedWalletId", "servicePrincipalId", "network", "asset", "recipient", "amountRaw", "maxFeeRaw"], ["comment"]);
|
|
245
|
+
const members = new Set([...Object.keys(shown), ...Object.keys(again.display)]);
|
|
246
|
+
if ([...members].some((m) => shown[m] !== again.display[m]) ||
|
|
247
|
+
canonicalLinkedMediatedRequest(again.operation) !== canonicalLinkedMediatedRequest(prepared.operation))
|
|
248
|
+
fault("display_intent_mismatch");
|
|
249
|
+
if (!integer(serverNowSeconds) || serverNowSeconds <= 0 || serverNowSeconds >= grant.grant.expiresAt)
|
|
250
|
+
fault("request_grant_reenrollment_required");
|
|
251
|
+
const share = wireDeviceShare(input.deviceShare);
|
|
252
|
+
const issuedAt = serverNowSeconds, expiresAt = Math.min(issuedAt + MaxModeCProofLifetimeSeconds, grant.grant.expiresAt);
|
|
253
|
+
const requestHash = await sha256HexLower(crypto, new TextEncoder().encode(canonicalLinkedMediatedRequest(prepared.operation)));
|
|
254
|
+
const jti = base64url(crypto.getRandomValues(new Uint8Array(ModeCJtiBytes)));
|
|
255
|
+
const b = grant.grant.binding;
|
|
256
|
+
const payload = canonicalJSONBytes({
|
|
257
|
+
version: ModeCLinkedProofVersion, aud: ModeCProofAudience, tenant_id: b.tenantId, application_id: b.applicationId, environment_id: b.environmentId,
|
|
258
|
+
client_id: b.clientId, account_id: b.accountId, session_id: b.sessionId, service_principal_id: b.servicePrincipalId,
|
|
259
|
+
linked_wallet_id: b.linkedWalletId, binding_revision: b.bindingRevision, device_share_digest: await deviceShareDigest(crypto, share),
|
|
260
|
+
kind: ModeCProofKind, request_hash: requestHash, jti, iat: issuedAt, exp: expiresAt,
|
|
261
|
+
});
|
|
262
|
+
const signingInput = `${base64url(new TextEncoder().encode(modeCProtectedHeader(grant.grant.requestGrantId)))}.${base64url(payload)}`;
|
|
263
|
+
const signature = new Uint8Array(await crypto.subtle.sign({ name: "ECDSA", hash: { name: "SHA-256" } }, grant.key.privateKey, new TextEncoder().encode(signingInput)));
|
|
264
|
+
if (signature.length !== 64)
|
|
265
|
+
fault("invalid_request_signature");
|
|
266
|
+
return Object.freeze({
|
|
267
|
+
display: prepared.display, idempotencyKey: jti, requestHash, issuedAt, expiresAt,
|
|
268
|
+
request: Object.freeze({ operation: prepared.operation, user_proof: `${signingInput}.${base64url(signature)}`, device_share: share }),
|
|
269
|
+
});
|
|
270
|
+
}
|
package/dist/pages.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface PageOptions {
|
|
2
|
+
limit?: number;
|
|
3
|
+
cursor?: string;
|
|
4
|
+
signal?: AbortSignal;
|
|
5
|
+
}
|
|
6
|
+
export type PageEnvelope = {
|
|
7
|
+
request_id: string;
|
|
8
|
+
items: unknown[];
|
|
9
|
+
has_more: boolean;
|
|
10
|
+
next_cursor?: string;
|
|
11
|
+
};
|
|
12
|
+
export declare function pageRequest(path: string, input?: AbortSignal | PageOptions): {
|
|
13
|
+
path: string;
|
|
14
|
+
signal?: AbortSignal;
|
|
15
|
+
};
|
|
16
|
+
export declare function pageEnvelope(payload: unknown): PageEnvelope;
|
|
17
|
+
export declare function pageProjection<T>(requestId: string, items: T[], hasMore: boolean, nextCursor?: string): {
|
|
18
|
+
requestId: string;
|
|
19
|
+
items: T[];
|
|
20
|
+
hasMore: boolean;
|
|
21
|
+
nextCursor?: string;
|
|
22
|
+
};
|
package/dist/pages.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Paging of the account lists: the closed request options and the envelope
|
|
2
|
+
// every page arrives in.
|
|
3
|
+
import { ClientFlowError, integer, opaqueId, record } from "./auth.js";
|
|
4
|
+
export function pageRequest(path, input) {
|
|
5
|
+
if (isAbortSignal(input))
|
|
6
|
+
return { path, signal: input };
|
|
7
|
+
const options = input ?? {};
|
|
8
|
+
if (options === null || typeof options !== "object" || Array.isArray(options) || Object.keys(options).some((key) => !["limit", "cursor", "signal"].includes(key))) {
|
|
9
|
+
throw new ClientFlowError("invalid_page_request");
|
|
10
|
+
}
|
|
11
|
+
if (options.signal !== undefined && !isAbortSignal(options.signal))
|
|
12
|
+
throw new ClientFlowError("invalid_page_request");
|
|
13
|
+
const query = new URLSearchParams();
|
|
14
|
+
if (options.limit !== undefined) {
|
|
15
|
+
if (!integer(options.limit) || options.limit < 1 || options.limit > 100)
|
|
16
|
+
throw new ClientFlowError("invalid_page_request");
|
|
17
|
+
query.set("limit", String(options.limit));
|
|
18
|
+
}
|
|
19
|
+
if (options.cursor !== undefined) {
|
|
20
|
+
if (typeof options.cursor !== "string" || options.cursor.length === 0)
|
|
21
|
+
throw new ClientFlowError("invalid_page_request");
|
|
22
|
+
query.set("cursor", options.cursor);
|
|
23
|
+
}
|
|
24
|
+
return { path: query.size === 0 ? path : `${path}?${query.toString()}`, signal: options.signal };
|
|
25
|
+
}
|
|
26
|
+
function isAbortSignal(value) {
|
|
27
|
+
return value !== null && typeof value === "object" && typeof value.aborted === "boolean" && typeof value.addEventListener === "function";
|
|
28
|
+
}
|
|
29
|
+
export function pageEnvelope(payload) {
|
|
30
|
+
const root = record(payload, ["request_id", "items", "has_more"], ["next_cursor"]);
|
|
31
|
+
if (!opaqueId(root.request_id) || typeof root.has_more !== "boolean" || !Array.isArray(root.items))
|
|
32
|
+
throw new ClientFlowError("invalid_server_response");
|
|
33
|
+
const hasCursor = Object.prototype.hasOwnProperty.call(root, "next_cursor");
|
|
34
|
+
if (root.has_more) {
|
|
35
|
+
if (!hasCursor || typeof root.next_cursor !== "string" || root.next_cursor.length === 0)
|
|
36
|
+
throw new ClientFlowError("invalid_server_response");
|
|
37
|
+
}
|
|
38
|
+
else if (hasCursor) {
|
|
39
|
+
throw new ClientFlowError("invalid_server_response");
|
|
40
|
+
}
|
|
41
|
+
return root;
|
|
42
|
+
}
|
|
43
|
+
export function pageProjection(requestId, items, hasMore, nextCursor) {
|
|
44
|
+
return hasMore ? { requestId, items, hasMore, nextCursor } : { requestId, items, hasMore };
|
|
45
|
+
}
|
package/dist/poll.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface PollOptions<T> {
|
|
2
|
+
read: (signal: AbortSignal) => Promise<T>;
|
|
3
|
+
terminal: (value: T) => boolean;
|
|
4
|
+
signal?: AbortSignal;
|
|
5
|
+
intervalMs?: number;
|
|
6
|
+
timeoutMs?: number;
|
|
7
|
+
now?: () => number;
|
|
8
|
+
sleep?: (milliseconds: number, signal: AbortSignal) => Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
export type PollDisposition = "server_terminal" | "local_timeout" | "local_canceled";
|
|
11
|
+
export interface PollResult<T> {
|
|
12
|
+
lastKnown: T | undefined;
|
|
13
|
+
disposition: PollDisposition;
|
|
14
|
+
}
|
|
15
|
+
export declare function pollAuthoritative<T>(options: PollOptions<T>): Promise<PollResult<T>>;
|
package/dist/poll.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { ClientFlowError } from "./auth.js";
|
|
2
|
+
export async function pollAuthoritative(options) {
|
|
3
|
+
const intervalMs = options.intervalMs ?? 2_000;
|
|
4
|
+
const timeoutMs = options.timeoutMs ?? 60_000;
|
|
5
|
+
if (intervalMs < 2_000 || timeoutMs <= 0 || timeoutMs > 60_000)
|
|
6
|
+
throw new ClientFlowError("invalid_poll_bounds");
|
|
7
|
+
if (options.signal?.aborted)
|
|
8
|
+
return { lastKnown: undefined, disposition: "local_canceled" };
|
|
9
|
+
const now = options.now ?? Date.now;
|
|
10
|
+
const sleep = options.sleep ?? boundedSleep;
|
|
11
|
+
const deadline = now() + timeoutMs;
|
|
12
|
+
const boundary = new AbortController();
|
|
13
|
+
let timedOut = false;
|
|
14
|
+
const timeout = setTimeout(() => {
|
|
15
|
+
timedOut = true;
|
|
16
|
+
boundary.abort();
|
|
17
|
+
}, timeoutMs);
|
|
18
|
+
const cancel = () => boundary.abort();
|
|
19
|
+
options.signal?.addEventListener("abort", cancel, { once: true });
|
|
20
|
+
let lastKnown;
|
|
21
|
+
try {
|
|
22
|
+
while (true) {
|
|
23
|
+
if (options.signal?.aborted)
|
|
24
|
+
return { lastKnown, disposition: "local_canceled" };
|
|
25
|
+
if (timedOut || now() >= deadline)
|
|
26
|
+
return { lastKnown, disposition: "local_timeout" };
|
|
27
|
+
const observed = await raceBoundary(options.read(boundary.signal), boundary.signal);
|
|
28
|
+
if (!observed.completed)
|
|
29
|
+
return { lastKnown, disposition: timedOut ? "local_timeout" : "local_canceled" };
|
|
30
|
+
if (now() >= deadline)
|
|
31
|
+
return { lastKnown, disposition: "local_timeout" };
|
|
32
|
+
lastKnown = observed.value;
|
|
33
|
+
if (options.terminal(lastKnown))
|
|
34
|
+
return { lastKnown, disposition: "server_terminal" };
|
|
35
|
+
const remaining = deadline - now();
|
|
36
|
+
if (remaining <= 0)
|
|
37
|
+
return { lastKnown, disposition: "local_timeout" };
|
|
38
|
+
const waited = await raceBoundary(sleep(Math.min(intervalMs, remaining), boundary.signal), boundary.signal);
|
|
39
|
+
if (!waited.completed)
|
|
40
|
+
return { lastKnown, disposition: timedOut ? "local_timeout" : "local_canceled" };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
clearTimeout(timeout);
|
|
45
|
+
options.signal?.removeEventListener("abort", cancel);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async function raceBoundary(operation, signal) {
|
|
49
|
+
if (signal.aborted)
|
|
50
|
+
return { completed: false };
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
const aborted = () => resolve({ completed: false });
|
|
53
|
+
signal.addEventListener("abort", aborted, { once: true });
|
|
54
|
+
operation.then((value) => {
|
|
55
|
+
signal.removeEventListener("abort", aborted);
|
|
56
|
+
resolve({ completed: true, value });
|
|
57
|
+
}, (error) => {
|
|
58
|
+
signal.removeEventListener("abort", aborted);
|
|
59
|
+
reject(error);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
function boundedSleep(milliseconds, signal) {
|
|
64
|
+
return new Promise((resolve) => {
|
|
65
|
+
const timer = setTimeout(done, milliseconds);
|
|
66
|
+
const aborted = () => {
|
|
67
|
+
clearTimeout(timer);
|
|
68
|
+
done();
|
|
69
|
+
};
|
|
70
|
+
function done() {
|
|
71
|
+
signal.removeEventListener("abort", aborted);
|
|
72
|
+
resolve();
|
|
73
|
+
}
|
|
74
|
+
signal.addEventListener("abort", aborted, { once: true });
|
|
75
|
+
});
|
|
76
|
+
}
|
|
@@ -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;
|