@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,14 @@
|
|
|
1
|
+
/** Bytes WebCrypto accepts: a view over its own, unshared buffer. */
|
|
2
|
+
export type Bytes = Uint8Array<ArrayBuffer>;
|
|
3
|
+
/** polyval over whole 16-byte blocks. */
|
|
4
|
+
export declare function polyval(key: Uint8Array, blocks: Uint8Array): Bytes;
|
|
5
|
+
/**
|
|
6
|
+
* counterBlock is the GCM-SIV counter for one keystream block: the tag with
|
|
7
|
+
* its top bit set, counting in its first 32 bits, little-endian, wrapping
|
|
8
|
+
* inside those 32 bits.
|
|
9
|
+
*/
|
|
10
|
+
export declare function counterBlock(tag: Uint8Array, index: number): Bytes;
|
|
11
|
+
/** seal returns ciphertext followed by the 16-byte tag. */
|
|
12
|
+
export declare function aes256GcmSivSeal(subtle: SubtleCrypto, key: Bytes, nonce: Uint8Array, plaintext: Uint8Array, aad: Uint8Array): Promise<Bytes>;
|
|
13
|
+
/** open returns the plaintext, or throws when the tag does not match. */
|
|
14
|
+
export declare function aes256GcmSivOpen(subtle: SubtleCrypto, key: Bytes, nonce: Uint8Array, sealed: Uint8Array, aad: Uint8Array): Promise<Bytes>;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// AES-256-GCM-SIV (RFC 8452). The own-wallet system seals a device share with
|
|
2
|
+
// it, and WebCrypto does not offer it, so the mode is built here from the one
|
|
3
|
+
// primitive WebCrypto does offer: the AES block function, reached through
|
|
4
|
+
// AES-CTR over a single zero block. Inputs are a few dozen bytes, so every
|
|
5
|
+
// block is one WebCrypto call and POLYVAL runs on BigInt.
|
|
6
|
+
import { ClientFlowError } from "./auth.js";
|
|
7
|
+
const KEY_BYTES = 32;
|
|
8
|
+
const NONCE_BYTES = 12;
|
|
9
|
+
const TAG_BYTES = 16;
|
|
10
|
+
const BLOCK = 16;
|
|
11
|
+
// x^128 + x^127 + x^126 + x^121 + 1, the POLYVAL field polynomial.
|
|
12
|
+
const POLYNOMIAL = (1n << 128n) | (1n << 127n) | (1n << 126n) | (1n << 121n) | 1n;
|
|
13
|
+
async function importBlockKey(subtle, raw) {
|
|
14
|
+
return subtle.importKey("raw", raw, { name: "AES-CTR" }, false, ["encrypt"]);
|
|
15
|
+
}
|
|
16
|
+
/** One AES block encryption: the first CTR keystream block for this counter. */
|
|
17
|
+
async function encryptBlock(subtle, key, block) {
|
|
18
|
+
return new Uint8Array(await subtle.encrypt({ name: "AES-CTR", counter: block, length: 64 }, key, new Uint8Array(BLOCK)));
|
|
19
|
+
}
|
|
20
|
+
function fieldElement(bytes) {
|
|
21
|
+
let value = 0n;
|
|
22
|
+
for (let index = BLOCK - 1; index >= 0; index--)
|
|
23
|
+
value = (value << 8n) | BigInt(bytes[index]);
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
function fieldBytes(value) {
|
|
27
|
+
const out = new Uint8Array(BLOCK);
|
|
28
|
+
for (let index = 0; index < BLOCK; index++)
|
|
29
|
+
out[index] = Number((value >> BigInt(8 * index)) & 0xffn);
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
/** dot(a, b) = a * b * x^-128 in the POLYVAL field. */
|
|
33
|
+
function dot(a, b) {
|
|
34
|
+
let product = 0n;
|
|
35
|
+
for (let bit = 0n; bit < 128n; bit++)
|
|
36
|
+
if ((b >> bit) & 1n)
|
|
37
|
+
product ^= a << bit;
|
|
38
|
+
for (let bit = 0n; bit < 128n; bit++)
|
|
39
|
+
if ((product >> bit) & 1n)
|
|
40
|
+
product ^= POLYNOMIAL << bit;
|
|
41
|
+
return product >> 128n;
|
|
42
|
+
}
|
|
43
|
+
/** polyval over whole 16-byte blocks. */
|
|
44
|
+
export function polyval(key, blocks) {
|
|
45
|
+
if (key.length !== BLOCK || blocks.length % BLOCK !== 0)
|
|
46
|
+
throw new ClientFlowError("device_share_unavailable");
|
|
47
|
+
const h = fieldElement(key);
|
|
48
|
+
let state = 0n;
|
|
49
|
+
for (let offset = 0; offset < blocks.length; offset += BLOCK)
|
|
50
|
+
state = dot(state ^ fieldElement(blocks.subarray(offset, offset + BLOCK)), h);
|
|
51
|
+
return fieldBytes(state);
|
|
52
|
+
}
|
|
53
|
+
async function deriveRecordKeys(subtle, key, nonce) {
|
|
54
|
+
const generating = await importBlockKey(subtle, key);
|
|
55
|
+
const part = async (counter) => {
|
|
56
|
+
const block = new Uint8Array(BLOCK);
|
|
57
|
+
block[0] = counter;
|
|
58
|
+
block.set(nonce, 4);
|
|
59
|
+
return (await encryptBlock(subtle, generating, block)).subarray(0, 8);
|
|
60
|
+
};
|
|
61
|
+
const authentication = new Uint8Array(16);
|
|
62
|
+
const encryption = new Uint8Array(KEY_BYTES);
|
|
63
|
+
try {
|
|
64
|
+
authentication.set(await part(0), 0);
|
|
65
|
+
authentication.set(await part(1), 8);
|
|
66
|
+
for (let index = 0; index < 4; index++)
|
|
67
|
+
encryption.set(await part(2 + index), 8 * index);
|
|
68
|
+
return { authentication, encryption: await importBlockKey(subtle, encryption) };
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
authentication.fill(0);
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
encryption.fill(0);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function padded(length) { return Math.ceil(length / BLOCK) * BLOCK; }
|
|
79
|
+
async function computeTag(subtle, keys, nonce, plaintext, aad) {
|
|
80
|
+
const input = new Uint8Array(padded(aad.length) + padded(plaintext.length) + BLOCK);
|
|
81
|
+
input.set(aad, 0);
|
|
82
|
+
input.set(plaintext, padded(aad.length));
|
|
83
|
+
const lengths = new DataView(input.buffer, input.length - BLOCK, BLOCK);
|
|
84
|
+
lengths.setBigUint64(0, BigInt(aad.length) * 8n, true);
|
|
85
|
+
lengths.setBigUint64(8, BigInt(plaintext.length) * 8n, true);
|
|
86
|
+
const digest = polyval(keys.authentication, input);
|
|
87
|
+
input.fill(0);
|
|
88
|
+
for (let index = 0; index < NONCE_BYTES; index++)
|
|
89
|
+
digest[index] ^= nonce[index];
|
|
90
|
+
digest[15] &= 0x7f;
|
|
91
|
+
return encryptBlock(subtle, keys.encryption, digest);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* counterBlock is the GCM-SIV counter for one keystream block: the tag with
|
|
95
|
+
* its top bit set, counting in its first 32 bits, little-endian, wrapping
|
|
96
|
+
* inside those 32 bits.
|
|
97
|
+
*/
|
|
98
|
+
export function counterBlock(tag, index) {
|
|
99
|
+
const counter = new Uint8Array(tag);
|
|
100
|
+
counter[15] |= 0x80;
|
|
101
|
+
const view = new DataView(counter.buffer);
|
|
102
|
+
view.setUint32(0, (view.getUint32(0, true) + index) >>> 0, true);
|
|
103
|
+
return counter;
|
|
104
|
+
}
|
|
105
|
+
async function applyKeystream(subtle, key, tag, data) {
|
|
106
|
+
const out = new Uint8Array(data.length);
|
|
107
|
+
for (let offset = 0, index = 0; offset < data.length; offset += BLOCK, index++) {
|
|
108
|
+
const stream = await encryptBlock(subtle, key, counterBlock(tag, index));
|
|
109
|
+
for (let byte = 0; byte < BLOCK && offset + byte < data.length; byte++)
|
|
110
|
+
out[offset + byte] = data[offset + byte] ^ stream[byte];
|
|
111
|
+
stream.fill(0);
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
function checked(key, nonce) {
|
|
116
|
+
if (key.length !== KEY_BYTES || nonce.length !== NONCE_BYTES)
|
|
117
|
+
throw new ClientFlowError("device_share_unavailable");
|
|
118
|
+
}
|
|
119
|
+
/** seal returns ciphertext followed by the 16-byte tag. */
|
|
120
|
+
export async function aes256GcmSivSeal(subtle, key, nonce, plaintext, aad) {
|
|
121
|
+
checked(key, nonce);
|
|
122
|
+
const keys = await deriveRecordKeys(subtle, key, nonce);
|
|
123
|
+
try {
|
|
124
|
+
const tag = await computeTag(subtle, keys, nonce, plaintext, aad);
|
|
125
|
+
const out = new Uint8Array(plaintext.length + TAG_BYTES);
|
|
126
|
+
out.set(await applyKeystream(subtle, keys.encryption, tag, plaintext), 0);
|
|
127
|
+
out.set(tag, plaintext.length);
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
keys.authentication.fill(0);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/** open returns the plaintext, or throws when the tag does not match. */
|
|
135
|
+
export async function aes256GcmSivOpen(subtle, key, nonce, sealed, aad) {
|
|
136
|
+
checked(key, nonce);
|
|
137
|
+
if (sealed.length < TAG_BYTES)
|
|
138
|
+
throw new ClientFlowError("device_share_unavailable");
|
|
139
|
+
const keys = await deriveRecordKeys(subtle, key, nonce);
|
|
140
|
+
const tag = sealed.subarray(sealed.length - TAG_BYTES);
|
|
141
|
+
let plaintext;
|
|
142
|
+
let opened = false;
|
|
143
|
+
try {
|
|
144
|
+
plaintext = await applyKeystream(subtle, keys.encryption, tag, sealed.subarray(0, sealed.length - TAG_BYTES));
|
|
145
|
+
const expected = await computeTag(subtle, keys, nonce, plaintext, aad);
|
|
146
|
+
let difference = 0;
|
|
147
|
+
for (let index = 0; index < TAG_BYTES; index++)
|
|
148
|
+
difference |= expected[index] ^ tag[index];
|
|
149
|
+
if (difference !== 0)
|
|
150
|
+
throw new ClientFlowError("device_share_unavailable");
|
|
151
|
+
opened = true;
|
|
152
|
+
return plaintext;
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
// The plaintext leaves this function only when its tag was verified.
|
|
156
|
+
if (!opened)
|
|
157
|
+
plaintext?.fill(0);
|
|
158
|
+
keys.authentication.fill(0);
|
|
159
|
+
}
|
|
160
|
+
}
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export declare const identityMethods: readonly ["google", "apple", "telegram", "passkey"];
|
|
2
|
+
export type IdentityMethod = (typeof identityMethods)[number];
|
|
3
|
+
export type AuthIntent = "signup" | "login";
|
|
4
|
+
export type ClientChannel = "web" | "mobile";
|
|
5
|
+
export type NextAction = "none" | "reauthenticate" | "start_signup" | "start_login" | "use_passkey" | "sign_user_request" | "refresh_resource" | "retry_same_request" | "wait_for_result" | "lookup_operation" | "resync_streams";
|
|
6
|
+
export declare class ClientFlowError extends Error {
|
|
7
|
+
readonly code: string;
|
|
8
|
+
constructor(code: string);
|
|
9
|
+
}
|
|
10
|
+
export declare class AuthorizationError extends Error {
|
|
11
|
+
readonly code: string;
|
|
12
|
+
readonly retryable: boolean;
|
|
13
|
+
readonly nextAction: NextAction;
|
|
14
|
+
readonly requestId: string;
|
|
15
|
+
readonly operationId?: string;
|
|
16
|
+
readonly window?: {
|
|
17
|
+
earliestCursor: string;
|
|
18
|
+
latestCursor: string;
|
|
19
|
+
};
|
|
20
|
+
readonly status: number;
|
|
21
|
+
constructor(input: {
|
|
22
|
+
code: string;
|
|
23
|
+
retryable: boolean;
|
|
24
|
+
nextAction: NextAction;
|
|
25
|
+
requestId: string;
|
|
26
|
+
operationId?: string;
|
|
27
|
+
window?: {
|
|
28
|
+
earliestCursor: string;
|
|
29
|
+
latestCursor: string;
|
|
30
|
+
};
|
|
31
|
+
status: number;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
export declare function parseAuthorizationError(payload: unknown, status: number): AuthorizationError;
|
|
35
|
+
type ErrorExpectation = {
|
|
36
|
+
status: number;
|
|
37
|
+
retryable: boolean;
|
|
38
|
+
nextAction: NextAction;
|
|
39
|
+
};
|
|
40
|
+
declare const errorCatalog: Readonly<Record<string, ErrorExpectation>>;
|
|
41
|
+
export type AuthorizationErrorCode = keyof typeof errorCatalog;
|
|
42
|
+
export declare function isAuthorizationErrorCode(value: unknown): value is AuthorizationErrorCode;
|
|
43
|
+
export declare function newHandoffBinding(crypto: Crypto): Promise<{
|
|
44
|
+
state: string;
|
|
45
|
+
verifier: string;
|
|
46
|
+
challenge: string;
|
|
47
|
+
}>;
|
|
48
|
+
export declare function base64url(bytes: Uint8Array): string;
|
|
49
|
+
export declare function opaqueId(value: unknown): value is string;
|
|
50
|
+
export declare function canonicalDecimal(value: unknown): value is string;
|
|
51
|
+
export declare function integer(value: unknown): value is number;
|
|
52
|
+
/**
|
|
53
|
+
* rfc3339Millis accepts only a UTC RFC 3339 date-time string as source §2.2
|
|
54
|
+
* requires for resource timestamps: four-digit year, two-digit components, the
|
|
55
|
+
* literal T and Z, an optional 1-9 digit fraction and no other offset form. It
|
|
56
|
+
* returns epoch milliseconds, or undefined for anything else. Integer
|
|
57
|
+
* NumericDate seconds (JWT claims, the Mode C request-grant result and
|
|
58
|
+
* /v1/me server_time) are validated with `integer`, never here.
|
|
59
|
+
*/
|
|
60
|
+
export declare function rfc3339Millis(value: unknown): number | undefined;
|
|
61
|
+
export declare function rfc3339UTC(value: unknown): value is string;
|
|
62
|
+
/** deadlineAfter requires an accepted resource deadline strictly after `created`. */
|
|
63
|
+
export declare function deadlineAfter(created: string, expires: string): boolean;
|
|
64
|
+
/** unexpiredRFC3339 requires a resource deadline strictly after `nowMs`. */
|
|
65
|
+
export declare function unexpiredRFC3339(value: unknown, nowMs: number): value is string;
|
|
66
|
+
export declare function record(value: unknown, required: readonly string[], optional?: readonly string[]): Record<string, unknown>;
|
|
67
|
+
export declare function isNextAction(value: unknown): value is NextAction;
|
|
68
|
+
export {};
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
export const identityMethods = ["google", "apple", "telegram", "passkey"];
|
|
2
|
+
export class ClientFlowError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
constructor(code) {
|
|
5
|
+
super(code);
|
|
6
|
+
this.name = "ClientFlowError";
|
|
7
|
+
this.code = code;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export class AuthorizationError extends Error {
|
|
11
|
+
code;
|
|
12
|
+
retryable;
|
|
13
|
+
nextAction;
|
|
14
|
+
requestId;
|
|
15
|
+
operationId;
|
|
16
|
+
window;
|
|
17
|
+
status;
|
|
18
|
+
constructor(input) {
|
|
19
|
+
super(input.code);
|
|
20
|
+
this.name = "AuthorizationError";
|
|
21
|
+
this.code = input.code;
|
|
22
|
+
this.retryable = input.retryable;
|
|
23
|
+
this.nextAction = input.nextAction;
|
|
24
|
+
this.requestId = input.requestId;
|
|
25
|
+
this.operationId = input.operationId;
|
|
26
|
+
this.window = input.window;
|
|
27
|
+
this.status = input.status;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export function parseAuthorizationError(payload, status) {
|
|
31
|
+
const outer = record(payload, ["error"]);
|
|
32
|
+
const error = record(outer.error, ["code", "retryable", "next_action", "request_id"], ["operation_id", "window"]);
|
|
33
|
+
if (typeof error.code !== "string" || typeof error.retryable !== "boolean" || !isNextAction(error.next_action) || !opaqueId(error.request_id)) {
|
|
34
|
+
throw new ClientFlowError("invalid_error_response");
|
|
35
|
+
}
|
|
36
|
+
if (error.operation_id !== undefined && !opaqueId(error.operation_id)) {
|
|
37
|
+
throw new ClientFlowError("invalid_error_response");
|
|
38
|
+
}
|
|
39
|
+
const expected = errorCatalog[error.code];
|
|
40
|
+
const possibleCommit = possibleCommitCodes.has(error.code) && error.operation_id !== undefined && error.retryable === false && error.next_action === "lookup_operation";
|
|
41
|
+
if (!expected || expected.status !== status || (!possibleCommit && (error.retryable !== expected.retryable || error.next_action !== expected.nextAction))) {
|
|
42
|
+
throw new ClientFlowError("invalid_error_response");
|
|
43
|
+
}
|
|
44
|
+
if (error.operation_id !== undefined && !possibleCommit && !operationIdCodes.has(error.code)) {
|
|
45
|
+
throw new ClientFlowError("invalid_error_response");
|
|
46
|
+
}
|
|
47
|
+
if (error.code === "RESULT_NOT_READY" && error.operation_id === undefined)
|
|
48
|
+
throw new ClientFlowError("invalid_error_response");
|
|
49
|
+
let window;
|
|
50
|
+
if (error.code === "RESYNC_REQUIRED") {
|
|
51
|
+
if (error.window === undefined)
|
|
52
|
+
throw new ClientFlowError("invalid_error_response");
|
|
53
|
+
let raw;
|
|
54
|
+
try {
|
|
55
|
+
raw = record(error.window, ["earliest_cursor", "latest_cursor"]);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
throw new ClientFlowError("invalid_error_response");
|
|
59
|
+
}
|
|
60
|
+
if (!opaqueCursor(raw.earliest_cursor) || !opaqueCursor(raw.latest_cursor))
|
|
61
|
+
throw new ClientFlowError("invalid_error_response");
|
|
62
|
+
window = { earliestCursor: raw.earliest_cursor, latestCursor: raw.latest_cursor };
|
|
63
|
+
}
|
|
64
|
+
else if (error.window !== undefined) {
|
|
65
|
+
throw new ClientFlowError("invalid_error_response");
|
|
66
|
+
}
|
|
67
|
+
return new AuthorizationError({
|
|
68
|
+
code: error.code,
|
|
69
|
+
retryable: error.retryable,
|
|
70
|
+
nextAction: error.next_action,
|
|
71
|
+
requestId: error.request_id,
|
|
72
|
+
operationId: error.operation_id,
|
|
73
|
+
window,
|
|
74
|
+
status,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
const errorCatalog = Object.freeze({
|
|
78
|
+
INVALID_REQUEST: { status: 400, retryable: false, nextAction: "none" },
|
|
79
|
+
REGISTRATION_SCOPE_SELECTION_REQUIRED: { status: 409, retryable: false, nextAction: "none" },
|
|
80
|
+
CURSOR_INVALID: { status: 400, retryable: false, nextAction: "none" },
|
|
81
|
+
UNSUPPORTED_VERSION: { status: 400, retryable: false, nextAction: "none" },
|
|
82
|
+
UNAUTHORIZED: { status: 401, retryable: false, nextAction: "reauthenticate" },
|
|
83
|
+
SCOPE_DENIED: { status: 403, retryable: false, nextAction: "none" },
|
|
84
|
+
ORIGIN_NOT_ALLOWED: { status: 403, retryable: false, nextAction: "none" },
|
|
85
|
+
LIMIT_PER_TRANSFER_EXCEEDED: { status: 403, retryable: false, nextAction: "none" },
|
|
86
|
+
LIMIT_DAILY_EXCEEDED: { status: 403, retryable: false, nextAction: "none" },
|
|
87
|
+
RECIPIENT_NOT_ALLOWED: { status: 403, retryable: false, nextAction: "none" },
|
|
88
|
+
CONFIRMATION_METHOD_UNAVAILABLE: { status: 403, retryable: false, nextAction: "use_passkey" },
|
|
89
|
+
USER_PROOF_REQUIRED: { status: 403, retryable: false, nextAction: "sign_user_request" },
|
|
90
|
+
USER_PROOF_INVALID: { status: 403, retryable: false, nextAction: "sign_user_request" },
|
|
91
|
+
REQUEST_GRANT_INVALID: { status: 403, retryable: false, nextAction: "use_passkey" },
|
|
92
|
+
NOT_FOUND: { status: 404, retryable: false, nextAction: "none" },
|
|
93
|
+
IDENTITY_NOT_REGISTERED: { status: 409, retryable: false, nextAction: "start_signup" },
|
|
94
|
+
IDENTITY_ALREADY_REGISTERED: { status: 409, retryable: false, nextAction: "start_login" },
|
|
95
|
+
IDENTITY_ALREADY_LINKED: { status: 409, retryable: false, nextAction: "none" },
|
|
96
|
+
ACCOUNT_REFERENCE_UNBOUND: { status: 409, retryable: false, nextAction: "none" },
|
|
97
|
+
LAST_FACTOR: { status: 409, retryable: false, nextAction: "none" },
|
|
98
|
+
CONFIRMATION_REPLAYED: { status: 409, retryable: false, nextAction: "none" },
|
|
99
|
+
CANNOT_CANCEL: { status: 409, retryable: false, nextAction: "none" },
|
|
100
|
+
VERSION_CONFLICT: { status: 409, retryable: false, nextAction: "refresh_resource" },
|
|
101
|
+
AUTHORIZATION_STALE: { status: 409, retryable: false, nextAction: "refresh_resource" },
|
|
102
|
+
IDEMPOTENCY_CONFLICT: { status: 409, retryable: false, nextAction: "lookup_operation" },
|
|
103
|
+
USER_PROOF_REPLAYED: { status: 409, retryable: false, nextAction: "lookup_operation" },
|
|
104
|
+
OPERATION_NOT_AUTHORIZED: { status: 409, retryable: false, nextAction: "use_passkey" },
|
|
105
|
+
RESULT_NOT_READY: { status: 409, retryable: true, nextAction: "wait_for_result" },
|
|
106
|
+
TRANSFER_IN_FLIGHT: { status: 409, retryable: true, nextAction: "retry_same_request" },
|
|
107
|
+
ATTEMPT_EXPIRED: { status: 410, retryable: false, nextAction: "reauthenticate" },
|
|
108
|
+
OPERATION_EXPIRED: { status: 410, retryable: false, nextAction: "none" },
|
|
109
|
+
RESULT_EXPIRED: { status: 410, retryable: false, nextAction: "none" },
|
|
110
|
+
RESYNC_REQUIRED: { status: 410, retryable: false, nextAction: "resync_streams" },
|
|
111
|
+
USER_PROOF_EXPIRED: { status: 410, retryable: false, nextAction: "sign_user_request" },
|
|
112
|
+
CAPABILITY_UNAVAILABLE: { status: 422, retryable: false, nextAction: "none" },
|
|
113
|
+
RECOVERY_UNAVAILABLE: { status: 422, retryable: false, nextAction: "none" },
|
|
114
|
+
PAYLOAD_NOT_SUPPORTED: { status: 422, retryable: false, nextAction: "none" },
|
|
115
|
+
RATE_LIMITED: { status: 429, retryable: true, nextAction: "retry_same_request" },
|
|
116
|
+
DEPENDENCY_UNAVAILABLE: { status: 503, retryable: true, nextAction: "retry_same_request" },
|
|
117
|
+
NETWORK_FEE_TOO_HIGH: { status: 503, retryable: true, nextAction: "retry_same_request" },
|
|
118
|
+
TENANT_AUTH_UNAVAILABLE: { status: 503, retryable: true, nextAction: "retry_same_request" },
|
|
119
|
+
AUDIT_UNAVAILABLE: { status: 503, retryable: true, nextAction: "retry_same_request" },
|
|
120
|
+
ADAPTER_INTEGRITY_CONFLICT: { status: 503, retryable: false, nextAction: "none" },
|
|
121
|
+
INTERNAL_ERROR: { status: 500, retryable: true, nextAction: "retry_same_request" },
|
|
122
|
+
});
|
|
123
|
+
export function isAuthorizationErrorCode(value) {
|
|
124
|
+
return typeof value === "string" && Object.prototype.hasOwnProperty.call(errorCatalog, value);
|
|
125
|
+
}
|
|
126
|
+
const possibleCommitCodes = new Set(["DEPENDENCY_UNAVAILABLE", "TENANT_AUTH_UNAVAILABLE", "AUDIT_UNAVAILABLE", "INTERNAL_ERROR"]);
|
|
127
|
+
const operationIdCodes = new Set(["IDEMPOTENCY_CONFLICT", "USER_PROOF_REPLAYED", "RESULT_NOT_READY", "OPERATION_EXPIRED", "RESULT_EXPIRED", "OPERATION_NOT_AUTHORIZED", "ADAPTER_INTEGRITY_CONFLICT"]);
|
|
128
|
+
function opaqueCursor(value) {
|
|
129
|
+
return typeof value === "string" && value.length > 0;
|
|
130
|
+
}
|
|
131
|
+
export async function newHandoffBinding(crypto) {
|
|
132
|
+
const state = base64url(crypto.getRandomValues(new Uint8Array(32)));
|
|
133
|
+
const verifier = base64url(crypto.getRandomValues(new Uint8Array(32)));
|
|
134
|
+
const challenge = base64url(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier))));
|
|
135
|
+
return { state, verifier, challenge };
|
|
136
|
+
}
|
|
137
|
+
export function base64url(bytes) {
|
|
138
|
+
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
139
|
+
let result = "";
|
|
140
|
+
for (let index = 0; index < bytes.length; index += 3) {
|
|
141
|
+
const value = (bytes[index] << 16) | ((bytes[index + 1] ?? 0) << 8) | (bytes[index + 2] ?? 0);
|
|
142
|
+
result += alphabet[(value >>> 18) & 63] + alphabet[(value >>> 12) & 63];
|
|
143
|
+
if (index + 1 < bytes.length)
|
|
144
|
+
result += alphabet[(value >>> 6) & 63];
|
|
145
|
+
if (index + 2 < bytes.length)
|
|
146
|
+
result += alphabet[value & 63];
|
|
147
|
+
}
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
export function opaqueId(value) {
|
|
151
|
+
return typeof value === "string" && /^[A-Za-z0-9_-]{1,128}$/.test(value);
|
|
152
|
+
}
|
|
153
|
+
export function canonicalDecimal(value) {
|
|
154
|
+
return typeof value === "string" && /^(0|[1-9][0-9]*)$/.test(value);
|
|
155
|
+
}
|
|
156
|
+
export function integer(value) {
|
|
157
|
+
return typeof value === "number" && Number.isSafeInteger(value);
|
|
158
|
+
}
|
|
159
|
+
const utcDateTime = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(\.\d{1,9})?Z$/;
|
|
160
|
+
/**
|
|
161
|
+
* rfc3339Millis accepts only a UTC RFC 3339 date-time string as source §2.2
|
|
162
|
+
* requires for resource timestamps: four-digit year, two-digit components, the
|
|
163
|
+
* literal T and Z, an optional 1-9 digit fraction and no other offset form. It
|
|
164
|
+
* returns epoch milliseconds, or undefined for anything else. Integer
|
|
165
|
+
* NumericDate seconds (JWT claims, the Mode C request-grant result and
|
|
166
|
+
* /v1/me server_time) are validated with `integer`, never here.
|
|
167
|
+
*/
|
|
168
|
+
export function rfc3339Millis(value) {
|
|
169
|
+
if (typeof value !== "string")
|
|
170
|
+
return undefined;
|
|
171
|
+
const parts = utcDateTime.exec(value);
|
|
172
|
+
if (!parts)
|
|
173
|
+
return undefined;
|
|
174
|
+
const [year, month, day, hour, minute, second] = parts.slice(1, 7).map(Number);
|
|
175
|
+
const seconds = Date.UTC(year, month - 1, day, hour, minute, second);
|
|
176
|
+
if (!Number.isSafeInteger(seconds))
|
|
177
|
+
return undefined;
|
|
178
|
+
const calendar = new Date(seconds);
|
|
179
|
+
if (calendar.getUTCFullYear() !== year || calendar.getUTCMonth() + 1 !== month || calendar.getUTCDate() !== day ||
|
|
180
|
+
calendar.getUTCHours() !== hour || calendar.getUTCMinutes() !== minute || calendar.getUTCSeconds() !== second) {
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
const fraction = parts[7];
|
|
184
|
+
return fraction === undefined ? seconds : seconds + Number(fraction.slice(1, 4).padEnd(3, "0"));
|
|
185
|
+
}
|
|
186
|
+
export function rfc3339UTC(value) {
|
|
187
|
+
return rfc3339Millis(value) !== undefined;
|
|
188
|
+
}
|
|
189
|
+
/** deadlineAfter requires an accepted resource deadline strictly after `created`. */
|
|
190
|
+
export function deadlineAfter(created, expires) {
|
|
191
|
+
const from = rfc3339Millis(created);
|
|
192
|
+
const until = rfc3339Millis(expires);
|
|
193
|
+
return from !== undefined && until !== undefined && until > from;
|
|
194
|
+
}
|
|
195
|
+
/** unexpiredRFC3339 requires a resource deadline strictly after `nowMs`. */
|
|
196
|
+
export function unexpiredRFC3339(value, nowMs) {
|
|
197
|
+
const until = rfc3339Millis(value);
|
|
198
|
+
return until !== undefined && until > nowMs;
|
|
199
|
+
}
|
|
200
|
+
export function record(value, required, optional = []) {
|
|
201
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
202
|
+
throw new ClientFlowError("invalid_server_response");
|
|
203
|
+
const result = value;
|
|
204
|
+
const allowed = new Set([...required, ...optional]);
|
|
205
|
+
if (required.some((key) => !(key in result)) || Object.keys(result).some((key) => !allowed.has(key))) {
|
|
206
|
+
throw new ClientFlowError("invalid_server_response");
|
|
207
|
+
}
|
|
208
|
+
return result;
|
|
209
|
+
}
|
|
210
|
+
export function isNextAction(value) {
|
|
211
|
+
return typeof value === "string" && [
|
|
212
|
+
"none", "reauthenticate", "start_signup", "start_login", "use_passkey", "sign_user_request",
|
|
213
|
+
"refresh_resource", "retry_same_request", "wait_for_result", "lookup_operation", "resync_streams",
|
|
214
|
+
].includes(value);
|
|
215
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** ok: the node gave every amount. partial: some. unavailable: none. */
|
|
2
|
+
export type NetworkBalanceStatus = 'ok' | 'partial' | 'unavailable';
|
|
3
|
+
/**
|
|
4
|
+
* One asset of a network. amountRaw is in the asset's minimal units and is
|
|
5
|
+
* absent when the node did not give it: an unread amount is never zero.
|
|
6
|
+
* contract is the token's contract on the network, its mint on Solana and its
|
|
7
|
+
* master on TON; the network's own coin has none.
|
|
8
|
+
*/
|
|
9
|
+
export interface AssetBalance {
|
|
10
|
+
readonly asset: 'native' | 'usdt' | 'usdc';
|
|
11
|
+
readonly decimals: number;
|
|
12
|
+
readonly contract?: string;
|
|
13
|
+
readonly amountRaw?: string;
|
|
14
|
+
}
|
|
15
|
+
/** One network of the wallet: its own coin first, then its tokens. The assets are named even when no amount could be read. */
|
|
16
|
+
export interface NetworkBalances {
|
|
17
|
+
readonly network: string;
|
|
18
|
+
readonly status: NetworkBalanceStatus;
|
|
19
|
+
readonly balances: readonly AssetBalance[];
|
|
20
|
+
}
|
|
21
|
+
/** An EVM wallet has one network entry for each EVM network of the stand; any other wallet one, or none where the stand has no node for its network. */
|
|
22
|
+
export interface LinkedWalletBalances {
|
|
23
|
+
readonly requestId: string;
|
|
24
|
+
readonly linkedWalletId: string;
|
|
25
|
+
readonly address: string;
|
|
26
|
+
/** When the provider finished reading the nodes. */
|
|
27
|
+
readonly observedAt: string;
|
|
28
|
+
readonly networks: readonly NetworkBalances[];
|
|
29
|
+
}
|
|
30
|
+
/** Parses the closed balances answer of the wallet that was asked about. */
|
|
31
|
+
export declare function parseLinkedWalletBalances(payload: unknown, expectedLinkedWalletId: string): LinkedWalletBalances;
|
package/dist/balances.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// What a linked wallet holds: on each network its key has an address on, the
|
|
2
|
+
// network's own coin and the tokens the stand names there. The provider reads
|
|
3
|
+
// the amounts from the stand's own nodes, so the application hands a wallet's
|
|
4
|
+
// address to no node operator.
|
|
5
|
+
import { ClientFlowError, canonicalDecimal, integer, opaqueId, record, rfc3339UTC } from './auth.js';
|
|
6
|
+
const statuses = ['ok', 'partial', 'unavailable'];
|
|
7
|
+
const assets = ['native', 'usdt', 'usdc'];
|
|
8
|
+
/** Parses the closed balances answer of the wallet that was asked about. */
|
|
9
|
+
export function parseLinkedWalletBalances(payload, expectedLinkedWalletId) {
|
|
10
|
+
const root = record(payload, ['request_id', 'linked_wallet_id', 'address', 'observed_at', 'networks']);
|
|
11
|
+
if (!opaqueId(root.request_id) || root.linked_wallet_id !== expectedLinkedWalletId || typeof root.address !== 'string' || !/^[\x21-\x7e]{1,256}$/.test(root.address) ||
|
|
12
|
+
!rfc3339UTC(root.observed_at) || !Array.isArray(root.networks))
|
|
13
|
+
fault();
|
|
14
|
+
const networks = root.networks.map(parseNetwork);
|
|
15
|
+
if (new Set(networks.map((network) => network.network)).size !== networks.length)
|
|
16
|
+
fault();
|
|
17
|
+
return Object.freeze({ requestId: root.request_id, linkedWalletId: expectedLinkedWalletId, address: root.address, observedAt: root.observed_at, networks: Object.freeze(networks) });
|
|
18
|
+
}
|
|
19
|
+
function parseNetwork(payload) {
|
|
20
|
+
const n = record(payload, ['network', 'status', 'balances']);
|
|
21
|
+
if (typeof n.network !== 'string' || !/^[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32}$/.test(n.network) || typeof n.status !== 'string' || !statuses.includes(n.status) || !Array.isArray(n.balances) || n.balances.length === 0)
|
|
22
|
+
fault();
|
|
23
|
+
const balances = n.balances.map(parseAsset);
|
|
24
|
+
const read = balances.filter((balance) => balance.amountRaw !== undefined).length;
|
|
25
|
+
// The coin comes first and once, no asset twice, and the status says what was read.
|
|
26
|
+
if (balances[0].asset !== 'native' || new Set(balances.map((balance) => balance.asset)).size !== balances.length ||
|
|
27
|
+
n.status !== (read === balances.length ? 'ok' : read === 0 ? 'unavailable' : 'partial'))
|
|
28
|
+
fault();
|
|
29
|
+
return Object.freeze({ network: n.network, status: n.status, balances: Object.freeze(balances) });
|
|
30
|
+
}
|
|
31
|
+
function parseAsset(payload) {
|
|
32
|
+
const a = record(payload, ['asset', 'decimals'], ['contract', 'amount_raw']);
|
|
33
|
+
if (typeof a.asset !== 'string' || !assets.includes(a.asset) || !integer(a.decimals) || a.decimals < 0 || a.decimals > 30 ||
|
|
34
|
+
(a.asset === 'native') !== (a.contract === undefined) || (a.contract !== undefined && (typeof a.contract !== 'string' || !/^[\x21-\x7e]{1,128}$/.test(a.contract))) ||
|
|
35
|
+
(a.amount_raw !== undefined && (!canonicalDecimal(a.amount_raw) || a.amount_raw.length > 78)))
|
|
36
|
+
fault();
|
|
37
|
+
const out = { asset: a.asset, decimals: a.decimals };
|
|
38
|
+
if (a.contract !== undefined)
|
|
39
|
+
out.contract = a.contract;
|
|
40
|
+
if (a.amount_raw !== undefined)
|
|
41
|
+
out.amountRaw = a.amount_raw;
|
|
42
|
+
return Object.freeze(out);
|
|
43
|
+
}
|
|
44
|
+
function fault() { throw new ClientFlowError('invalid_server_response'); }
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type CanonicalValue = string | number | boolean | null | readonly CanonicalValue[] | {
|
|
2
|
+
readonly [member: string]: CanonicalValue;
|
|
3
|
+
};
|
|
4
|
+
/**
|
|
5
|
+
* MaxCanonicalDepth bounds nesting. The Mode C request nests one object inside
|
|
6
|
+
* another and the proof payload is flat, so this bound is far above every
|
|
7
|
+
* accepted document and exists only to keep a hostile value from recursing.
|
|
8
|
+
*/
|
|
9
|
+
export declare const MaxCanonicalDepth = 8;
|
|
10
|
+
/** canonicalJSON returns the RFC 8785 canonical serialization of value. */
|
|
11
|
+
export declare function canonicalJSON(value: CanonicalValue): string;
|
|
12
|
+
/** canonicalJSONBytes returns the UTF-8 bytes canonicalJSON produces. */
|
|
13
|
+
export declare function canonicalJSONBytes(value: CanonicalValue): Uint8Array<ArrayBuffer>;
|
|
14
|
+
/**
|
|
15
|
+
* sha256HexLower is the lowercase hexadecimal SHA-256 digest Mode C uses for
|
|
16
|
+
* `request_hash`. Uppercase hexadecimal is a different value and the server
|
|
17
|
+
* rejects it, so the alphabet is fixed here.
|
|
18
|
+
*/
|
|
19
|
+
export declare function sha256HexLower(crypto: Crypto, bytes: Uint8Array<ArrayBuffer>): Promise<string>;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// RFC 8785 JSON Canonicalization Scheme (source §2.5). Mode C hashes the
|
|
2
|
+
// normalized OperationCreate and signs the closed proof payload over exactly
|
|
3
|
+
// these bytes, so the client canonicalizes locally and never accepts a
|
|
4
|
+
// server-, backend- or library-supplied serialization of either value.
|
|
5
|
+
//
|
|
6
|
+
// The implementation is deliberately closed rather than general: only the JSON
|
|
7
|
+
// value forms the two Mode C documents contain are accepted, and every other
|
|
8
|
+
// input is a local fault instead of a best-effort approximation. In particular
|
|
9
|
+
// a JSON number is accepted only as a safe integer, because the closed payload
|
|
10
|
+
// carries integer NumericDate seconds and nothing else; a fractional or
|
|
11
|
+
// out-of-range number would otherwise be canonicalized through a rounding rule
|
|
12
|
+
// this package has no requirement to own.
|
|
13
|
+
import { ClientFlowError } from "./auth.js";
|
|
14
|
+
/**
|
|
15
|
+
* MaxCanonicalDepth bounds nesting. The Mode C request nests one object inside
|
|
16
|
+
* another and the proof payload is flat, so this bound is far above every
|
|
17
|
+
* accepted document and exists only to keep a hostile value from recursing.
|
|
18
|
+
*/
|
|
19
|
+
export const MaxCanonicalDepth = 8;
|
|
20
|
+
/** canonicalJSON returns the RFC 8785 canonical serialization of value. */
|
|
21
|
+
export function canonicalJSON(value) {
|
|
22
|
+
return serialize(value, 0);
|
|
23
|
+
}
|
|
24
|
+
/** canonicalJSONBytes returns the UTF-8 bytes canonicalJSON produces. */
|
|
25
|
+
export function canonicalJSONBytes(value) {
|
|
26
|
+
return new TextEncoder().encode(canonicalJSON(value));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* sha256HexLower is the lowercase hexadecimal SHA-256 digest Mode C uses for
|
|
30
|
+
* `request_hash`. Uppercase hexadecimal is a different value and the server
|
|
31
|
+
* rejects it, so the alphabet is fixed here.
|
|
32
|
+
*/
|
|
33
|
+
export async function sha256HexLower(crypto, bytes) {
|
|
34
|
+
if (!crypto?.subtle || typeof crypto.subtle.digest !== "function")
|
|
35
|
+
throw new ClientFlowError("runtime_capability_unavailable");
|
|
36
|
+
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes));
|
|
37
|
+
let result = "";
|
|
38
|
+
for (const octet of digest)
|
|
39
|
+
result += lowerHex[octet >>> 4] + lowerHex[octet & 15];
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
const lowerHex = "0123456789abcdef";
|
|
43
|
+
function serialize(value, depth) {
|
|
44
|
+
if (depth > MaxCanonicalDepth)
|
|
45
|
+
throw new ClientFlowError("noncanonical_value");
|
|
46
|
+
if (value === null)
|
|
47
|
+
return "null";
|
|
48
|
+
switch (typeof value) {
|
|
49
|
+
case "boolean":
|
|
50
|
+
return value ? "true" : "false";
|
|
51
|
+
case "number":
|
|
52
|
+
return serializeNumber(value);
|
|
53
|
+
case "string":
|
|
54
|
+
return serializeString(value);
|
|
55
|
+
case "object":
|
|
56
|
+
break;
|
|
57
|
+
default:
|
|
58
|
+
throw new ClientFlowError("noncanonical_value");
|
|
59
|
+
}
|
|
60
|
+
if (Array.isArray(value)) {
|
|
61
|
+
const items = value;
|
|
62
|
+
return "[" + items.map((item) => serialize(item, depth + 1)).join(",") + "]";
|
|
63
|
+
}
|
|
64
|
+
const object = value;
|
|
65
|
+
const members = Object.keys(object).sort(compareUTF16);
|
|
66
|
+
const encoded = members.map((member) => {
|
|
67
|
+
const child = object[member];
|
|
68
|
+
if (child === undefined)
|
|
69
|
+
throw new ClientFlowError("noncanonical_value");
|
|
70
|
+
return serializeString(member) + ":" + serialize(child, depth + 1);
|
|
71
|
+
});
|
|
72
|
+
return "{" + encoded.join(",") + "}";
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* serializeNumber accepts only a safe integer. RFC 8785 serializes numbers
|
|
76
|
+
* through the ECMAScript Number-to-String algorithm, which `String` performs
|
|
77
|
+
* exactly for this range, including -0 as "0".
|
|
78
|
+
*/
|
|
79
|
+
function serializeNumber(value) {
|
|
80
|
+
if (!Number.isSafeInteger(value))
|
|
81
|
+
throw new ClientFlowError("noncanonical_value");
|
|
82
|
+
return String(value);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* serializeString uses the ECMAScript JSON string serialization RFC 8785
|
|
86
|
+
* §3.2.2.2 requires. A lone surrogate is not valid Unicode text, so it is
|
|
87
|
+
* refused here instead of being escaped into a document the server would then
|
|
88
|
+
* canonicalize differently.
|
|
89
|
+
*/
|
|
90
|
+
function serializeString(value) {
|
|
91
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
92
|
+
const unit = value.charCodeAt(index);
|
|
93
|
+
if (unit < 0xd800 || unit > 0xdfff)
|
|
94
|
+
continue;
|
|
95
|
+
const next = index + 1 < value.length ? value.charCodeAt(index + 1) : 0;
|
|
96
|
+
if (unit >= 0xdc00 || next < 0xdc00 || next > 0xdfff)
|
|
97
|
+
throw new ClientFlowError("noncanonical_value");
|
|
98
|
+
index += 1;
|
|
99
|
+
}
|
|
100
|
+
return JSON.stringify(value);
|
|
101
|
+
}
|
|
102
|
+
/** compareUTF16 orders member names by UTF-16 code unit as RFC 8785 §3.2.3 requires. */
|
|
103
|
+
function compareUTF16(left, right) {
|
|
104
|
+
if (left < right)
|
|
105
|
+
return -1;
|
|
106
|
+
return left > right ? 1 : 0;
|
|
107
|
+
}
|