@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.
Files changed (56) hide show
  1. package/README.md +708 -2
  2. package/dist/aesGcmSiv.d.ts +14 -0
  3. package/dist/aesGcmSiv.js +160 -0
  4. package/dist/auth.d.ts +68 -0
  5. package/dist/auth.js +215 -0
  6. package/dist/balances.d.ts +31 -0
  7. package/dist/balances.js +44 -0
  8. package/dist/canonicalize.d.ts +19 -0
  9. package/dist/canonicalize.js +107 -0
  10. package/dist/client.d.ts +264 -0
  11. package/dist/client.js +754 -0
  12. package/dist/deviceKey.d.ts +13 -0
  13. package/dist/deviceKey.js +28 -0
  14. package/dist/deviceShare.d.ts +9 -0
  15. package/dist/deviceShare.js +24 -0
  16. package/dist/deviceShareEnvelope.d.ts +13 -0
  17. package/dist/deviceShareEnvelope.js +139 -0
  18. package/dist/linkedChecks.d.ts +17 -0
  19. package/dist/linkedChecks.js +31 -0
  20. package/dist/linkedSendDisplay.d.ts +73 -0
  21. package/dist/linkedSendDisplay.js +84 -0
  22. package/dist/linkedWalletSignals.d.ts +22 -0
  23. package/dist/linkedWalletSignals.js +101 -0
  24. package/dist/linkedWallets.d.ts +373 -0
  25. package/dist/linkedWallets.js +471 -0
  26. package/dist/mediatedOperation.d.ts +27 -0
  27. package/dist/mediatedOperation.js +68 -0
  28. package/dist/modeC.d.ts +131 -0
  29. package/dist/modeC.js +248 -0
  30. package/dist/modeCLinked.d.ts +207 -0
  31. package/dist/modeCLinked.js +270 -0
  32. package/dist/pages.d.ts +22 -0
  33. package/dist/pages.js +45 -0
  34. package/dist/poll.d.ts +15 -0
  35. package/dist/poll.js +76 -0
  36. package/dist/requestGrant.d.ts +169 -0
  37. package/dist/requestGrant.js +243 -0
  38. package/dist/sends.d.ts +89 -0
  39. package/dist/sends.js +127 -0
  40. package/dist/slice4.d.ts +173 -0
  41. package/dist/slice4.js +285 -0
  42. package/dist/streams.d.ts +212 -0
  43. package/dist/streams.js +282 -0
  44. package/dist/trackedWallets.d.ts +53 -0
  45. package/dist/trackedWallets.js +104 -0
  46. package/dist/transferStatus.d.ts +45 -0
  47. package/dist/transferStatus.js +43 -0
  48. package/dist/walletDisplayParts.d.ts +15 -0
  49. package/dist/walletDisplayParts.js +43 -0
  50. package/dist/walletEvents.d.ts +43 -0
  51. package/dist/walletEvents.js +97 -0
  52. package/dist/walletLimits.d.ts +65 -0
  53. package/dist/walletLimits.js +102 -0
  54. package/dist/walletOperations.d.ts +192 -0
  55. package/dist/walletOperations.js +308 -0
  56. package/package.json +64 -4
