@shuttle.technology/authorization 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +191 -0
- package/README.md +1124 -2
- package/dist/accountLists.d.ts +28 -0
- package/dist/accountLists.js +27 -0
- package/dist/aesGcmSiv.d.ts +14 -0
- package/dist/aesGcmSiv.js +160 -0
- package/dist/auth.d.ts +68 -0
- package/dist/auth.js +189 -0
- package/dist/balances.d.ts +31 -0
- package/dist/balances.js +44 -0
- package/dist/canonicalize.d.ts +19 -0
- package/dist/canonicalize.js +107 -0
- package/dist/client.d.ts +152 -0
- package/dist/client.js +781 -0
- package/dist/clientTypes.d.ts +56 -0
- package/dist/clientTypes.js +5 -0
- package/dist/configuration.d.ts +48 -0
- package/dist/configuration.js +72 -0
- package/dist/consentDebit.d.ts +36 -0
- package/dist/consentDebit.js +95 -0
- package/dist/credentials.d.ts +116 -0
- package/dist/credentials.js +315 -0
- package/dist/debitPermissionWire.d.ts +90 -0
- package/dist/debitPermissionWire.js +118 -0
- package/dist/debitPermissions.d.ts +39 -0
- package/dist/debitPermissions.js +117 -0
- package/dist/deviceKey.d.ts +13 -0
- package/dist/deviceKey.js +28 -0
- package/dist/deviceRecovery.d.ts +40 -0
- package/dist/deviceRecovery.js +143 -0
- package/dist/deviceShare.d.ts +9 -0
- package/dist/deviceShare.js +24 -0
- package/dist/deviceShareEnvelope.d.ts +13 -0
- package/dist/deviceShareEnvelope.js +139 -0
- package/dist/deviceShares.d.ts +91 -0
- package/dist/deviceShares.js +204 -0
- package/dist/errors.d.ts +280 -0
- package/dist/errors.js +85 -0
- package/dist/linkedChecks.d.ts +17 -0
- package/dist/linkedChecks.js +31 -0
- package/dist/linkedRecovery.d.ts +40 -0
- package/dist/linkedRecovery.js +49 -0
- package/dist/linkedRegistrations.d.ts +11 -0
- package/dist/linkedRegistrations.js +109 -0
- package/dist/linkedSendDisplay.d.ts +73 -0
- package/dist/linkedSendDisplay.js +84 -0
- package/dist/linkedWalletSignals.d.ts +22 -0
- package/dist/linkedWalletSignals.js +101 -0
- package/dist/linkedWallets.d.ts +401 -0
- package/dist/linkedWallets.js +436 -0
- package/dist/me.d.ts +27 -0
- package/dist/me.js +30 -0
- package/dist/mediatedOperation.d.ts +27 -0
- package/dist/mediatedOperation.js +68 -0
- package/dist/mobile.d.ts +64 -0
- package/dist/mobile.js +69 -0
- package/dist/modeC.d.ts +131 -0
- package/dist/modeC.js +248 -0
- package/dist/modeCLinked.d.ts +207 -0
- package/dist/modeCLinked.js +270 -0
- package/dist/pages.d.ts +22 -0
- package/dist/pages.js +45 -0
- package/dist/poll.d.ts +15 -0
- package/dist/poll.js +76 -0
- package/dist/redirects.d.ts +45 -0
- package/dist/redirects.js +108 -0
- package/dist/requestGrant.d.ts +169 -0
- package/dist/requestGrant.js +243 -0
- package/dist/sends.d.ts +89 -0
- package/dist/sends.js +127 -0
- package/dist/serviceGrants.d.ts +52 -0
- package/dist/serviceGrants.js +46 -0
- package/dist/slice4.d.ts +187 -0
- package/dist/slice4.js +299 -0
- package/dist/streams.d.ts +212 -0
- package/dist/streams.js +282 -0
- package/dist/trackedWallets.d.ts +53 -0
- package/dist/trackedWallets.js +104 -0
- package/dist/transferStatus.d.ts +45 -0
- package/dist/transferStatus.js +43 -0
- package/dist/walletCreateGrant.d.ts +62 -0
- package/dist/walletCreateGrant.js +89 -0
- package/dist/walletDisplayParts.d.ts +17 -0
- package/dist/walletDisplayParts.js +47 -0
- package/dist/walletEvents.d.ts +43 -0
- package/dist/walletEvents.js +97 -0
- package/dist/walletLimits.d.ts +65 -0
- package/dist/walletLimits.js +102 -0
- package/dist/walletOperations.d.ts +209 -0
- package/dist/walletOperations.js +317 -0
- package/dist/webRenewal.d.ts +92 -0
- package/dist/webRenewal.js +179 -0
- package/package.json +65 -3
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// The wire of a linked wallet's device-share recovery (wallet.recover naming
|
|
2
|
+
// linked_wallet_id): the closed create body, the display the person confirms
|
|
3
|
+
// with the passkey, and the result, the device share sealed to the new device
|
|
4
|
+
// key. deviceRecovery.ts runs it; walletOperations.ts reads it among the
|
|
5
|
+
// other wallet operations.
|
|
6
|
+
import { ClientFlowError, opaqueId, record } from "./auth.js";
|
|
7
|
+
import { bindingRevision, displayCommon, lowerHexBytes, lowerUUID, summaryOf, walletRecipient } from "./walletDisplayParts.js";
|
|
8
|
+
const compressedP256Key = /^(02|03)[0-9a-f]{64}$/;
|
|
9
|
+
const linkedNetwork = /^[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32}$/;
|
|
10
|
+
export function linkedRecoverCreate(ownerId, linkedWalletId, recipientPublicKey) {
|
|
11
|
+
return { kind: "wallet.recover", owner_type: "account", owner_id: ownerId, linked_wallet_id: linkedWalletId, payload: { recipient_public_key: recipientPublicKey } };
|
|
12
|
+
}
|
|
13
|
+
/** validLinkedRecoverCreate checks the members a recovery adds to the common ones validateUserWalletCreate checked. */
|
|
14
|
+
export function validLinkedRecoverCreate(common) {
|
|
15
|
+
if (common.kind !== "wallet.recover" || common.wallet_id !== undefined || !lowerUUID(common.linked_wallet_id))
|
|
16
|
+
return false;
|
|
17
|
+
const payload = common.payload;
|
|
18
|
+
return payload !== null && typeof payload === "object" && !Array.isArray(payload) && Object.keys(payload).length === 1 &&
|
|
19
|
+
typeof payload.recipient_public_key === "string" && compressedP256Key.test(payload.recipient_public_key);
|
|
20
|
+
}
|
|
21
|
+
export function parseLinkedRecoverDisplay(value) {
|
|
22
|
+
const display = record(value, ["kind", "owner_id", "requester_id", "result_recipient", "binding_source", "connection_id", "linked_wallet_id", "binding_revision", "network", "address", "recipient_public_key"], ["prepared_summary"]);
|
|
23
|
+
if (display.kind !== "wallet.recover" || display.binding_source !== "linked_wallet" || !lowerUUID(display.connection_id) || !lowerUUID(display.linked_wallet_id) ||
|
|
24
|
+
!bindingRevision(display.binding_revision) || typeof display.network !== "string" || !linkedNetwork.test(display.network) ||
|
|
25
|
+
typeof display.address !== "string" || !walletRecipient.test(display.address) || typeof display.recipient_public_key !== "string" || !compressedP256Key.test(display.recipient_public_key)) {
|
|
26
|
+
throw new ClientFlowError("invalid_server_response");
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
...displayCommon(display), kind: "wallet.recover", connectionId: display.connection_id, linkedWalletId: display.linked_wallet_id, bindingRevision: display.binding_revision,
|
|
30
|
+
network: display.network, address: display.address, recipientPublicKey: display.recipient_public_key, ...summaryOf(display.prepared_summary),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** The released result of a recovery: the wallet it is of and its device share, sealed to the new device key. */
|
|
34
|
+
export function parseRecoveredShareResult(requestId, value, expectedOperationId) {
|
|
35
|
+
const result = record(value, ["kind", "operation_id", "receipt_id", "linked_wallet_id", "device_share"]);
|
|
36
|
+
const share = record(result.device_share, ["ciphertext", "nonce", "ephemeral_public_key", "encryption_algorithm", "encryption_context"]);
|
|
37
|
+
if (!opaqueId(requestId) || result.kind !== "device_share_recovered" || result.operation_id !== expectedOperationId || !lowerUUID(result.receipt_id) || !lowerUUID(result.linked_wallet_id) ||
|
|
38
|
+
!lowerHexBytes(share.ciphertext) || !lowerHexBytes(share.nonce) || !lowerHexBytes(share.ephemeral_public_key) ||
|
|
39
|
+
typeof share.encryption_algorithm !== "string" || !/^[a-z0-9_-]{1,128}$/.test(share.encryption_algorithm) || !lowerHexBytes(share.encryption_context)) {
|
|
40
|
+
throw new ClientFlowError("invalid_server_response");
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
requestId,
|
|
44
|
+
result: {
|
|
45
|
+
operationId: expectedOperationId, kind: "device_share_recovered", receiptId: result.receipt_id, linkedWalletId: result.linked_wallet_id,
|
|
46
|
+
deviceShare: { ciphertext: share.ciphertext, nonce: share.nonce, ephemeralPublicKey: share.ephemeral_public_key, encryptionAlgorithm: share.encryption_algorithm, encryptionContext: share.encryption_context },
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { LinkedEventsOptions, LinkedRegistrationCreate, LinkedRegistrationListOptions, LinkedRegistrationOperation, LinkedRegistrationResult, LinkedStreamsRegistrationList } from './linkedWallets.ts';
|
|
2
|
+
export declare function validateLinkedRegistrationCreate(value: LinkedRegistrationCreate, ownerId: string): LinkedRegistrationCreate;
|
|
3
|
+
export declare function parseLinkedRegistrationOperationEnvelope(payload: unknown): {
|
|
4
|
+
requestId: string;
|
|
5
|
+
operation: LinkedRegistrationOperation;
|
|
6
|
+
};
|
|
7
|
+
export declare function parseLinkedRegistrationOperation(payload: unknown): LinkedRegistrationOperation;
|
|
8
|
+
export declare function parseLinkedRegistrationResult(payload: unknown, operationId: string): LinkedRegistrationResult;
|
|
9
|
+
export declare function linkedRegistrationsPath(linkedWalletId: string, options?: LinkedRegistrationListOptions): string;
|
|
10
|
+
export declare function linkedEventsPath(linkedWalletId: string, options?: LinkedEventsOptions): string;
|
|
11
|
+
export declare function parseLinkedStreamsRegistrationList(payload: unknown, linkedWalletId: string): LinkedStreamsRegistrationList;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// The linked-wallet registrations for tracking, as Authorization frames them:
|
|
2
|
+
// the closed create body, the operation and its result, and the paths and
|
|
3
|
+
// pages of a linked wallet's registrations and events. linkedWallets.ts calls
|
|
4
|
+
// them and re-exports them.
|
|
5
|
+
import { ClientFlowError, canonicalDecimal, deadlineAfter, integer, isNextAction, opaqueId, record, rfc3339UTC } from './auth.js';
|
|
6
|
+
import { appendEventTypes, MaxStreamsCursorLength, MaxStreamsPageLimit } from './streams.js';
|
|
7
|
+
import { activityTypes, assets, digest, fault, https, network, recipient, requestUUID, safeRevision, text, uuid } from './linkedChecks.js';
|
|
8
|
+
export function validateLinkedRegistrationCreate(value, ownerId) {
|
|
9
|
+
const r = record(value, ['kind', 'owner_type', 'owner_id', 'linked_wallet_id', 'payload']);
|
|
10
|
+
const p = record(r.payload, ['network'], ['assets', 'activity_types']);
|
|
11
|
+
if (r.kind !== 'streams.register' || r.owner_type !== 'account' || !opaqueId(r.owner_id) || r.owner_id !== ownerId || !uuid(r.linked_wallet_id) || !network(p.network) || (p.assets !== undefined && !assets(p.assets)) || (p.activity_types !== undefined && !activityTypes(p.activity_types)))
|
|
12
|
+
throw new ClientFlowError('invalid_operation_create');
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
export function parseLinkedRegistrationOperationEnvelope(payload) {
|
|
16
|
+
const r = record(payload, ['request_id', 'operation']);
|
|
17
|
+
if (!opaqueId(r.request_id))
|
|
18
|
+
fault();
|
|
19
|
+
return { requestId: r.request_id, operation: parseLinkedRegistrationOperation(r.operation) };
|
|
20
|
+
}
|
|
21
|
+
export function parseLinkedRegistrationOperation(payload) {
|
|
22
|
+
const r = record(payload, ['id', 'version', 'kind', 'state', 'effect_state', 'authorization_basis', 'owner_type', 'owner_id', 'requester_id', 'binding_source', 'connection_id', 'linked_wallet_id', 'binding_revision', 'request_fingerprint', 'operation_fingerprint', 'display', 'created_at', 'expires_at', 'next_action'], ['confirmation_location']);
|
|
23
|
+
if (!opaqueId(r.id) || !canonicalDecimal(r.version) || r.kind !== 'streams.register' || r.owner_type !== 'account' || !opaqueId(r.owner_id) || !opaqueId(r.requester_id) || r.binding_source !== 'linked_wallet' || !uuid(r.connection_id) || !uuid(r.linked_wallet_id) || !safeRevision(r.binding_revision) || !digest(r.request_fingerprint) || !digest(r.operation_fingerprint) || !rfc3339UTC(r.created_at) || !rfc3339UTC(r.expires_at) || !deadlineAfter(r.created_at, r.expires_at) || !isNextAction(r.next_action) || !['awaiting_confirmation', 'authorized', 'dispatching', 'outcome_unknown', 'succeeded', 'failed', 'denied', 'canceled', 'expired'].includes(r.state) || !['no_effect', 'possible', 'known'].includes(r.effect_state) || !['', 'user_confirmed', 'session_authorized'].includes(r.authorization_basis) || (r.confirmation_location !== undefined && !https(r.confirmation_location)))
|
|
24
|
+
fault();
|
|
25
|
+
const purged = typeof r.display === 'object' && r.display !== null && 'material_purged' in r.display;
|
|
26
|
+
const d = record(r.display, ['kind', 'owner_id', 'requester_id', 'result_recipient', ...(purged ? ['material_purged'] : ['binding_source', 'connection_id', 'linked_wallet_id', 'binding_revision', 'assets', 'network', 'address', 'streams_mapping_id', 'streams_mapping_version', 'target_customer_id', 'target_project_id', 'target_environment_id'])]);
|
|
27
|
+
if (d.kind !== r.kind || d.owner_id !== r.owner_id || d.requester_id !== r.requester_id)
|
|
28
|
+
fault();
|
|
29
|
+
const common = { kind: 'streams.register', ownerId: r.owner_id, requesterId: r.requester_id, resultRecipient: recipient(d.result_recipient) };
|
|
30
|
+
let display;
|
|
31
|
+
if (purged) {
|
|
32
|
+
if (d.material_purged !== true)
|
|
33
|
+
fault();
|
|
34
|
+
display = { ...common, materialPurged: true };
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
if (d.binding_source !== r.binding_source || d.connection_id !== r.connection_id || d.linked_wallet_id !== r.linked_wallet_id || d.binding_revision !== r.binding_revision || !assets(d.assets) || !network(d.network) || !text(d.address, 256) || !safeRevision(d.streams_mapping_version) || ![d.streams_mapping_id, d.target_customer_id, d.target_project_id, d.target_environment_id].every(opaqueId))
|
|
38
|
+
fault();
|
|
39
|
+
display = { ...common, bindingSource: 'linked_wallet', connectionId: r.connection_id, linkedWalletId: r.linked_wallet_id, bindingRevision: r.binding_revision, assets: Object.freeze([...d.assets]), network: d.network, address: d.address, streamsMappingId: d.streams_mapping_id, streamsMappingVersion: d.streams_mapping_version, targetCustomerId: d.target_customer_id, targetProjectId: d.target_project_id, targetEnvironmentId: d.target_environment_id };
|
|
40
|
+
}
|
|
41
|
+
const operation = { id: r.id, version: r.version, kind: 'streams.register', state: r.state, effectState: r.effect_state, authorizationBasis: r.authorization_basis, ownerType: 'account', ownerId: r.owner_id, requesterId: r.requester_id, bindingSource: 'linked_wallet', connectionId: r.connection_id, linkedWalletId: r.linked_wallet_id, bindingRevision: r.binding_revision, requestFingerprint: r.request_fingerprint, operationFingerprint: r.operation_fingerprint, display, createdAt: r.created_at, expiresAt: r.expires_at, nextAction: r.next_action };
|
|
42
|
+
if (r.confirmation_location !== undefined)
|
|
43
|
+
operation.confirmationLocation = r.confirmation_location;
|
|
44
|
+
return operation;
|
|
45
|
+
}
|
|
46
|
+
export function parseLinkedRegistrationResult(payload, operationId) {
|
|
47
|
+
const r = record(payload, ['request_id', 'result']);
|
|
48
|
+
const v = record(r.result, ['kind', 'operation_id', 'binding_source', 'connection_id', 'linked_wallet_id', 'binding_revision', 'source_registration_id', 'registration_digest', 'receipt_id']);
|
|
49
|
+
if (!opaqueId(r.request_id) || !opaqueId(v.operation_id) || v.operation_id !== operationId || v.kind !== 'streams_registration' || v.binding_source !== 'linked_wallet' || !uuid(v.connection_id) || !uuid(v.linked_wallet_id) || !safeRevision(v.binding_revision) || !text(v.source_registration_id, 128) || !text(v.receipt_id, 128) || !digest(v.registration_digest))
|
|
50
|
+
fault();
|
|
51
|
+
return { requestId: r.request_id, result: { kind: 'streams_registration', operationId: v.operation_id, bindingSource: 'linked_wallet', connectionId: v.connection_id, linkedWalletId: v.linked_wallet_id, bindingRevision: v.binding_revision, sourceRegistrationId: v.source_registration_id, registrationDigest: v.registration_digest, receiptId: v.receipt_id } };
|
|
52
|
+
}
|
|
53
|
+
export function linkedRegistrationsPath(linkedWalletId, options = {}) {
|
|
54
|
+
requestUUID(linkedWalletId);
|
|
55
|
+
const declared = record(options, [], ['afterRegistrationId', 'limit', 'signal']);
|
|
56
|
+
const query = new URLSearchParams();
|
|
57
|
+
if (declared.afterRegistrationId !== undefined) {
|
|
58
|
+
requestUUID(declared.afterRegistrationId);
|
|
59
|
+
query.set('after_registration_id', declared.afterRegistrationId);
|
|
60
|
+
}
|
|
61
|
+
readLimit(query, declared.limit);
|
|
62
|
+
return linkedReadPath(linkedWalletId, 'registrations', query);
|
|
63
|
+
}
|
|
64
|
+
export function linkedEventsPath(linkedWalletId, options = {}) {
|
|
65
|
+
requestUUID(linkedWalletId);
|
|
66
|
+
const declared = record(options, [], ['registrationId', 'cursor', 'limit', 'eventTypes', 'signal']);
|
|
67
|
+
const query = new URLSearchParams();
|
|
68
|
+
if (declared.registrationId !== undefined) {
|
|
69
|
+
requestUUID(declared.registrationId);
|
|
70
|
+
query.set('registration_id', declared.registrationId);
|
|
71
|
+
}
|
|
72
|
+
if (declared.cursor !== undefined) {
|
|
73
|
+
if (typeof declared.cursor !== 'string' || declared.cursor.length < 1 || declared.cursor.length > MaxStreamsCursorLength)
|
|
74
|
+
throw new ClientFlowError('invalid_streams_request');
|
|
75
|
+
query.set('cursor', declared.cursor);
|
|
76
|
+
}
|
|
77
|
+
readLimit(query, declared.limit);
|
|
78
|
+
appendEventTypes(query, declared.eventTypes);
|
|
79
|
+
return linkedReadPath(linkedWalletId, 'events', query);
|
|
80
|
+
}
|
|
81
|
+
export function parseLinkedStreamsRegistrationList(payload, linkedWalletId) {
|
|
82
|
+
const root = record(payload, ['items', 'has_more'], ['next_after_registration_id']);
|
|
83
|
+
if (!Array.isArray(root.items) || root.items.length > MaxStreamsPageLimit || typeof root.has_more !== 'boolean' || root.has_more !== Object.hasOwn(root, 'next_after_registration_id'))
|
|
84
|
+
fault();
|
|
85
|
+
const items = root.items.map(item => parseLinkedStreamsRegistration(item, linkedWalletId));
|
|
86
|
+
if (items.some((item, index) => index > 0 && items[index - 1].registrationId >= item.registrationId))
|
|
87
|
+
fault();
|
|
88
|
+
if (root.has_more && (!uuid(root.next_after_registration_id) || items.length === 0 || root.next_after_registration_id !== items.at(-1).registrationId))
|
|
89
|
+
fault();
|
|
90
|
+
const page = { items: Object.freeze(items), hasMore: root.has_more };
|
|
91
|
+
return Object.freeze(root.has_more ? { ...page, nextAfterRegistrationId: root.next_after_registration_id } : page);
|
|
92
|
+
}
|
|
93
|
+
function parseLinkedStreamsRegistration(payload, linkedWalletId) {
|
|
94
|
+
const r = record(payload, ['registration_id', 'binding_source', 'connection_id', 'linked_wallet_id', 'binding_revision', 'source_registration_id', 'network', 'address', 'assets', 'status', 'effective_from', 'revision', 'source_version']);
|
|
95
|
+
if (!uuid(r.registration_id) || r.binding_source !== 'linked_wallet' || !uuid(r.connection_id) || !uuid(r.linked_wallet_id) || r.linked_wallet_id !== linkedWalletId || !safeRevision(r.binding_revision) || !opaqueId(r.source_registration_id) || !network(r.network) || typeof r.address !== 'string' || !/^[A-Za-z0-9_:-]{1,256}$/.test(r.address) || !assets(r.assets) || !['pending', 'active'].includes(r.status) || typeof r.effective_from !== 'string' || new TextEncoder().encode(r.effective_from).length < 1 || new TextEncoder().encode(r.effective_from).length > 256 || !integer(r.revision) || r.revision < 1 || typeof r.source_version !== 'string' || !/^[0-9]+\.[0-9]+$/.test(r.source_version))
|
|
96
|
+
fault();
|
|
97
|
+
return Object.freeze({ registrationId: r.registration_id, bindingSource: 'linked_wallet', connectionId: r.connection_id, linkedWalletId: r.linked_wallet_id, bindingRevision: r.binding_revision, sourceRegistrationId: r.source_registration_id, network: r.network, address: r.address, assets: Object.freeze([...r.assets]), status: r.status, effectiveFrom: r.effective_from, revision: r.revision, sourceVersion: r.source_version });
|
|
98
|
+
}
|
|
99
|
+
function linkedReadPath(id, kind, query) {
|
|
100
|
+
const path = `/v1/linked-wallets/${id}/${kind}`;
|
|
101
|
+
return query.size === 0 ? path : `${path}?${query.toString()}`;
|
|
102
|
+
}
|
|
103
|
+
function readLimit(query, value) {
|
|
104
|
+
if (value === undefined)
|
|
105
|
+
return;
|
|
106
|
+
if (!integer(value) || value < 1 || value > MaxStreamsPageLimit)
|
|
107
|
+
throw new ClientFlowError('invalid_streams_request');
|
|
108
|
+
query.set('limit', String(value));
|
|
109
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { type DisplayCommon } from "./walletDisplayParts.ts";
|
|
2
|
+
/**
|
|
3
|
+
* LinkedWalletIdentity names a wallet created through the provider in the
|
|
4
|
+
* own-wallet system. It has no walletId: the operation names it by this
|
|
5
|
+
* identity instead.
|
|
6
|
+
*/
|
|
7
|
+
export interface LinkedWalletIdentity {
|
|
8
|
+
connectionId: string;
|
|
9
|
+
linkedWalletId: string;
|
|
10
|
+
bindingRevision: string;
|
|
11
|
+
}
|
|
12
|
+
/** What a send from a linked wallet may move: the network's own coin, or a token the own-wallet system sends there. */
|
|
13
|
+
export declare const LinkedSendAssets: readonly ["native", "usdt", "usdc"];
|
|
14
|
+
export type LinkedSendAsset = (typeof LinkedSendAssets)[number];
|
|
15
|
+
export declare function linkedSendAsset(value: unknown): value is LinkedSendAsset;
|
|
16
|
+
/**
|
|
17
|
+
* What replaces a send the network has not included, on an EVM network:
|
|
18
|
+
* "speed_up" sends the same transfer again with a higher fee, "cancel" sends
|
|
19
|
+
* nothing to the wallet's own address in its place. Either way at most one of
|
|
20
|
+
* the two transactions is ever included.
|
|
21
|
+
*/
|
|
22
|
+
export declare const ReplaceActions: readonly ["speed_up", "cancel"];
|
|
23
|
+
export type ReplaceAction = (typeof ReplaceActions)[number];
|
|
24
|
+
export declare function replaceAction(value: unknown): value is ReplaceAction;
|
|
25
|
+
/** The longest comment a transfer carries, in bytes of UTF-8: what one TON comment holds. */
|
|
26
|
+
export declare const MaxTransferCommentBytes = 120;
|
|
27
|
+
/**
|
|
28
|
+
* transferComment says whether a value is a text a send may carry to its
|
|
29
|
+
* recipient: 1 to 120 bytes of UTF-8 with no control character, line
|
|
30
|
+
* separator or character that reorders text. The provider applies the same
|
|
31
|
+
* rule, and the owner confirms the comment exactly as it is shown.
|
|
32
|
+
*/
|
|
33
|
+
export declare function transferComment(value: unknown): value is string;
|
|
34
|
+
/**
|
|
35
|
+
* LinkedSendDisplay is what the owner confirms for a send from a linked
|
|
36
|
+
* wallet. address, preparedSummary and feeEstimate appear once the own-wallet
|
|
37
|
+
* system prepared the transfer (LinkedWallets.prepare()). feeEstimate is "estimated"
|
|
38
|
+
* with estimatedFeeRaw, the network fee in minimal units of the network's own
|
|
39
|
+
* coin, paid on top of the amount; or "unavailable" when that system could
|
|
40
|
+
* estimate none, which is never a fee of zero. It is an estimate: the
|
|
41
|
+
* confirmation does not bound the fee.
|
|
42
|
+
*
|
|
43
|
+
* A replacement carries replacesOperationId, the send whose transfer it
|
|
44
|
+
* replaces, and replaceAction. network, asset, recipient and amountRaw are
|
|
45
|
+
* then what the replacing transaction itself moves: for a cancel nothing
|
|
46
|
+
* ("0") of the network's own coin to the wallet's own address.
|
|
47
|
+
*
|
|
48
|
+
* comment is the text the transfer carries to its recipient, on TON; it is
|
|
49
|
+
* absent for a send without one.
|
|
50
|
+
*/
|
|
51
|
+
export type LinkedSendDisplay = DisplayCommon & LinkedWalletIdentity & {
|
|
52
|
+
kind: "wallet.send";
|
|
53
|
+
network: string;
|
|
54
|
+
asset: string;
|
|
55
|
+
recipient: string;
|
|
56
|
+
amountRaw: string;
|
|
57
|
+
address?: string;
|
|
58
|
+
preparedSummary?: string;
|
|
59
|
+
feeEstimate?: "estimated" | "unavailable";
|
|
60
|
+
estimatedFeeRaw?: string;
|
|
61
|
+
replacesOperationId?: string;
|
|
62
|
+
replaceAction?: ReplaceAction;
|
|
63
|
+
comment?: string;
|
|
64
|
+
};
|
|
65
|
+
export declare function parseLinkedSendDisplay(value: unknown): LinkedSendDisplay;
|
|
66
|
+
/**
|
|
67
|
+
* MediatedLinkedSendDisplay is the safe display of a Mode C transfer from a
|
|
68
|
+
* linked wallet that a service forwarded on the person's signed request. It
|
|
69
|
+
* freezes no result recipient and is never a replacement.
|
|
70
|
+
*/
|
|
71
|
+
export type MediatedLinkedSendDisplay = Omit<LinkedSendDisplay, "resultRecipient" | "replacesOperationId" | "replaceAction">;
|
|
72
|
+
/** parseMediatedLinkedSendDisplay reads exactly the display of a forwarded linked transfer. */
|
|
73
|
+
export declare function parseMediatedLinkedSendDisplay(value: unknown): MediatedLinkedSendDisplay;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// The safe display of a send from a linked wallet: a wallet created through
|
|
2
|
+
// the provider that lives in the own-wallet system. The same display serves
|
|
3
|
+
// the replacement of such a send that the network has not included.
|
|
4
|
+
import { ClientFlowError, opaqueId, record } from "./auth.js";
|
|
5
|
+
import { bindingRevision, displayCommon, lowerUUID, rawQuantity, summaryOf, walletRecipient } from "./walletDisplayParts.js";
|
|
6
|
+
const linkedNetwork = /^[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32}$/;
|
|
7
|
+
/** What a send from a linked wallet may move: the network's own coin, or a token the own-wallet system sends there. */
|
|
8
|
+
export const LinkedSendAssets = ["native", "usdt", "usdc"];
|
|
9
|
+
export function linkedSendAsset(value) {
|
|
10
|
+
return typeof value === "string" && LinkedSendAssets.includes(value);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* What replaces a send the network has not included, on an EVM network:
|
|
14
|
+
* "speed_up" sends the same transfer again with a higher fee, "cancel" sends
|
|
15
|
+
* nothing to the wallet's own address in its place. Either way at most one of
|
|
16
|
+
* the two transactions is ever included.
|
|
17
|
+
*/
|
|
18
|
+
export const ReplaceActions = ["speed_up", "cancel"];
|
|
19
|
+
export function replaceAction(value) {
|
|
20
|
+
return typeof value === "string" && ReplaceActions.includes(value);
|
|
21
|
+
}
|
|
22
|
+
/** The longest comment a transfer carries, in bytes of UTF-8: what one TON comment holds. */
|
|
23
|
+
export const MaxTransferCommentBytes = 120;
|
|
24
|
+
// Control characters, line and paragraph separators, characters that reorder
|
|
25
|
+
// the text around them, and halves of a character.
|
|
26
|
+
const notInComment = /[\p{Cc}\p{Zl}\p{Zp}\p{Bidi_Control}\p{Cs}]/u;
|
|
27
|
+
const utf8 = new TextEncoder();
|
|
28
|
+
/**
|
|
29
|
+
* transferComment says whether a value is a text a send may carry to its
|
|
30
|
+
* recipient: 1 to 120 bytes of UTF-8 with no control character, line
|
|
31
|
+
* separator or character that reorders text. The provider applies the same
|
|
32
|
+
* rule, and the owner confirms the comment exactly as it is shown.
|
|
33
|
+
*/
|
|
34
|
+
export function transferComment(value) {
|
|
35
|
+
return typeof value === "string" && value.length > 0 && value.length <= MaxTransferCommentBytes && !notInComment.test(value) && utf8.encode(value).length <= MaxTransferCommentBytes;
|
|
36
|
+
}
|
|
37
|
+
export function parseLinkedSendDisplay(value) {
|
|
38
|
+
const display = record(value, [...linkedSendMembers, "result_recipient"], [...linkedSendOptional, "replaces_operation_id", "replace_action"]);
|
|
39
|
+
const replaces = display.replaces_operation_id !== undefined;
|
|
40
|
+
if (!linkedSendValid(display, replaces) || replaces !== (display.replace_action !== undefined) || (replaces && (!opaqueId(display.replaces_operation_id) || !replaceAction(display.replace_action))) ||
|
|
41
|
+
// A replacement carries no comment.
|
|
42
|
+
(replaces && display.comment !== undefined)) {
|
|
43
|
+
throw new ClientFlowError("invalid_server_response");
|
|
44
|
+
}
|
|
45
|
+
const parsed = { ...displayCommon(display), ...linkedSendParts(display) };
|
|
46
|
+
if (replaces) {
|
|
47
|
+
parsed.replacesOperationId = display.replaces_operation_id;
|
|
48
|
+
parsed.replaceAction = display.replace_action;
|
|
49
|
+
}
|
|
50
|
+
return parsed;
|
|
51
|
+
}
|
|
52
|
+
/** parseMediatedLinkedSendDisplay reads exactly the display of a forwarded linked transfer. */
|
|
53
|
+
export function parseMediatedLinkedSendDisplay(value) {
|
|
54
|
+
const display = record(value, linkedSendMembers, linkedSendOptional);
|
|
55
|
+
if (!linkedSendValid(display, false) || !opaqueId(display.owner_id) || !opaqueId(display.requester_id))
|
|
56
|
+
throw new ClientFlowError("invalid_server_response");
|
|
57
|
+
return { ownerId: display.owner_id, requesterId: display.requester_id, ...linkedSendParts(display) };
|
|
58
|
+
}
|
|
59
|
+
const linkedSendMembers = ["kind", "owner_id", "requester_id", "binding_source", "connection_id", "linked_wallet_id", "binding_revision", "network", "asset", "recipient", "amount_raw"];
|
|
60
|
+
const linkedSendOptional = ["address", "prepared_summary", "fee_estimate", "estimated_fee_raw", "comment"];
|
|
61
|
+
function linkedSendValid(display, replaces) {
|
|
62
|
+
return display.kind === "wallet.send" && display.binding_source === "linked_wallet" && lowerUUID(display.connection_id) && lowerUUID(display.linked_wallet_id) && bindingRevision(display.binding_revision) &&
|
|
63
|
+
typeof display.network === "string" && linkedNetwork.test(display.network) && linkedSendAsset(display.asset) && typeof display.recipient === "string" && walletRecipient.test(display.recipient) &&
|
|
64
|
+
// Only a replacement moves nothing: a cancel, or a cancel sent again.
|
|
65
|
+
rawQuantity(display.amount_raw, !replaces) && (display.address === undefined || (typeof display.address === "string" && walletRecipient.test(display.address))) &&
|
|
66
|
+
(display.fee_estimate === undefined || display.fee_estimate === "estimated" || display.fee_estimate === "unavailable") &&
|
|
67
|
+
(display.fee_estimate === "estimated") === (display.estimated_fee_raw !== undefined) && (display.estimated_fee_raw === undefined || rawQuantity(display.estimated_fee_raw, false)) &&
|
|
68
|
+
(display.comment === undefined || transferComment(display.comment));
|
|
69
|
+
}
|
|
70
|
+
function linkedSendParts(display) {
|
|
71
|
+
const parts = {
|
|
72
|
+
kind: "wallet.send", connectionId: display.connection_id, linkedWalletId: display.linked_wallet_id, bindingRevision: display.binding_revision,
|
|
73
|
+
network: display.network, asset: display.asset, recipient: display.recipient, amountRaw: display.amount_raw, ...summaryOf(display.prepared_summary),
|
|
74
|
+
};
|
|
75
|
+
if (display.address !== undefined)
|
|
76
|
+
parts.address = display.address;
|
|
77
|
+
if (display.fee_estimate !== undefined)
|
|
78
|
+
parts.feeEstimate = display.fee_estimate;
|
|
79
|
+
if (display.estimated_fee_raw !== undefined)
|
|
80
|
+
parts.estimatedFeeRaw = display.estimated_fee_raw;
|
|
81
|
+
if (display.comment !== undefined)
|
|
82
|
+
parts.comment = display.comment;
|
|
83
|
+
return parts;
|
|
84
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** The most linked wallets one stream names. */
|
|
2
|
+
export declare const MaxSignalWallets = 32;
|
|
3
|
+
/** Why a stream ended: its time ran out, the server is restarting, or the connection closed. */
|
|
4
|
+
export type LinkedWalletSignalEnd = 'expired' | 'server_draining' | 'connection_closed';
|
|
5
|
+
export interface LinkedWalletSignalHandlers {
|
|
6
|
+
/**
|
|
7
|
+
* The stream is live. Read every wallet once now: whatever happened before
|
|
8
|
+
* this moment is in that read, whatever happens after it is signalled.
|
|
9
|
+
* expiresAt is when the server ends the stream, in epoch milliseconds.
|
|
10
|
+
*/
|
|
11
|
+
onOpen?: (expiresAt: number) => void;
|
|
12
|
+
/** These linked wallets have new events. Read them now. */
|
|
13
|
+
onWallets: (linkedWalletIds: readonly string[]) => void;
|
|
14
|
+
signal?: AbortSignal;
|
|
15
|
+
}
|
|
16
|
+
export declare function linkedSignalsPath(linkedWalletIds: readonly string[]): string;
|
|
17
|
+
/**
|
|
18
|
+
* Reads one stream to its end and reports what it says. It resolves with the
|
|
19
|
+
* reason the stream ended and rejects when the answer is not this protocol or
|
|
20
|
+
* the connection fails. Aborting the signal rejects with the abort reason.
|
|
21
|
+
*/
|
|
22
|
+
export declare function readLinkedWalletSignals(body: ReadableStream<Uint8Array>, linkedWalletIds: readonly string[], handlers: LinkedWalletSignalHandlers): Promise<LinkedWalletSignalEnd>;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Live signals for linked wallets. Authorization holds one long-lived answer
|
|
2
|
+
// open and names on it the linked wallets that have new events. A signal
|
|
3
|
+
// carries no event: the wallets it names are read through events(), which
|
|
4
|
+
// authorizes every read. A stream lives no longer than an access token does;
|
|
5
|
+
// when it ends, a new one is opened with the session's current token.
|
|
6
|
+
import { ClientFlowError, record, rfc3339UTC } from './auth.js';
|
|
7
|
+
/** The most linked wallets one stream names. */
|
|
8
|
+
export const MaxSignalWallets = 32;
|
|
9
|
+
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
10
|
+
// One frame is a short line or two; anything longer is not this protocol.
|
|
11
|
+
const maxFrameLength = 4096;
|
|
12
|
+
export function linkedSignalsPath(linkedWalletIds) {
|
|
13
|
+
if (!Array.isArray(linkedWalletIds) || linkedWalletIds.length < 1 || linkedWalletIds.length > MaxSignalWallets ||
|
|
14
|
+
new Set(linkedWalletIds).size !== linkedWalletIds.length || linkedWalletIds.some(id => typeof id !== 'string' || !uuidPattern.test(id))) {
|
|
15
|
+
throw new ClientFlowError('invalid_linked_wallet_request');
|
|
16
|
+
}
|
|
17
|
+
const query = new URLSearchParams();
|
|
18
|
+
for (const id of linkedWalletIds)
|
|
19
|
+
query.append('linked_wallet_id', id);
|
|
20
|
+
return `/v1/linked-wallets/signals?${query.toString()}`;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Reads one stream to its end and reports what it says. It resolves with the
|
|
24
|
+
* reason the stream ended and rejects when the answer is not this protocol or
|
|
25
|
+
* the connection fails. Aborting the signal rejects with the abort reason.
|
|
26
|
+
*/
|
|
27
|
+
export async function readLinkedWalletSignals(body, linkedWalletIds, handlers) {
|
|
28
|
+
const named = new Set(linkedWalletIds);
|
|
29
|
+
const reader = body.getReader();
|
|
30
|
+
const decoder = new TextDecoder('utf-8', { fatal: true });
|
|
31
|
+
const signal = handlers.signal;
|
|
32
|
+
// Aborting ends the read whoever opened the stream: a read in flight then
|
|
33
|
+
// comes back as done, and the abort reason is what the caller gets.
|
|
34
|
+
const abort = () => { reader.cancel(signal?.reason).catch(() => undefined); };
|
|
35
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
36
|
+
let pending = '';
|
|
37
|
+
let open = false;
|
|
38
|
+
try {
|
|
39
|
+
for (;;) {
|
|
40
|
+
if (signal?.aborted)
|
|
41
|
+
throw signal.reason;
|
|
42
|
+
const { value, done } = await reader.read();
|
|
43
|
+
if (signal?.aborted)
|
|
44
|
+
throw signal.reason;
|
|
45
|
+
if (done) {
|
|
46
|
+
if (pending.length > 0)
|
|
47
|
+
fault(); // cut inside a frame
|
|
48
|
+
return 'connection_closed';
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
pending += decoder.decode(value, { stream: true });
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
fault();
|
|
55
|
+
}
|
|
56
|
+
for (let end = pending.indexOf('\n\n'); end >= 0; end = pending.indexOf('\n\n')) {
|
|
57
|
+
const frame = pending.slice(0, end);
|
|
58
|
+
pending = pending.slice(end + 2);
|
|
59
|
+
if (frame.startsWith(':'))
|
|
60
|
+
continue; // a comment keeps the connection alive
|
|
61
|
+
const parsed = /^event: (open|wallets|close)\ndata: (\{[^\n]*\})$/.exec(frame);
|
|
62
|
+
if (!parsed)
|
|
63
|
+
fault();
|
|
64
|
+
let data;
|
|
65
|
+
try {
|
|
66
|
+
data = JSON.parse(parsed[2]);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
fault();
|
|
70
|
+
}
|
|
71
|
+
if (parsed[1] === 'open') {
|
|
72
|
+
const root = record(data, ['expires_at']);
|
|
73
|
+
if (open || !rfc3339UTC(root.expires_at))
|
|
74
|
+
fault();
|
|
75
|
+
open = true;
|
|
76
|
+
handlers.onOpen?.(Date.parse(root.expires_at));
|
|
77
|
+
}
|
|
78
|
+
else if (parsed[1] === 'wallets') {
|
|
79
|
+
const root = record(data, ['linked_wallet_ids']);
|
|
80
|
+
const ids = root.linked_wallet_ids;
|
|
81
|
+
if (!open || !Array.isArray(ids) || ids.length < 1 || new Set(ids).size !== ids.length || ids.some(id => typeof id !== 'string' || !named.has(id)))
|
|
82
|
+
fault();
|
|
83
|
+
handlers.onWallets(Object.freeze([...ids]));
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
const root = record(data, ['reason']);
|
|
87
|
+
if (root.reason !== 'expired' && root.reason !== 'server_draining')
|
|
88
|
+
fault();
|
|
89
|
+
return root.reason;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (pending.length > maxFrameLength)
|
|
93
|
+
fault();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
signal?.removeEventListener('abort', abort);
|
|
98
|
+
reader.cancel().catch(() => undefined);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function fault() { throw new ClientFlowError('invalid_server_response'); }
|