@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,89 @@
|
|
|
1
|
+
// A wallet's creation that opens the wallet to a service in the same passkey
|
|
2
|
+
// confirmation (owner decision of 28 October 2026): the read grant
|
|
3
|
+
// service_grant.set would give, named beside the wallet. The person confirms
|
|
4
|
+
// once, on a page that names the service and what it may see; the provider
|
|
5
|
+
// sets the grant when the creation's result is read. A grant that cannot be
|
|
6
|
+
// set leaves the wallet as it is, and the result says why: the app then asks
|
|
7
|
+
// for the grant alone (createServiceGrant), with its own passkey confirmation.
|
|
8
|
+
import { ClientFlowError, opaqueId, record } from "./auth.js";
|
|
9
|
+
import { isAuthorizationErrorCode } from "./errors.js";
|
|
10
|
+
import { printableReference, serviceScopes } from "./slice4.js";
|
|
11
|
+
import { lowerUUID } from "./walletDisplayParts.js";
|
|
12
|
+
const order = ["wallet:read", "operation:read", "streams:read"];
|
|
13
|
+
/** validWalletCreateGrant reads the wire member as the provider takes it: one of its services, one to three scopes, an optional printable reference. */
|
|
14
|
+
export function validWalletCreateGrant(value) {
|
|
15
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
16
|
+
return false;
|
|
17
|
+
const grant = value;
|
|
18
|
+
const members = Object.keys(grant);
|
|
19
|
+
return members.every((member) => member === "service_principal_id" || member === "scopes" || member === "external_ref") &&
|
|
20
|
+
opaqueId(grant.service_principal_id) && serviceScopes(grant.scopes) && grant.scopes.length >= 1 && (grant.external_ref === undefined || printableReference(grant.external_ref));
|
|
21
|
+
}
|
|
22
|
+
function grantWire(input) {
|
|
23
|
+
let declared;
|
|
24
|
+
try {
|
|
25
|
+
declared = record(input, ["servicePrincipalId", "scopes"], ["externalRef"]);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
throw new ClientFlowError("invalid_operation_create");
|
|
29
|
+
}
|
|
30
|
+
// The scopes go in the provider's order, so a repeat of the request is the same request.
|
|
31
|
+
const raw = declared.scopes;
|
|
32
|
+
const scopes = serviceScopes(raw) ? order.filter((scope) => raw.includes(scope)) : raw;
|
|
33
|
+
const wire = { service_principal_id: declared.servicePrincipalId, scopes: scopes };
|
|
34
|
+
if (declared.externalRef !== undefined)
|
|
35
|
+
wire.external_ref = declared.externalRef;
|
|
36
|
+
if (!validWalletCreateGrant(wire))
|
|
37
|
+
throw new ClientFlowError("invalid_operation_create");
|
|
38
|
+
return wire;
|
|
39
|
+
}
|
|
40
|
+
/** walletCreateBody is the closed wallet.create body of createWallet(): the device key the creation seals the share to, and the grant when one is asked for. */
|
|
41
|
+
export function walletCreateBody(ownerId, input, sdkKey) {
|
|
42
|
+
if (input === null || typeof input !== "object")
|
|
43
|
+
throw new ClientFlowError("invalid_operation_create");
|
|
44
|
+
const create = { kind: "wallet.create", owner_type: "account", owner_id: ownerId, payload: { network: input.network, recipient_public_key: sdkKey ?? input.recipientPublicKey } };
|
|
45
|
+
if (input.grant !== undefined)
|
|
46
|
+
create.grant = grantWire(input.grant);
|
|
47
|
+
return create;
|
|
48
|
+
}
|
|
49
|
+
function sameGrant(shown, asked) {
|
|
50
|
+
if (shown === undefined || asked === undefined)
|
|
51
|
+
return shown === asked;
|
|
52
|
+
return shown.servicePrincipalId === asked.service_principal_id && shown.scopes.join(" ") === asked.scopes.join(" ") && shown.externalRef === asked.external_ref;
|
|
53
|
+
}
|
|
54
|
+
/** createdAsAsked refuses an admitted creation whose display names another grant than the one asked for, or one where none was. */
|
|
55
|
+
export function createdAsAsked(operation, create) {
|
|
56
|
+
const display = operation.display;
|
|
57
|
+
if (operation.kind !== "wallet.create" || display.kind !== "wallet.create")
|
|
58
|
+
throw new ClientFlowError("invalid_server_response");
|
|
59
|
+
if (!("materialPurged" in display) && !sameGrant(display.grant, create.grant))
|
|
60
|
+
throw new ClientFlowError("invalid_server_response");
|
|
61
|
+
return operation;
|
|
62
|
+
}
|
|
63
|
+
/** parseWalletCreateGrant reads the grant a creation's display names. */
|
|
64
|
+
export function parseWalletCreateGrant(value) {
|
|
65
|
+
if (!validWalletCreateGrant(value) || order.filter((scope) => value.scopes.includes(scope)).join(" ") !== value.scopes.join(" "))
|
|
66
|
+
throw new ClientFlowError("invalid_server_response");
|
|
67
|
+
const grant = { servicePrincipalId: value.service_principal_id, scopes: [...value.scopes] };
|
|
68
|
+
if (value.external_ref !== undefined)
|
|
69
|
+
grant.externalRef = value.external_ref;
|
|
70
|
+
return grant;
|
|
71
|
+
}
|
|
72
|
+
/** parseWalletCreateGrantOutcome reads the grant member of a creation's result: a code exactly for not_set, an operation for every grant that was set. */
|
|
73
|
+
export function parseWalletCreateGrantOutcome(value) {
|
|
74
|
+
const outcome = record(value, ["state"], ["operation_id", "linked_wallet_id", "code"]);
|
|
75
|
+
const state = outcome.state;
|
|
76
|
+
if ((state !== "succeeded" && state !== "pending_target_fence" && state !== "not_set") || (outcome.operation_id !== undefined && !lowerUUID(outcome.operation_id)) ||
|
|
77
|
+
(outcome.linked_wallet_id !== undefined && !lowerUUID(outcome.linked_wallet_id)) || (state === "not_set") !== (outcome.code !== undefined) ||
|
|
78
|
+
(outcome.code !== undefined && !isAuthorizationErrorCode(outcome.code)) || (state !== "not_set" && outcome.operation_id === undefined)) {
|
|
79
|
+
throw new ClientFlowError("invalid_server_response");
|
|
80
|
+
}
|
|
81
|
+
const parsed = { state };
|
|
82
|
+
if (outcome.operation_id !== undefined)
|
|
83
|
+
parsed.operationId = outcome.operation_id;
|
|
84
|
+
if (outcome.linked_wallet_id !== undefined)
|
|
85
|
+
parsed.linkedWalletId = outcome.linked_wallet_id;
|
|
86
|
+
if (outcome.code !== undefined)
|
|
87
|
+
parsed.code = outcome.code;
|
|
88
|
+
return parsed;
|
|
89
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ResultRecipient } from "./slice4.ts";
|
|
2
|
+
export type DisplayCommon = {
|
|
3
|
+
ownerId: string;
|
|
4
|
+
requesterId: string;
|
|
5
|
+
resultRecipient: ResultRecipient;
|
|
6
|
+
walletId?: string;
|
|
7
|
+
};
|
|
8
|
+
export declare const walletRecipient: RegExp;
|
|
9
|
+
export declare function displayCommon(display: Record<string, unknown>): DisplayCommon;
|
|
10
|
+
export declare function summaryOf(value: unknown): {
|
|
11
|
+
preparedSummary?: string;
|
|
12
|
+
};
|
|
13
|
+
export declare function rawQuantity(value: unknown, positive: boolean): value is string;
|
|
14
|
+
export declare function bindingRevision(value: unknown): value is string;
|
|
15
|
+
export declare function lowerUUID(value: unknown): value is string;
|
|
16
|
+
/** lowerHexBytes accepts 1..1024 bytes of lowercase hexadecimal. */
|
|
17
|
+
export declare function lowerHexBytes(value: unknown): value is string;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Parts every safe display of a wallet operation is read with: the identity
|
|
2
|
+
// members each display carries, and the closed value forms shared by the
|
|
3
|
+
// displays of own wallets and of linked wallets.
|
|
4
|
+
import { ClientFlowError, canonicalDecimal, opaqueId, record } from "./auth.js";
|
|
5
|
+
/** maxUint256 bounds every raw quantity as source §2.4 requires. */
|
|
6
|
+
const maxUint256 = (1n << 256n) - 1n;
|
|
7
|
+
export const walletRecipient = /^[\x21-\x7e]{1,256}$/;
|
|
8
|
+
export function displayCommon(display) {
|
|
9
|
+
if (!opaqueId(display.owner_id) || !opaqueId(display.requester_id) || (display.wallet_id !== undefined && !opaqueId(display.wallet_id))) {
|
|
10
|
+
throw new ClientFlowError("invalid_server_response");
|
|
11
|
+
}
|
|
12
|
+
const common = { ownerId: display.owner_id, requesterId: display.requester_id, resultRecipient: resultRecipientOf(display.result_recipient) };
|
|
13
|
+
if (display.wallet_id !== undefined)
|
|
14
|
+
common.walletId = display.wallet_id;
|
|
15
|
+
return common;
|
|
16
|
+
}
|
|
17
|
+
function resultRecipientOf(value) {
|
|
18
|
+
const recipient = record(value, ["type", "id"]);
|
|
19
|
+
if (recipient.type !== "account" && recipient.type !== "service")
|
|
20
|
+
throw new ClientFlowError("invalid_server_response");
|
|
21
|
+
if (!opaqueId(recipient.id))
|
|
22
|
+
throw new ClientFlowError("invalid_server_response");
|
|
23
|
+
return { type: recipient.type, id: recipient.id };
|
|
24
|
+
}
|
|
25
|
+
export function summaryOf(value) {
|
|
26
|
+
if (value === undefined)
|
|
27
|
+
return {};
|
|
28
|
+
if (typeof value !== "string" || value.length === 0)
|
|
29
|
+
throw new ClientFlowError("invalid_server_response");
|
|
30
|
+
return { preparedSummary: value };
|
|
31
|
+
}
|
|
32
|
+
export function rawQuantity(value, positive) {
|
|
33
|
+
if (!canonicalDecimal(value))
|
|
34
|
+
return false;
|
|
35
|
+
const quantity = BigInt(value);
|
|
36
|
+
return quantity <= maxUint256 && (!positive || quantity > 0n);
|
|
37
|
+
}
|
|
38
|
+
export function bindingRevision(value) {
|
|
39
|
+
return canonicalDecimal(value) && value !== "0" && value.length <= 16 && BigInt(value) <= BigInt(Number.MAX_SAFE_INTEGER);
|
|
40
|
+
}
|
|
41
|
+
export function lowerUUID(value) {
|
|
42
|
+
return typeof value === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(value);
|
|
43
|
+
}
|
|
44
|
+
/** lowerHexBytes accepts 1..1024 bytes of lowercase hexadecimal. */
|
|
45
|
+
export function lowerHexBytes(value) {
|
|
46
|
+
return typeof value === "string" && value.length >= 2 && value.length <= 2048 && value.length % 2 === 0 && /^[0-9a-f]+$/.test(value);
|
|
47
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** MaxWalletEventPayloadLength bounds the body text one activity event releases whole. */
|
|
2
|
+
export declare const MaxWalletEventPayloadLength = 131072;
|
|
3
|
+
export type WalletEventKind = "deposit" | "withdrawal" | "activity";
|
|
4
|
+
export type WalletEventStatus = "completed" | "finalized" | "reverted";
|
|
5
|
+
/** WalletEvent is one wallet event 3.0. */
|
|
6
|
+
export interface WalletEvent {
|
|
7
|
+
readonly contractVersion: "3.0";
|
|
8
|
+
/** Unique per subject and revision. */
|
|
9
|
+
readonly eventId: string;
|
|
10
|
+
/**
|
|
11
|
+
* The event's own place in the stream of events: an opaque text no two
|
|
12
|
+
* events share. A stream of wallet events is continued after it.
|
|
13
|
+
*/
|
|
14
|
+
readonly cursor: string;
|
|
15
|
+
readonly kind: WalletEventKind;
|
|
16
|
+
readonly status: WalletEventStatus;
|
|
17
|
+
/** Native decimal revision, from "1"; it is never converted to a JavaScript number. */
|
|
18
|
+
readonly revision: string;
|
|
19
|
+
/** What the revisions are about: one transfer in, one transfer out, or one activity. */
|
|
20
|
+
readonly subjectId: string;
|
|
21
|
+
readonly registrationId: string;
|
|
22
|
+
readonly networkId: string;
|
|
23
|
+
/** The tracked address. */
|
|
24
|
+
readonly address: string;
|
|
25
|
+
/** Present for a deposit and a withdrawal, and for an activity that names an asset. */
|
|
26
|
+
readonly asset?: string;
|
|
27
|
+
/** Canonical unsigned decimal string in minimal units. Present for a deposit and a withdrawal. */
|
|
28
|
+
readonly rawAmount?: string;
|
|
29
|
+
readonly counterparty?: string;
|
|
30
|
+
/** Present for an activity: its kind as Streams names it. */
|
|
31
|
+
readonly activityType?: string;
|
|
32
|
+
readonly sourceReference: string;
|
|
33
|
+
readonly observedAt: string;
|
|
34
|
+
/** Present for reverted, when Streams names a reason. */
|
|
35
|
+
readonly invalidationReason?: string;
|
|
36
|
+
/** The body of an expanded activity as its canonical JSON text, and the same read as an object. */
|
|
37
|
+
readonly payloadJson?: string;
|
|
38
|
+
readonly payload?: Readonly<Record<string, unknown>>;
|
|
39
|
+
/** Present instead of payloadJson when the body is too large for this read. */
|
|
40
|
+
readonly payloadOmittedBytes?: number;
|
|
41
|
+
}
|
|
42
|
+
/** parseWalletEvent parses one wallet event 3.0, closed. */
|
|
43
|
+
export declare function parseWalletEvent(value: unknown): WalletEvent;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// The wallet event 3.0: one shape for everything a client hears about a
|
|
2
|
+
// tracked wallet.
|
|
3
|
+
//
|
|
4
|
+
// kind "deposit" (value came in), "withdrawal" (value went out),
|
|
5
|
+
// "activity" (anything else)
|
|
6
|
+
// status "completed" (it is in a block), "finalized" (the block is final),
|
|
7
|
+
// "reverted" (the block was replaced)
|
|
8
|
+
// revision "1" for the first status of a subject, "2" for the next
|
|
9
|
+
//
|
|
10
|
+
// Every subject is told at least twice, completed and then finalized, on
|
|
11
|
+
// every network. Where a block is final as soon as it exists the two arrive
|
|
12
|
+
// together; on Ethereum finalized follows completed after thirteen to
|
|
13
|
+
// nineteen minutes. A client that acts on a transfer once it is in a block
|
|
14
|
+
// waits for completed and may ignore what follows.
|
|
15
|
+
//
|
|
16
|
+
// The parser is closed, like every parser of this package: a member that is
|
|
17
|
+
// missing, unknown or owned by another kind refuses the event.
|
|
18
|
+
import { ClientFlowError, integer, opaqueId, record, rfc3339UTC } from "./auth.js";
|
|
19
|
+
/** MaxWalletEventPayloadLength bounds the body text one activity event releases whole. */
|
|
20
|
+
export const MaxWalletEventPayloadLength = 131072;
|
|
21
|
+
const kinds = ["deposit", "withdrawal", "activity"];
|
|
22
|
+
const statuses = ["completed", "finalized", "reverted"];
|
|
23
|
+
const decimalPattern = /^[1-9][0-9]{0,255}$/;
|
|
24
|
+
const revisionPattern = /^[1-9][0-9]{0,19}$/;
|
|
25
|
+
const cursorPattern = /^[1-9][0-9]{0,18}\.[1-9]$/;
|
|
26
|
+
const optional = ["asset", "raw_amount", "counterparty", "activity_type", "invalidation_reason", "payload_json", "payload_omitted_bytes"];
|
|
27
|
+
/** parseWalletEvent parses one wallet event 3.0, closed. */
|
|
28
|
+
export function parseWalletEvent(value) {
|
|
29
|
+
const event = record(value, [
|
|
30
|
+
"contract_version", "event_id", "cursor", "kind", "status", "revision", "subject_id",
|
|
31
|
+
"registration_id", "network_id", "address", "source_reference", "observed_at",
|
|
32
|
+
], optional);
|
|
33
|
+
const has = (member) => Object.prototype.hasOwnProperty.call(event, member);
|
|
34
|
+
const transfer = event.kind === "deposit" || event.kind === "withdrawal";
|
|
35
|
+
if (event.contract_version !== "3.0" || !text(event.event_id, 512) || !matches(cursorPattern, event.cursor) ||
|
|
36
|
+
typeof event.kind !== "string" || !kinds.includes(event.kind) ||
|
|
37
|
+
typeof event.status !== "string" || !statuses.includes(event.status) ||
|
|
38
|
+
!matches(revisionPattern, event.revision) || (event.status === "completed") !== (event.revision === "1") ||
|
|
39
|
+
!text(event.subject_id, 256) || !opaqueId(event.registration_id) || !text(event.network_id, 256) ||
|
|
40
|
+
!text(event.address, 256) || !text(event.source_reference, 512) || !rfc3339UTC(event.observed_at) ||
|
|
41
|
+
// A transfer names its asset and its amount and nothing an activity owns.
|
|
42
|
+
(transfer && (!text(event.asset, 512) || !matches(decimalPattern, event.raw_amount) || has("activity_type") ||
|
|
43
|
+
has("payload_json") || has("payload_omitted_bytes"))) ||
|
|
44
|
+
(!transfer && (!text(event.activity_type, 128) || (has("asset") && !text(event.asset, 512)) ||
|
|
45
|
+
(has("raw_amount") && !matches(decimalPattern, event.raw_amount)) ||
|
|
46
|
+
(has("payload_json") && has("payload_omitted_bytes")))) ||
|
|
47
|
+
(has("counterparty") && !text(event.counterparty, 512)) ||
|
|
48
|
+
(has("invalidation_reason") && (event.status !== "reverted" || !text(event.invalidation_reason, 128))) ||
|
|
49
|
+
(has("payload_omitted_bytes") && !(integer(event.payload_omitted_bytes) && event.payload_omitted_bytes >= 1))) {
|
|
50
|
+
throw new ClientFlowError("invalid_server_response");
|
|
51
|
+
}
|
|
52
|
+
const parsed = {
|
|
53
|
+
contractVersion: "3.0", eventId: event.event_id, cursor: event.cursor, kind: event.kind, status: event.status,
|
|
54
|
+
revision: event.revision, subjectId: event.subject_id, registrationId: event.registration_id,
|
|
55
|
+
networkId: event.network_id, address: event.address, sourceReference: event.source_reference,
|
|
56
|
+
observedAt: event.observed_at,
|
|
57
|
+
};
|
|
58
|
+
if (has("asset"))
|
|
59
|
+
parsed.asset = event.asset;
|
|
60
|
+
if (has("raw_amount"))
|
|
61
|
+
parsed.rawAmount = event.raw_amount;
|
|
62
|
+
if (has("counterparty"))
|
|
63
|
+
parsed.counterparty = event.counterparty;
|
|
64
|
+
if (has("activity_type"))
|
|
65
|
+
parsed.activityType = event.activity_type;
|
|
66
|
+
if (has("invalidation_reason"))
|
|
67
|
+
parsed.invalidationReason = event.invalidation_reason;
|
|
68
|
+
if (has("payload_json")) {
|
|
69
|
+
parsed.payload = payloadObject(event.payload_json);
|
|
70
|
+
parsed.payloadJson = event.payload_json;
|
|
71
|
+
}
|
|
72
|
+
if (has("payload_omitted_bytes"))
|
|
73
|
+
parsed.payloadOmittedBytes = event.payload_omitted_bytes;
|
|
74
|
+
return Object.freeze(parsed);
|
|
75
|
+
}
|
|
76
|
+
/** Only the JSON text of an object within the bound is a body; anything else refuses the event. */
|
|
77
|
+
function payloadObject(value) {
|
|
78
|
+
if (typeof value !== "string" || value.length < 1 || value.length > MaxWalletEventPayloadLength) {
|
|
79
|
+
throw new ClientFlowError("invalid_server_response");
|
|
80
|
+
}
|
|
81
|
+
let parsed;
|
|
82
|
+
try {
|
|
83
|
+
parsed = JSON.parse(value);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
throw new ClientFlowError("invalid_server_response");
|
|
87
|
+
}
|
|
88
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
89
|
+
throw new ClientFlowError("invalid_server_response");
|
|
90
|
+
return Object.freeze(parsed);
|
|
91
|
+
}
|
|
92
|
+
function text(value, maximum) {
|
|
93
|
+
return typeof value === "string" && value.length >= 1 && value.length <= maximum;
|
|
94
|
+
}
|
|
95
|
+
function matches(pattern, value) {
|
|
96
|
+
return typeof value === "string" && pattern.test(value);
|
|
97
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export type LimitAsset = 'native' | 'usdt' | 'usdc';
|
|
2
|
+
/** At most this many network and asset pairs, and this many recipients. */
|
|
3
|
+
export declare const MaxLimitCeilings = 24;
|
|
4
|
+
export declare const MaxLimitRecipients = 100;
|
|
5
|
+
/** The ceilings of one asset on one network, in that network's minimal units. An absent ceiling bounds nothing. */
|
|
6
|
+
export interface WalletCeiling {
|
|
7
|
+
network: string;
|
|
8
|
+
asset: LimitAsset;
|
|
9
|
+
perTransferRaw?: string;
|
|
10
|
+
perDayRaw?: string;
|
|
11
|
+
}
|
|
12
|
+
/** A wallet's limits now. revision "0" is a wallet whose limits were never set; no recipient means any recipient. */
|
|
13
|
+
export interface WalletLimits {
|
|
14
|
+
requestId: string;
|
|
15
|
+
walletId: string;
|
|
16
|
+
revision: string;
|
|
17
|
+
ceilings: readonly WalletCeiling[];
|
|
18
|
+
recipients: readonly string[];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The whole of a wallet's new limits. Empty ceilings and recipients remove every limit. revision is the
|
|
22
|
+
* revision of the limits they were edited from, as limits() read it: a change made from limits that
|
|
23
|
+
* changed since (in another tab, say) is refused with VERSION_CONFLICT; read them again and redo it.
|
|
24
|
+
*/
|
|
25
|
+
export interface WalletLimitsInput {
|
|
26
|
+
ceilings: readonly WalletCeiling[];
|
|
27
|
+
recipients: readonly string[];
|
|
28
|
+
revision: string;
|
|
29
|
+
}
|
|
30
|
+
/** What the owner confirms: the wallet's address and its new limits as they will be kept, recipients in the form the network compares them by. */
|
|
31
|
+
export interface WalletLimitsDisplay {
|
|
32
|
+
kind: 'wallet_limits.set';
|
|
33
|
+
ownerId: string;
|
|
34
|
+
requesterId: string;
|
|
35
|
+
linkedWalletId: string;
|
|
36
|
+
walletAddress: string;
|
|
37
|
+
ceilings: readonly WalletCeiling[];
|
|
38
|
+
recipients: readonly string[];
|
|
39
|
+
expectedRevision: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The answers to a send the wallet's limits do not allow. Each is 403 and
|
|
43
|
+
* nothing is sent; the send goes through only after the limits change.
|
|
44
|
+
* - LIMIT_PER_TRANSFER_EXCEEDED: the amount is over the ceiling of one
|
|
45
|
+
* transfer of its asset on its network.
|
|
46
|
+
* - LIMIT_DAILY_EXCEEDED: with what the wallet's sends of that asset on that
|
|
47
|
+
* network moved in the last 24 hours, the send would pass the daily
|
|
48
|
+
* ceiling. It is judged when the send is executed.
|
|
49
|
+
* - RECIPIENT_NOT_ALLOWED: the wallet has a recipient list and the recipient
|
|
50
|
+
* is not in it.
|
|
51
|
+
* The first and the last are answered when the send is created and again
|
|
52
|
+
* when it is executed, against the limits as they are then; a repeated
|
|
53
|
+
* create with the same key answers the send it created. A speed-up is
|
|
54
|
+
* judged by them too and adds nothing to the day, but is refused when the
|
|
55
|
+
* day already holds more than the daily ceiling; a cancel by none.
|
|
56
|
+
*/
|
|
57
|
+
export declare const WalletLimitRefusals: readonly ['LIMIT_PER_TRANSFER_EXCEEDED', 'LIMIT_DAILY_EXCEEDED', 'RECIPIENT_NOT_ALLOWED'];
|
|
58
|
+
export type WalletLimitRefusal = (typeof WalletLimitRefusals)[number];
|
|
59
|
+
export declare const isWalletLimitRefusal: (code: unknown) => code is WalletLimitRefusal;
|
|
60
|
+
/** The closed body of POST /v1/operations that asks to replace a linked wallet's limits. */
|
|
61
|
+
export declare function walletLimitsCreate(ownerId: string, linkedWalletId: string, input: WalletLimitsInput): Record<string, unknown>;
|
|
62
|
+
/** The answer of GET /v1/linked-wallets/{id}/limits for walletId. */
|
|
63
|
+
export declare function parseWalletLimits(payload: unknown, walletId: string): WalletLimits;
|
|
64
|
+
/** The display of a wallet_limits.set operation. */
|
|
65
|
+
export declare function parseWalletLimitsDisplay(value: unknown): WalletLimitsDisplay;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// The limits of a linked wallet (owner decision of 14 October 2026). The
|
|
2
|
+
// account's session reads them; changing them, removing them included, is the
|
|
3
|
+
// wallet_limits.set operation the owner confirms with a passkey on the
|
|
4
|
+
// provider's page, which says the new limits in words; and every send from
|
|
5
|
+
// the wallet obeys them. Amounts are in each asset's minimal units on its own
|
|
6
|
+
// network: a token's decimals differ between networks (USDT has 6 on Ethereum
|
|
7
|
+
// and 18 on BNB Smart Chain), so a ceiling names its network.
|
|
8
|
+
import { ClientFlowError, canonicalDecimal, integer, opaqueId, record } from './auth.js';
|
|
9
|
+
import { fault, network, uuid } from './linkedChecks.js';
|
|
10
|
+
/** At most this many network and asset pairs, and this many recipients. */
|
|
11
|
+
export const MaxLimitCeilings = 24;
|
|
12
|
+
export const MaxLimitRecipients = 100;
|
|
13
|
+
/**
|
|
14
|
+
* The answers to a send the wallet's limits do not allow. Each is 403 and
|
|
15
|
+
* nothing is sent; the send goes through only after the limits change.
|
|
16
|
+
* - LIMIT_PER_TRANSFER_EXCEEDED: the amount is over the ceiling of one
|
|
17
|
+
* transfer of its asset on its network.
|
|
18
|
+
* - LIMIT_DAILY_EXCEEDED: with what the wallet's sends of that asset on that
|
|
19
|
+
* network moved in the last 24 hours, the send would pass the daily
|
|
20
|
+
* ceiling. It is judged when the send is executed.
|
|
21
|
+
* - RECIPIENT_NOT_ALLOWED: the wallet has a recipient list and the recipient
|
|
22
|
+
* is not in it.
|
|
23
|
+
* The first and the last are answered when the send is created and again
|
|
24
|
+
* when it is executed, against the limits as they are then; a repeated
|
|
25
|
+
* create with the same key answers the send it created. A speed-up is
|
|
26
|
+
* judged by them too and adds nothing to the day, but is refused when the
|
|
27
|
+
* day already holds more than the daily ceiling; a cancel by none.
|
|
28
|
+
*/
|
|
29
|
+
export const WalletLimitRefusals = ['LIMIT_PER_TRANSFER_EXCEEDED', 'LIMIT_DAILY_EXCEEDED', 'RECIPIENT_NOT_ALLOWED'];
|
|
30
|
+
export const isWalletLimitRefusal = (code) => WalletLimitRefusals.includes(code);
|
|
31
|
+
/** The closed body of POST /v1/operations that asks to replace a linked wallet's limits. */
|
|
32
|
+
export function walletLimitsCreate(ownerId, linkedWalletId, input) {
|
|
33
|
+
const invalid = () => { throw new ClientFlowError('invalid_operation_create'); };
|
|
34
|
+
const closed = (value, required, optional = []) => { try {
|
|
35
|
+
return record(value, required, optional);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return invalid();
|
|
39
|
+
} };
|
|
40
|
+
const declared = closed(input, ['ceilings', 'recipients', 'revision']);
|
|
41
|
+
if (!opaqueId(ownerId) || !uuid(linkedWalletId) || !Array.isArray(declared.ceilings) || declared.ceilings.length > MaxLimitCeilings || !recipients(declared.recipients) || !revision(declared.revision))
|
|
42
|
+
invalid();
|
|
43
|
+
const pairs = new Set();
|
|
44
|
+
const ceilings = declared.ceilings.map((value) => {
|
|
45
|
+
const c = closed(value, ['network', 'asset'], ['perTransferRaw', 'perDayRaw']);
|
|
46
|
+
const pair = `${c.network} ${c.asset}`;
|
|
47
|
+
if (!network(c.network) || !asset(c.asset) || pairs.has(pair) || (c.perTransferRaw === undefined && c.perDayRaw === undefined) ||
|
|
48
|
+
(c.perTransferRaw !== undefined && !amount(c.perTransferRaw)) || (c.perDayRaw !== undefined && !amount(c.perDayRaw)))
|
|
49
|
+
invalid();
|
|
50
|
+
pairs.add(pair);
|
|
51
|
+
const out = { network: c.network, asset: c.asset };
|
|
52
|
+
if (c.perTransferRaw !== undefined)
|
|
53
|
+
out.per_transfer_raw = c.perTransferRaw;
|
|
54
|
+
if (c.perDayRaw !== undefined)
|
|
55
|
+
out.per_day_raw = c.perDayRaw;
|
|
56
|
+
return out;
|
|
57
|
+
});
|
|
58
|
+
return { kind: 'wallet_limits.set', owner_type: 'account', owner_id: ownerId, linked_wallet_id: linkedWalletId, payload: { ceilings, recipients: [...declared.recipients], expected_revision: declared.revision } };
|
|
59
|
+
}
|
|
60
|
+
/** The answer of GET /v1/linked-wallets/{id}/limits for walletId. */
|
|
61
|
+
export function parseWalletLimits(payload, walletId) {
|
|
62
|
+
const root = record(payload, ['request_id', 'limits']);
|
|
63
|
+
const l = record(root.limits, ['ceilings', 'recipients', 'revision', 'wallet_id']);
|
|
64
|
+
if (!opaqueId(root.request_id) || l.wallet_id !== walletId || !canonicalDecimal(l.revision) || !recipients(l.recipients))
|
|
65
|
+
fault();
|
|
66
|
+
return Object.freeze({ requestId: root.request_id, walletId, revision: l.revision, ceilings: parseCeilings(l.ceilings), recipients: Object.freeze([...l.recipients]) });
|
|
67
|
+
}
|
|
68
|
+
/** The display of a wallet_limits.set operation. */
|
|
69
|
+
export function parseWalletLimitsDisplay(value) {
|
|
70
|
+
const d = record(value, ['kind', 'owner_id', 'requester_id', 'linked_wallet_id', 'wallet_address', 'ceilings', 'recipients', 'expected_revision']);
|
|
71
|
+
if (d.kind !== 'wallet_limits.set' || !opaqueId(d.owner_id) || !opaqueId(d.requester_id) || !uuid(d.linked_wallet_id) || typeof d.wallet_address !== 'string' ||
|
|
72
|
+
d.wallet_address.length < 1 || d.wallet_address.length > 256 || !recipients(d.recipients) || !revision(d.expected_revision))
|
|
73
|
+
fault();
|
|
74
|
+
return Object.freeze({ kind: 'wallet_limits.set', ownerId: d.owner_id, requesterId: d.requester_id, linkedWalletId: d.linked_wallet_id, walletAddress: d.wallet_address,
|
|
75
|
+
ceilings: parseCeilings(d.ceilings), recipients: Object.freeze([...d.recipients]), expectedRevision: d.expected_revision });
|
|
76
|
+
}
|
|
77
|
+
function parseCeilings(value) {
|
|
78
|
+
if (!Array.isArray(value) || value.length > MaxLimitCeilings)
|
|
79
|
+
fault();
|
|
80
|
+
const pairs = new Set();
|
|
81
|
+
return Object.freeze(value.map((item) => {
|
|
82
|
+
const c = record(item, ['network', 'asset'], ['per_transfer_raw', 'per_day_raw', 'decimals']);
|
|
83
|
+
const pair = `${c.network} ${c.asset}`;
|
|
84
|
+
if (!network(c.network) || !asset(c.asset) || pairs.has(pair) || (c.per_transfer_raw === undefined && c.per_day_raw === undefined) ||
|
|
85
|
+
(c.per_transfer_raw !== undefined && !amount(c.per_transfer_raw)) || (c.per_day_raw !== undefined && !amount(c.per_day_raw)) ||
|
|
86
|
+
(c.decimals !== undefined && (!integer(c.decimals) || c.decimals < 0 || c.decimals > 30)))
|
|
87
|
+
fault();
|
|
88
|
+
pairs.add(pair);
|
|
89
|
+
const out = { network: c.network, asset: c.asset };
|
|
90
|
+
if (c.per_transfer_raw !== undefined)
|
|
91
|
+
out.perTransferRaw = c.per_transfer_raw;
|
|
92
|
+
if (c.per_day_raw !== undefined)
|
|
93
|
+
out.perDayRaw = c.per_day_raw;
|
|
94
|
+
return Object.freeze(out);
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
function asset(v) { return v === 'native' || v === 'usdt' || v === 'usdc'; }
|
|
98
|
+
function amount(v) { return canonicalDecimal(v) && v.length <= 78; }
|
|
99
|
+
function revision(v) { return canonicalDecimal(v) && v.length <= 20; }
|
|
100
|
+
function recipients(v) {
|
|
101
|
+
return Array.isArray(v) && v.length <= MaxLimitRecipients && new Set(v).size === v.length && v.every((r) => typeof r === 'string' && /^[\x21-\x7e]{1,128}$/.test(r));
|
|
102
|
+
}
|