@@ -0,0 +1,243 @@
1
+ // Mode C request-signing key and request grant (source §1.4, §2.2, §2.5).
2
+ //
3
+ // The private key is a non-extractable P-256 WebCrypto key held only in this
4
+ // package's memory; only its public kty/crv/x/y JWK is ever enrolled. The
5
+ // protected grant result is retained beside the exact key that produced it: a
6
+ // result paired with another key, an altered thumbprint or a lost key all
7
+ // refuse proof generation locally and require explicit re-enrollment with
8
+ // fresh passkey UV. Nothing here reads or writes browser storage, and nothing
9
+ // here re-enrolls on refresh, reload or login.
10
+ import { ClientFlowError, base64url, canonicalDecimal, integer, opaqueId, record } from "./auth.js";
11
+ import { canonicalJSONBytes } from "./canonicalize.js";
12
+ /** MaxRequestGrantRecipients is the source §2.8 allowlist bound. */
13
+ export const MaxRequestGrantRecipients = 10;
14
+ /** MaxRequestGrantLifetimeSeconds is the source §2.5 ceiling of fifteen minutes. */
15
+ export const MaxRequestGrantLifetimeSeconds = 15 * 60;
16
+ const maxUint256 = (1n << 256n) - 1n;
17
+ const coordinate = /^[A-Za-z0-9_-]{43}$/;
18
+ const grantToken = /^[A-Za-z0-9_-]{43}$/;
19
+ const grantNetwork = /^[A-Za-z0-9_.-]{1,64}$/;
20
+ const grantRecipient = /^[\x21-\x7e]{1,256}$/;
21
+ /**
22
+ * generateRequestSigningKey creates the non-extractable P-256 key of source
23
+ * §2.5. It refuses a runtime that returns an extractable private key or a
24
+ * public JWK carrying any private member, so a weakened WebCrypto cannot
25
+ * silently become custody of a user key.
26
+ */
27
+ export async function generateRequestSigningKey(crypto) {
28
+ if (!crypto?.subtle || typeof crypto.subtle.generateKey !== "function" || typeof crypto.getRandomValues !== "function") {
29
+ throw new ClientFlowError("runtime_capability_unavailable");
30
+ }
31
+ const algorithm = { name: "ECDSA", namedCurve: "P-256" };
32
+ const pair = (await crypto.subtle.generateKey(algorithm, false, ["sign", "verify"]));
33
+ if (!pair?.privateKey || !pair.publicKey || pair.privateKey.extractable !== false)
34
+ throw new ClientFlowError("request_key_extractable");
35
+ const exported = await crypto.subtle.exportKey("jwk", pair.publicKey);
36
+ const publicJwk = exactPublicJwk(exported);
37
+ return {
38
+ handle: base64url(crypto.getRandomValues(new Uint8Array(16))),
39
+ publicJwk,
40
+ thumbprint: await jwkThumbprint(crypto, publicJwk),
41
+ privateKey: pair.privateKey,
42
+ };
43
+ }
44
+ /**
45
+ * jwkThumbprint is the RFC 7638 SHA-256 thumbprint in unpadded base64url over
46
+ * exactly the required EC members. The canonicalizer already orders them
47
+ * lexicographically, which is the order RFC 7638 prescribes.
48
+ */
49
+ export async function jwkThumbprint(crypto, key) {
50
+ if (!crypto?.subtle || typeof crypto.subtle.digest !== "function")
51
+ throw new ClientFlowError("runtime_capability_unavailable");
52
+ const canonical = canonicalJSONBytes({ crv: key.crv, kty: key.kty, x: key.x, y: key.y });
53
+ return base64url(new Uint8Array(await crypto.subtle.digest("SHA-256", canonical)));
54
+ }
55
+ /**
56
+ * buildRequestGrantEnrollment returns the closed `session.authorize_requests`
57
+ * OperationCreate. Every bound is validated locally first, so an empty or
58
+ * over-length allowlist, a duplicate or wildcard recipient and a noncanonical
59
+ * or out-of-range ceiling never leave the client.
60
+ */
61
+ export function buildRequestGrantEnrollment(key, enrollment) {
62
+ const declared = record(enrollment, ["ownerId", "servicePrincipalId", "walletId", "network", "recipientAllowlist", "maxAmountRaw", "maxFeeRaw"]);
63
+ if (!opaqueId(declared.ownerId) || !opaqueId(declared.servicePrincipalId) || !opaqueId(declared.walletId) ||
64
+ typeof declared.network !== "string" || !grantNetwork.test(declared.network) ||
65
+ !rawQuantity(declared.maxAmountRaw, true) || !rawQuantity(declared.maxFeeRaw, false)) {
66
+ throw new ClientFlowError("invalid_request_grant_enrollment");
67
+ }
68
+ const allowlist = exactAllowlist(declared.recipientAllowlist, "invalid_request_grant_enrollment");
69
+ return {
70
+ kind: "session.authorize_requests",
71
+ owner_type: "account",
72
+ owner_id: declared.ownerId,
73
+ payload: {
74
+ public_jwk: { kty: key.publicJwk.kty, crv: key.publicJwk.crv, x: key.publicJwk.x, y: key.publicJwk.y },
75
+ service_principal_id: declared.servicePrincipalId,
76
+ wallet_id: declared.walletId,
77
+ network: declared.network,
78
+ recipient_allowlist: allowlist,
79
+ max_amount_raw: declared.maxAmountRaw,
80
+ max_fee_raw: declared.maxFeeRaw,
81
+ },
82
+ };
83
+ }
84
+ /**
85
+ * enrollRequestGrant performs one explicit enrollment: it never runs from a
86
+ * refresh, reload or login callback, and it returns the protected grant result
87
+ * paired with the key that will sign under it. The caller must have obtained
88
+ * the hosted passkey confirmation through `confirm`; no proof is generated here
89
+ * and no mediated request is sent.
90
+ */
91
+ export async function enrollRequestGrant(host, key, enrollment, keys) {
92
+ if (!opaqueId(keys?.create) || !opaqueId(keys?.execute))
93
+ throw new ClientFlowError("invalid_idempotency_key");
94
+ const create = buildRequestGrantEnrollment(key, enrollment);
95
+ const admitted = await host.create(create, keys.create);
96
+ if (!opaqueId(admitted?.operationId))
97
+ throw new ClientFlowError("invalid_server_response");
98
+ const confirmed = await host.confirm(admitted.operationId);
99
+ if (confirmed?.operationId !== admitted.operationId || !canonicalDecimal(confirmed.version))
100
+ throw new ClientFlowError("invalid_server_response");
101
+ await host.execute(admitted.operationId, confirmed.version, keys.execute);
102
+ return parseRequestGrantResultEnvelope(await host.result(admitted.operationId), key, enrollment).result;
103
+ }
104
+ /**
105
+ * parseRequestGrantResultEnvelope accepts the `{request_id,result}` protected
106
+ * result envelope and requires the grant to belong to this key and to the
107
+ * enrollment that was submitted.
108
+ */
109
+ export function parseRequestGrantResultEnvelope(payload, key, enrollment) {
110
+ const root = record(payload, ["request_id", "result"]);
111
+ if (!opaqueId(root.request_id))
112
+ throw new ClientFlowError("invalid_server_response");
113
+ const result = parseRequestGrantResult(root.result);
114
+ assertGrantMatches(result, key, enrollment);
115
+ return { requestId: root.request_id, result };
116
+ }
117
+ /** parseRequestGrantResult accepts exactly the closed grant result of source §2.2. */
118
+ export function parseRequestGrantResult(payload) {
119
+ const root = record(payload, ["request_grant_id", "public_key_thumbprint", "server_time", "expires_at", "binding", "bounds"]);
120
+ if (typeof root.request_grant_id !== "string" || !grantToken.test(root.request_grant_id) ||
121
+ typeof root.public_key_thumbprint !== "string" || !grantToken.test(root.public_key_thumbprint) ||
122
+ !integer(root.server_time) || !integer(root.expires_at) || root.expires_at <= root.server_time ||
123
+ root.expires_at - root.server_time > MaxRequestGrantLifetimeSeconds) {
124
+ throw new ClientFlowError("invalid_request_grant_result");
125
+ }
126
+ const raw = record(root.binding, ["tenant_id", "application_id", "environment_id", "client_id", "account_id", "session_id", "service_principal_id", "wallet_id", "wallet_generation", "mapping_version"]);
127
+ const binding = {
128
+ tenantId: boundIdentifier(raw.tenant_id), applicationId: boundIdentifier(raw.application_id), environmentId: boundIdentifier(raw.environment_id),
129
+ clientId: boundIdentifier(raw.client_id), accountId: boundIdentifier(raw.account_id), sessionId: boundIdentifier(raw.session_id),
130
+ servicePrincipalId: boundIdentifier(raw.service_principal_id), walletId: boundIdentifier(raw.wallet_id),
131
+ walletGeneration: boundVersion(raw.wallet_generation), mappingVersion: boundVersion(raw.mapping_version),
132
+ };
133
+ const bounds = record(root.bounds, ["network", "recipient_allowlist", "max_amount_raw", "max_fee_raw"]);
134
+ if (typeof bounds.network !== "string" || !grantNetwork.test(bounds.network) ||
135
+ !rawQuantity(bounds.max_amount_raw, true) || !rawQuantity(bounds.max_fee_raw, false)) {
136
+ throw new ClientFlowError("invalid_request_grant_result");
137
+ }
138
+ return {
139
+ requestGrantId: root.request_grant_id,
140
+ publicKeyThumbprint: root.public_key_thumbprint,
141
+ serverTime: root.server_time,
142
+ expiresAt: root.expires_at,
143
+ binding,
144
+ bounds: {
145
+ network: bounds.network, recipientAllowlist: exactAllowlist(bounds.recipient_allowlist, "invalid_request_grant_result"),
146
+ maxAmountRaw: bounds.max_amount_raw, maxFeeRaw: bounds.max_fee_raw,
147
+ },
148
+ };
149
+ }
150
+ /**
151
+ * RequestGrantStore keeps one grant result beside the exact key that owns it,
152
+ * in memory only. It never touches browser storage, never renews a grant and
153
+ * never re-enrolls: a released, absent or expired pairing reports
154
+ * `request_grant_reenrollment_required` so the caller performs an explicit
155
+ * fresh-UV enrollment instead.
156
+ */
157
+ export class RequestGrantStore {
158
+ #paired;
159
+ /** pair refuses any grant result that is not this key's own enrolled grant. */
160
+ pair(key, grant, enrollment) {
161
+ assertGrantMatches(grant, key, enrollment);
162
+ const paired = Object.freeze({ grant, key });
163
+ this.#paired = paired;
164
+ return paired;
165
+ }
166
+ /**
167
+ * current returns the live pairing. An absent pairing is a lost key, and an
168
+ * expired grant is over: neither is repaired locally.
169
+ */
170
+ current(serverNowSeconds) {
171
+ const paired = this.#paired;
172
+ if (!paired)
173
+ throw new ClientFlowError("request_grant_reenrollment_required");
174
+ if (!integer(serverNowSeconds) || serverNowSeconds >= paired.grant.expiresAt)
175
+ throw new ClientFlowError("request_grant_reenrollment_required");
176
+ return paired;
177
+ }
178
+ /** release forgets the pairing, as logout or a lost key requires. */
179
+ release() {
180
+ this.#paired = undefined;
181
+ }
182
+ }
183
+ /**
184
+ * assertGrantMatches is the local pairing check of source §1.4: the result must
185
+ * carry this key's RFC 7638 thumbprint and the exact bounds and owner the
186
+ * client enrolled. A mismatch is a refusal, never a renegotiated grant.
187
+ */
188
+ export function assertGrantMatches(grant, key, enrollment) {
189
+ if (grant.publicKeyThumbprint !== key.thumbprint)
190
+ throw new ClientFlowError("request_grant_mismatch");
191
+ if (grant.binding.accountId !== enrollment.ownerId || grant.binding.servicePrincipalId !== enrollment.servicePrincipalId ||
192
+ grant.binding.walletId !== enrollment.walletId || grant.bounds.network !== enrollment.network ||
193
+ grant.bounds.maxAmountRaw !== enrollment.maxAmountRaw || grant.bounds.maxFeeRaw !== enrollment.maxFeeRaw) {
194
+ throw new ClientFlowError("request_grant_mismatch");
195
+ }
196
+ if (grant.bounds.recipientAllowlist.length !== enrollment.recipientAllowlist.length ||
197
+ grant.bounds.recipientAllowlist.some((recipient, index) => recipient !== enrollment.recipientAllowlist[index])) {
198
+ throw new ClientFlowError("request_grant_mismatch");
199
+ }
200
+ }
201
+ function exactPublicJwk(value) {
202
+ if (value === null || typeof value !== "object" || Array.isArray(value))
203
+ throw new ClientFlowError("invalid_request_key");
204
+ const jwk = value;
205
+ // A WebCrypto export carries advertised usage metadata; every other member,
206
+ // and any private member, refuses the key outright.
207
+ const allowed = new Set(["kty", "crv", "x", "y", "ext", "key_ops"]);
208
+ if (Object.keys(jwk).some((member) => !allowed.has(member)))
209
+ throw new ClientFlowError("invalid_request_key");
210
+ if (jwk.kty !== "EC" || jwk.crv !== "P-256" || typeof jwk.x !== "string" || typeof jwk.y !== "string" ||
211
+ !coordinate.test(jwk.x) || !coordinate.test(jwk.y)) {
212
+ throw new ClientFlowError("invalid_request_key");
213
+ }
214
+ return Object.freeze({ kty: "EC", crv: "P-256", x: jwk.x, y: jwk.y });
215
+ }
216
+ function exactAllowlist(value, code) {
217
+ if (!Array.isArray(value) || value.length < 1 || value.length > MaxRequestGrantRecipients)
218
+ throw new ClientFlowError(code);
219
+ const allowlist = [];
220
+ for (const recipient of value) {
221
+ if (typeof recipient !== "string" || !grantRecipient.test(recipient) || allowlist.includes(recipient))
222
+ throw new ClientFlowError(code);
223
+ allowlist.push(recipient);
224
+ }
225
+ return Object.freeze(allowlist);
226
+ }
227
+ function rawQuantity(value, positive) {
228
+ if (!canonicalDecimal(value))
229
+ return false;
230
+ const quantity = BigInt(value);
231
+ return quantity <= maxUint256 && (!positive || quantity > 0n);
232
+ }
233
+ function boundIdentifier(value) {
234
+ if (!opaqueId(value))
235
+ throw new ClientFlowError("invalid_request_grant_result");
236
+ return value;
237
+ }
238
+ /** boundVersion accepts the canonical decimal versions of source §2.2; zero is never a live version. */
239
+ function boundVersion(value) {
240
+ if (!canonicalDecimal(value) || value === "0")
241
+ throw new ClientFlowError("invalid_request_grant_result");
242
+ return value;
243
+ }
@@ -0,0 +1,89 @@
1
+ import { type PageOptions } from './pages.ts';
2
+ import { type ReplaceAction, type UserWalletEffectState, type UserWalletState } from './walletOperations.ts';
3
+ import type { WalletEvent } from './walletEvents.ts';
4
+ /** The recorded final fate of a send's transfer. It never changes. */
5
+ export interface TransferOutcome {
6
+ readonly state: 'confirmed' | 'failed' | 'expired';
7
+ /** When the provider recorded the own-wallet system's report. */
8
+ readonly observedAt: string;
9
+ readonly transactionHash?: string;
10
+ readonly failureReason?: string;
11
+ /** The account's own send whose transaction the network included in this transfer's place; only with failureReason nonce_used. */
12
+ readonly replacedByOperationId?: string;
13
+ }
14
+ /**
15
+ * One send of the history. network, asset, recipient and amountRaw are absent
16
+ * once the operation's material retention expired (materialPurged): the send
17
+ * stays listed with its state and its transfer, and without details.
18
+ */
19
+ export interface ListedSend {
20
+ readonly operationId: string;
21
+ readonly state: UserWalletState;
22
+ readonly effectState: UserWalletEffectState;
23
+ /**
24
+ * session_authorized: the account's session authorized a send from a linked
25
+ * wallet (no passkey, since 14 October 2026); user_confirmed: the passkey
26
+ * confirmed it; user_request_signed: a service sent it on the user's signed
27
+ * request; "" while it waits for a passkey.
28
+ */
29
+ readonly authorizationBasis: '' | 'user_confirmed' | 'session_authorized' | 'user_request_signed';
30
+ readonly createdAt: string;
31
+ readonly expiresAt: string;
32
+ readonly materialPurged: boolean;
33
+ /** Present for a send from a linked wallet. */
34
+ readonly linkedWalletId?: string;
35
+ /** Present for a send from a wallet bound in the provider. */
36
+ readonly walletId?: string;
37
+ readonly network?: string;
38
+ readonly asset?: string;
39
+ readonly recipient?: string;
40
+ readonly amountRaw?: string;
41
+ /** The sending address, once the own-wallet system prepared the transfer of a linked wallet. */
42
+ readonly address?: string;
43
+ /**
44
+ * Present for a send that replaces an earlier one the network had not
45
+ * included: that send's operation, and "speed_up" or "cancel". network,
46
+ * asset, recipient and amountRaw are then what the replacing transaction
47
+ * itself moves, for a cancel nothing ("0") to the wallet's own address.
48
+ */
49
+ readonly replacesOperationId?: string;
50
+ readonly replaceAction?: ReplaceAction;
51
+ /** The text the transfer carries to its recipient, on TON. */
52
+ readonly comment?: string;
53
+ /** The network's hash of a send from a linked wallet the own-wallet system accepted. */
54
+ readonly transactionHash?: string;
55
+ /** The final fate of that send's transfer; absent until transferStatus() read a final state. */
56
+ readonly transfer?: TransferOutcome;
57
+ }
58
+ export interface SendPage {
59
+ readonly requestId: string;
60
+ readonly items: readonly ListedSend[];
61
+ readonly hasMore: boolean;
62
+ readonly nextCursor?: string;
63
+ }
64
+ /** The path of one page of the send history. */
65
+ export declare function sendsPath(options?: PageOptions): {
66
+ path: string;
67
+ signal?: AbortSignal;
68
+ };
69
+ /** Parses one closed page of the send history. */
70
+ export declare function parseSendPage(payload: unknown): SendPage;
71
+ /**
72
+ * Says whether a wallet event is the network's record of the transaction with
73
+ * this hash on this network: true or false where an event's sourceReference
74
+ * is the transaction hash, and undefined on every other network, where
75
+ * nothing ties an event to a hash yet. On the EVM networks and TRON the two
76
+ * are one hexadecimal hash and differ at most in a 0x prefix and letter case.
77
+ * On Solana both are the transaction's signature in base58, where case is
78
+ * part of the value. On TON the event names the transaction the network made
79
+ * and the own-wallet system names the message it sent; the two hashes differ.
80
+ */
81
+ export declare function eventOfTransaction(event: WalletEvent, networkId: string, transactionHash: string): boolean | undefined;
82
+ /**
83
+ * The wallet events that record one listed send on its network: the
84
+ * withdrawal from the sending wallet and any activity of the same
85
+ * transaction. It returns undefined where events cannot be tied to the send
86
+ * (no hash yet, details purged, or a network without that tie), and an empty
87
+ * list where they can and none of the given events is of this send.
88
+ */
89
+ export declare function sendEvents(send: ListedSend, events: readonly WalletEvent[]): readonly WalletEvent[] | undefined;
package/dist/sends.js ADDED
@@ -0,0 +1,127 @@
1
+ // The account's history of sends: GET /v1/operations narrowed to wallet.send,
2
+ // newest first. Every row is one send with what a history shows of it: where,
3
+ // what, how much, to whom, its state, its transaction hash and, once the
4
+ // own-wallet system reported it, the final fate of its transfer.
5
+ import { ClientFlowError, opaqueId, record, rfc3339UTC } from './auth.js';
6
+ import { pageEnvelope, pageRequest } from './pages.js';
7
+ import { parseUserWalletOperation, walletMaterialPurged } from './walletOperations.js';
8
+ const hashPattern = /^[A-Za-z0-9_.:-]{1,128}$/;
9
+ /** The path of one page of the send history. */
10
+ export function sendsPath(options) {
11
+ const { path, signal } = pageRequest('/v1/operations', options);
12
+ return { path: `${path}${path.includes('?') ? '&' : '?'}kind=wallet.send`, signal };
13
+ }
14
+ /** Parses one closed page of the send history. */
15
+ export function parseSendPage(payload) {
16
+ const page = pageEnvelope(payload);
17
+ const items = page.items.map(parseListedSend);
18
+ // Newest first: a page out of order is not this list.
19
+ if (items.some((item, index) => index > 0 && Date.parse(items[index - 1].createdAt) < Date.parse(item.createdAt)))
20
+ fault();
21
+ const out = { requestId: page.request_id, items: Object.freeze(items), hasMore: page.has_more };
22
+ return Object.freeze(page.has_more ? { ...out, nextCursor: page.next_cursor } : out);
23
+ }
24
+ function parseListedSend(payload) {
25
+ const item = record(payload, ['operation'], ['transaction_hash', 'transfer']);
26
+ const raw = item.operation;
27
+ if (raw === null || typeof raw !== 'object' || Array.isArray(raw) || raw.kind !== 'wallet.send')
28
+ fault();
29
+ const send = listedSend(item.operation);
30
+ if (item.transaction_hash !== undefined) {
31
+ if (typeof item.transaction_hash !== 'string' || !hashPattern.test(item.transaction_hash) || send.linkedWalletId === undefined)
32
+ fault();
33
+ send.transactionHash = item.transaction_hash;
34
+ }
35
+ if (item.transfer !== undefined) {
36
+ if (send.linkedWalletId === undefined)
37
+ fault();
38
+ send.transfer = parseTransferOutcome(item.transfer);
39
+ if (send.transfer.replacedByOperationId === send.operationId)
40
+ fault();
41
+ }
42
+ return Object.freeze(send);
43
+ }
44
+ /**
45
+ * One send of the account: from a linked wallet authorized by its session,
46
+ * confirmed with its passkey, or forwarded by a service on the person's signed
47
+ * request (Mode C, from a bound or a linked wallet), each read with the parser
48
+ * of the projection the provider serves for it.
49
+ */
50
+ function listedSend(payload) {
51
+ const operation = parseUserWalletOperation(payload);
52
+ const send = {
53
+ operationId: operation.id, state: operation.state, effectState: operation.effectState, authorizationBasis: operation.authorizationBasis,
54
+ createdAt: operation.createdAt, expiresAt: operation.expiresAt, materialPurged: walletMaterialPurged(operation.display),
55
+ };
56
+ if (operation.linkedWallet)
57
+ send.linkedWalletId = operation.linkedWallet.linkedWalletId;
58
+ if (operation.walletId !== undefined)
59
+ send.walletId = operation.walletId;
60
+ const display = operation.display;
61
+ if (!walletMaterialPurged(display) && display.kind === 'wallet.send') {
62
+ send.network = display.network;
63
+ send.asset = display.asset;
64
+ send.recipient = display.recipient;
65
+ send.amountRaw = display.amountRaw;
66
+ if ('address' in display && display.address !== undefined)
67
+ send.address = display.address;
68
+ if ('comment' in display && display.comment !== undefined)
69
+ send.comment = display.comment;
70
+ if ('replacesOperationId' in display && display.replacesOperationId !== undefined) {
71
+ send.replacesOperationId = display.replacesOperationId;
72
+ send.replaceAction = display.replaceAction;
73
+ }
74
+ }
75
+ return send;
76
+ }
77
+ function parseTransferOutcome(payload) {
78
+ const t = record(payload, ['observed_at', 'state'], ['transaction_hash', 'failure_reason', 'replaced_by_operation_id']);
79
+ const hash = t.transaction_hash, reason = t.failure_reason, replacedBy = t.replaced_by_operation_id;
80
+ if (!['confirmed', 'failed', 'expired'].includes(t.state) || !rfc3339UTC(t.observed_at) ||
81
+ (hash !== undefined && (typeof hash !== 'string' || !hashPattern.test(hash))) || (t.state === 'confirmed' && hash === undefined) ||
82
+ (reason !== undefined && (t.state !== 'failed' || typeof reason !== 'string' || reason.length === 0 || new TextEncoder().encode(reason).length > 1024 || /[\x00-\x1f\x7f-\x9f]/.test(reason))) ||
83
+ (replacedBy !== undefined && (reason !== 'nonce_used' || !opaqueId(replacedBy))))
84
+ fault();
85
+ const out = { state: t.state, observedAt: t.observed_at };
86
+ if (hash !== undefined)
87
+ out.transactionHash = hash;
88
+ if (replacedBy !== undefined)
89
+ out.replacedByOperationId = replacedBy;
90
+ if (reason !== undefined)
91
+ out.failureReason = reason;
92
+ return Object.freeze(out);
93
+ }
94
+ /**
95
+ * Says whether a wallet event is the network's record of the transaction with
96
+ * this hash on this network: true or false where an event's sourceReference
97
+ * is the transaction hash, and undefined on every other network, where
98
+ * nothing ties an event to a hash yet. On the EVM networks and TRON the two
99
+ * are one hexadecimal hash and differ at most in a 0x prefix and letter case.
100
+ * On Solana both are the transaction's signature in base58, where case is
101
+ * part of the value. On TON the event names the transaction the network made
102
+ * and the own-wallet system names the message it sent; the two hashes differ.
103
+ */
104
+ export function eventOfTransaction(event, networkId, transactionHash) {
105
+ const family = networkId.split(':', 1)[0];
106
+ if (family === 'solana')
107
+ return event.networkId === networkId && /^[1-9A-HJ-NP-Za-km-z]{64,88}$/.test(transactionHash) && event.sourceReference === transactionHash;
108
+ if (family !== 'eip155' && family !== 'tron')
109
+ return undefined;
110
+ const bare = (value) => value.toLowerCase().replace(/^0x/, '');
111
+ return event.networkId === networkId && /^(0x)?[0-9a-fA-F]{64}$/.test(transactionHash) && bare(event.sourceReference) === bare(transactionHash);
112
+ }
113
+ /**
114
+ * The wallet events that record one listed send on its network: the
115
+ * withdrawal from the sending wallet and any activity of the same
116
+ * transaction. It returns undefined where events cannot be tied to the send
117
+ * (no hash yet, details purged, or a network without that tie), and an empty
118
+ * list where they can and none of the given events is of this send.
119
+ */
120
+ export function sendEvents(send, events) {
121
+ if (send.network === undefined || send.transactionHash === undefined)
122
+ return undefined;
123
+ if (eventOfTransaction({ networkId: send.network, sourceReference: '' }, send.network, send.transactionHash) === undefined)
124
+ return undefined;
125
+ return events.filter((event) => eventOfTransaction(event, send.network, send.transactionHash) === true);
126
+ }
127
+ function fault() { throw new ClientFlowError('invalid_server_response'); }
@@ -0,0 +1,173 @@
1
+ import { type IdentityMethod, type NextAction } from "./auth.ts";
2
+ import { type WalletLimitsDisplay } from "./walletLimits.ts";
3
+ import { type LinkedEnrollmentDisplay, type LinkedRequestGrantEnrollmentCreate, type LinkedRequestGrantResult } from "./modeCLinked.ts";
4
+ export interface HostedIdentityFlow {
5
+ requestId: string;
6
+ flowId: string;
7
+ launchUrl: string;
8
+ expiresAt: number;
9
+ }
10
+ export interface PasskeyBootstrapResult {
11
+ requestId: string;
12
+ credentialId: string;
13
+ version: string;
14
+ state: "active";
15
+ }
16
+ export interface VerifiedIdentityResult {
17
+ requestId: string;
18
+ verifiedIdentityHandle: string;
19
+ expiresAt: number;
20
+ }
21
+ export type IdentityVerificationMethod = IdentityMethod;
22
+ /**
23
+ * AccountIdentity is one active sign-in method of the account. A passkey
24
+ * carries credentialId: the credential to name when a confirmation is asked
25
+ * for, also in a browser that did not create that passkey.
26
+ */
27
+ export interface AccountIdentity {
28
+ identityId: string;
29
+ method: IdentityMethod;
30
+ credentialId?: string;
31
+ createdAt: string;
32
+ }
33
+ /** parseIdentityList accepts the closed answer of GET /v1/identities. */
34
+ export declare function parseIdentityList(payload: unknown): AccountIdentity[];
35
+ /**
36
+ * wallet_limits.set changes a linked wallet's limits (LinkedWallets.setLimits); its result kind is wallet_limits.
37
+ * session.authorize_requests enrolls a Mode C request key on a linked wallet (modeCLinked.ts); its result kind is
38
+ * request_grant, with the grant.
39
+ */
40
+ export type LocalOperationKind = "identity.link" | "identity.unlink" | "session.revoke" | "service_grant.set" | "wallet_limits.set" | "session.authorize_requests";
41
+ export type LocalResultKind = "identity" | "session" | "service_grant" | "wallet_limits" | "request_grant";
42
+ export type LocalResultState = "active" | "revoked";
43
+ export type ServiceReadScope = "wallet:read" | "operation:read" | "streams:read";
44
+ export type ResultRecipient = {
45
+ type: "account" | "service";
46
+ id: string;
47
+ };
48
+ type LocalOperationCommon = {
49
+ owner_type: "account";
50
+ owner_id: string;
51
+ result_recipient?: ResultRecipient;
52
+ };
53
+ export type LocalOperationCreate = (LocalOperationCommon & {
54
+ kind: "identity.link";
55
+ payload: {
56
+ verified_identity_handle: string;
57
+ };
58
+ }) | (LocalOperationCommon & {
59
+ kind: "identity.unlink";
60
+ payload: {
61
+ identity_id: string;
62
+ };
63
+ }) | (LocalOperationCommon & {
64
+ kind: "session.revoke";
65
+ payload: {
66
+ session_id: string;
67
+ };
68
+ }) | (LocalOperationCommon & {
69
+ kind: "service_grant.set";
70
+ wallet_id: string;
71
+ payload: {
72
+ service_principal_id: string;
73
+ enabled: boolean;
74
+ scopes: ServiceReadScope[];
75
+ external_ref?: string;
76
+ };
77
+ }) | LinkedRequestGrantEnrollmentCreate;
78
+ export type OperationDisplay = {
79
+ kind: "identity.link";
80
+ ownerId: string;
81
+ requesterId: string;
82
+ method: IdentityMethod;
83
+ } | {
84
+ kind: "identity.unlink";
85
+ ownerId: string;
86
+ requesterId: string;
87
+ identityId: string;
88
+ } | {
89
+ kind: "session.revoke";
90
+ ownerId: string;
91
+ requesterId: string;
92
+ sessionId: string;
93
+ } | {
94
+ kind: "service_grant.set";
95
+ ownerId: string;
96
+ requesterId: string;
97
+ walletId: string;
98
+ servicePrincipalId: string;
99
+ enabled: boolean;
100
+ scopes: ServiceReadScope[];
101
+ externalRef?: string;
102
+ } | WalletLimitsDisplay | LinkedEnrollmentDisplay;
103
+ export interface LocalOperation {
104
+ id: string;
105
+ version: string;
106
+ kind: LocalOperationKind;
107
+ state: "awaiting_confirmation" | "authorized" | "succeeded" | "denied" | "canceled" | "expired";
108
+ effectState: "no_effect" | "known";
109
+ authorizationBasis: "" | "user_confirmed";
110
+ ownerType: "account";
111
+ ownerId: string;
112
+ requesterId: string;
113
+ walletId?: string;
114
+ requestFingerprint: string;
115
+ operationFingerprint: string;
116
+ display: OperationDisplay;
117
+ /** UTC RFC 3339 date-time string (source §2.2), not NumericDate seconds. */
118
+ createdAt: string;
119
+ /** UTC RFC 3339 date-time string (source §2.2), not NumericDate seconds. */
120
+ expiresAt: string;
121
+ confirmationLocation?: string;
122
+ resultKind?: LocalResultKind;
123
+ nextAction: NextAction;
124
+ }
125
+ export interface ConfirmationChallenge {
126
+ requestId: string;
127
+ challengeId: string;
128
+ publicKey: {
129
+ challenge: string;
130
+ rpId: string;
131
+ timeoutMs: number;
132
+ allowCredentials: [{
133
+ type: "public-key";
134
+ id: string;
135
+ }];
136
+ userVerification: "required";
137
+ };
138
+ confirmationLocation: string;
139
+ /** UTC RFC 3339 date-time string (source §2.2), not NumericDate seconds. */
140
+ expiresAt: string;
141
+ }
142
+ export interface LocalOperationResult {
143
+ requestId: string;
144
+ /** grant is present for the request_grant kind of a linked wallet's enrollment. */
145
+ result: {
146
+ kind: LocalResultKind;
147
+ targetId: string;
148
+ newVersion: string;
149
+ state: LocalResultState;
150
+ grant?: LinkedRequestGrantResult;
151
+ };
152
+ }
153
+ export declare function parseHostedIdentityFlow(payload: unknown, now: number): HostedIdentityFlow;
154
+ export declare function parsePasskeyBootstrapResult(payload: unknown): PasskeyBootstrapResult;
155
+ export declare function parseVerifiedIdentityResult(payload: unknown, now: number): VerifiedIdentityResult;
156
+ export declare function parseIdentityFlowCallback(value: string, expectedRedirect: string): {
157
+ flowId: string;
158
+ state: string;
159
+ code: string;
160
+ };
161
+ export declare function validateLocalOperationCreate(value: LocalOperationCreate, expectedOwnerId: string): LocalOperationCreate;
162
+ export declare function parseOperationEnvelope(payload: unknown): {
163
+ requestId: string;
164
+ operation: LocalOperation;
165
+ };
166
+ export declare function parseOperation(payload: unknown): LocalOperation;
167
+ export declare function parseConfirmationChallenge(payload: unknown, now: number): ConfirmationChallenge;
168
+ export declare function parseOperationConfirmationCallback(value: string, expectedRedirect: string): {
169
+ state: string;
170
+ operationId: string;
171
+ };
172
+ export declare function parseLocalOperationResult(payload: unknown): LocalOperationResult;
173
+ export {};