@shuttle.technology/authorization 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +708 -2
- package/dist/aesGcmSiv.d.ts +14 -0
- package/dist/aesGcmSiv.js +160 -0
- package/dist/auth.d.ts +68 -0
- package/dist/auth.js +215 -0
- package/dist/balances.d.ts +31 -0
- package/dist/balances.js +44 -0
- package/dist/canonicalize.d.ts +19 -0
- package/dist/canonicalize.js +107 -0
- package/dist/client.d.ts +264 -0
- package/dist/client.js +754 -0
- package/dist/deviceKey.d.ts +13 -0
- package/dist/deviceKey.js +28 -0
- package/dist/deviceShare.d.ts +9 -0
- package/dist/deviceShare.js +24 -0
- package/dist/deviceShareEnvelope.d.ts +13 -0
- package/dist/deviceShareEnvelope.js +139 -0
- package/dist/linkedChecks.d.ts +17 -0
- package/dist/linkedChecks.js +31 -0
- package/dist/linkedSendDisplay.d.ts +73 -0
- package/dist/linkedSendDisplay.js +84 -0
- package/dist/linkedWalletSignals.d.ts +22 -0
- package/dist/linkedWalletSignals.js +101 -0
- package/dist/linkedWallets.d.ts +373 -0
- package/dist/linkedWallets.js +471 -0
- package/dist/mediatedOperation.d.ts +27 -0
- package/dist/mediatedOperation.js +68 -0
- package/dist/modeC.d.ts +131 -0
- package/dist/modeC.js +248 -0
- package/dist/modeCLinked.d.ts +207 -0
- package/dist/modeCLinked.js +270 -0
- package/dist/pages.d.ts +22 -0
- package/dist/pages.js +45 -0
- package/dist/poll.d.ts +15 -0
- package/dist/poll.js +76 -0
- package/dist/requestGrant.d.ts +169 -0
- package/dist/requestGrant.js +243 -0
- package/dist/sends.d.ts +89 -0
- package/dist/sends.js +127 -0
- package/dist/slice4.d.ts +173 -0
- package/dist/slice4.js +285 -0
- package/dist/streams.d.ts +212 -0
- package/dist/streams.js +282 -0
- package/dist/trackedWallets.d.ts +53 -0
- package/dist/trackedWallets.js +104 -0
- package/dist/transferStatus.d.ts +45 -0
- package/dist/transferStatus.js +43 -0
- package/dist/walletDisplayParts.d.ts +15 -0
- package/dist/walletDisplayParts.js +43 -0
- package/dist/walletEvents.d.ts +43 -0
- package/dist/walletEvents.js +97 -0
- package/dist/walletLimits.d.ts +65 -0
- package/dist/walletLimits.js +102 -0
- package/dist/walletOperations.d.ts +192 -0
- package/dist/walletOperations.js +308 -0
- package/package.json +64 -4
package/README.md
CHANGED
|
@@ -1,3 +1,709 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Streams Authorization SDK
|
|
2
2
|
|
|
3
|
-
This
|
|
3
|
+
This package is the framework-independent TypeScript client for the public
|
|
4
|
+
Mode B authorization flow. It holds access tokens in memory. Web refresh uses
|
|
5
|
+
the provider's secure HTTP-only cookie; mobile refresh tokens are available
|
|
6
|
+
only through the required secure-storage port.
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
npm install @shuttle.technology/authorization
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The package ships compiled ESM and TypeScript declarations. Its public entry
|
|
15
|
+
points are:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { initializeAuthorizationClient } from "@shuttle.technology/authorization";
|
|
19
|
+
import { signMediatedRequest } from "@shuttle.technology/authorization/mode-c";
|
|
20
|
+
import { signLinkedMediatedRequest } from "@shuttle.technology/authorization/mode-c-linked";
|
|
21
|
+
import { StreamsReads } from "@shuttle.technology/authorization/streams";
|
|
22
|
+
import { LinkedWallets } from "@shuttle.technology/authorization/linked-wallets";
|
|
23
|
+
import { generateDeviceKey } from "@shuttle.technology/authorization/device-key";
|
|
24
|
+
import { resealDeviceShare } from "@shuttle.technology/authorization/device-share";
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Publishing the package changes only how applications install it. It does not
|
|
28
|
+
add a server-managed custody mode. Mode A still requires the customer backend
|
|
29
|
+
to retain its device key and the sealed device share, then reseal and submit
|
|
30
|
+
that share when a wallet operation is executed.
|
|
31
|
+
|
|
32
|
+
## Web initialization
|
|
33
|
+
|
|
34
|
+
`initializeAuthorizationClient` checks the current HTTPS application origin,
|
|
35
|
+
then reads `GET /v1/public/clients/{id}`. It accepts only the exact configured
|
|
36
|
+
client, environment, method set, provider origin, redirect, and web platform.
|
|
37
|
+
Initialization creates no session authority.
|
|
38
|
+
|
|
39
|
+
Web restore is a top-level handoff. `restoreSession` keeps a fresh state and
|
|
40
|
+
S256 verifier in memory, then calls `openHosted` with the provider restore URL.
|
|
41
|
+
The host must open a separate top-level browsing context and return the exact
|
|
42
|
+
registered-app redirect URL after completion. A popup is one valid host
|
|
43
|
+
implementation. Full-page navigation that destroys the calling SDK instance
|
|
44
|
+
is not supported silently: if the host cannot return the redirect,
|
|
45
|
+
`restoreSession` fails locally. The SDK never stores restore state, verifier,
|
|
46
|
+
access token, CSRF token, or refresh cookie in browser storage.
|
|
47
|
+
|
|
48
|
+
## Mobile host boundary
|
|
49
|
+
|
|
50
|
+
Mobile configuration embeds only public provider configuration and a public
|
|
51
|
+
bundle/package identifier. The caller must supply:
|
|
52
|
+
|
|
53
|
+
- `MobileBrowserBridge`, implemented by the host/native layer;
|
|
54
|
+
- `MobileSecureStorage`, backed by platform-protected storage.
|
|
55
|
+
|
|
56
|
+
`MobileBrowserBridge.verifyAssociatedRedirect` must ask the operating system
|
|
57
|
+
to validate the registered HTTPS app/universal-link association for the exact
|
|
58
|
+
redirect URL and configured bundle/package identifier. The same bridge opens
|
|
59
|
+
the system browser. There is no default verifier, claimed bundle header,
|
|
60
|
+
custom-scheme fallback, or embedded webview.
|
|
61
|
+
|
|
62
|
+
The structured verification result is a host-port contract. It is not
|
|
63
|
+
cryptographic proof generated by this TypeScript package. A missing bridge,
|
|
64
|
+
unavailable or failing OS check, or any redirect/application mismatch fails
|
|
65
|
+
locally before an authentication request or browser launch.
|
|
66
|
+
|
|
67
|
+
If secure storage cannot persist a rotated mobile refresh successor, the SDK
|
|
68
|
+
deletes the predecessor best-effort, clears its in-memory session, and reports
|
|
69
|
+
`secure_storage_unavailable`. This prevents the SDK from knowingly retaining a
|
|
70
|
+
consumed predecessor. A failing storage provider can prevent proof that the
|
|
71
|
+
predecessor was erased; recovery is a new login, and the SDK does not report
|
|
72
|
+
server revocation from this local failure.
|
|
73
|
+
|
|
74
|
+
## Passkey proof and local operations
|
|
75
|
+
|
|
76
|
+
`bootstrapPasskey` and `verifyIdentity` start provider-hosted flows with a
|
|
77
|
+
fresh state and outer S256 verifier held only in memory. The host browser bridge
|
|
78
|
+
opens the returned HTTPS location and later delivers the exact registered-app
|
|
79
|
+
redirect to `completePasskeyBootstrap` or `completeIdentityVerification`.
|
|
80
|
+
Those redirects contain only one-use `code`, `state`, and `flow_id` values. A
|
|
81
|
+
hosted URL or flow ID alone is never authority, and neither web refresh cookies
|
|
82
|
+
nor mobile refresh storage are used as confirmation authority.
|
|
83
|
+
|
|
84
|
+
New passkey verification is a server-hosted registration followed by a
|
|
85
|
+
separate possession assertion. Fresh operation confirmation instead uses an
|
|
86
|
+
already linked passkey. The SDK requests that challenge, opens the server
|
|
87
|
+
confirmation page, validates the returned `state` and `operation_id`, and then
|
|
88
|
+
rereads authoritative operation status. It never accepts caller-supplied
|
|
89
|
+
WebAuthn options, auto-confirms, or auto-executes an operation.
|
|
90
|
+
|
|
91
|
+
The Slice 4 local-operation helpers cover `identity.link`, `identity.unlink`,
|
|
92
|
+
`session.revoke`, and `service_grant.set`. Creation, confirmation, execution,
|
|
93
|
+
and protected result reads remain separate calls. Write failures are returned
|
|
94
|
+
to the caller without automatic retries; `executeOperation` requires the
|
|
95
|
+
explicit operation version and idempotency key. Safe results expose only
|
|
96
|
+
`kind`, `targetId`, canonical `newVersion`, and `state`.
|
|
97
|
+
|
|
98
|
+
`createOperation` accepts the source-defined optional closed
|
|
99
|
+
`result_recipient`. Omitting it requests the owner-account default. Supplying
|
|
100
|
+
it does not grant result access: the server must resolve and authorize that
|
|
101
|
+
account or service before freezing the operation, and later rechecks current
|
|
102
|
+
recipient authority on the protected result read.
|
|
103
|
+
|
|
104
|
+
## Mode C: request-key enrollment and typed request proofs
|
|
105
|
+
|
|
106
|
+
`src/modeC.ts` is the package's second entry point (`@shuttle.technology/authorization/mode-c`).
|
|
107
|
+
It exposes exactly two client responsibilities and nothing else.
|
|
108
|
+
|
|
109
|
+
`generateRequestSigningKey` creates the non-extractable P-256 WebCrypto key of
|
|
110
|
+
source §2.5 and exports only its `kty/crv/x/y` JWK and RFC 7638 thumbprint. The
|
|
111
|
+
private `CryptoKey` never leaves that object, refuses `exportKey`, and is held
|
|
112
|
+
only in memory. `buildRequestGrantEnrollment` produces the closed
|
|
113
|
+
`session.authorize_requests` payload; `enrollRequestGrant` drives that operation
|
|
114
|
+
through the four separate calls the operation contract requires — create, fresh
|
|
115
|
+
provider-hosted passkey confirmation, explicit execute, and the protected
|
|
116
|
+
result read — behind one function. Enrollment never runs from a refresh, reload
|
|
117
|
+
or login callback.
|
|
118
|
+
|
|
119
|
+
`RequestGrantStore` retains one protected grant result beside the exact key that
|
|
120
|
+
produced it, in memory only. It refuses to pair a result whose RFC 7638
|
|
121
|
+
thumbprint, owner, service, wallet, network or bounds disagree with the
|
|
122
|
+
enrollment that was submitted, and it reports
|
|
123
|
+
`request_grant_reenrollment_required` when the pairing is absent, released or
|
|
124
|
+
past its deadline. It never renews, re-enrolls or serializes the grant.
|
|
125
|
+
|
|
126
|
+
`prepareMediatedTransfer` derives the displayed typed intent and the exact
|
|
127
|
+
closed `OperationCreate` from the grant binding: the caller supplies only the
|
|
128
|
+
recipient, amount and fee, and every value outside the frozen allowlist and
|
|
129
|
+
ceilings is refused locally. `signMediatedRequest` rederives that intent from
|
|
130
|
+
the grant and refuses a display that disagrees with the signed body before any
|
|
131
|
+
crypto call; it then canonicalizes with RFC 8785, takes the lowercase SHA-256
|
|
132
|
+
`request_hash`, mints a 256-bit `jti` that is also the `Idempotency-Key`, bounds
|
|
133
|
+
`exp` to sixty seconds and the grant deadline, and produces the compact ES256
|
|
134
|
+
JWS with the closed `{alg,typ,kid}` protected header. It accepts no bytes, no
|
|
135
|
+
payload and no proof from a caller, so arbitrary or backend-supplied material is
|
|
136
|
+
rejected with `opaque_signing_input_refused` and never signed.
|
|
137
|
+
|
|
138
|
+
`ModeCServerClock` carries the authenticated server time: it is seeded from the
|
|
139
|
+
grant result's `server_time` and refreshed only from a current `/v1/me`
|
|
140
|
+
response. Clock disagreement is resolved by adopting that reading and signing a
|
|
141
|
+
new proof with a new `jti` while the grant is still live; no local override or
|
|
142
|
+
tolerance exists.
|
|
143
|
+
|
|
144
|
+
Forwarding and refusal are the customer backend's, not the SDK's. This package
|
|
145
|
+
exports no forward, approve, refuse or backend-signing helper, and proof
|
|
146
|
+
generation performs no HTTP. `test/customerBackendFixture.ts` is a test-only
|
|
147
|
+
stand-in for that backend. Operation status and result reads use the generic
|
|
148
|
+
helpers in `client.ts`.
|
|
149
|
+
|
|
150
|
+
`session.authorize_requests` is not served yet, so `enrollRequestGrant` is
|
|
151
|
+
exercised against the operation contract rather than the live route; the
|
|
152
|
+
runtime harness supplies the protected grant result through its fixture and the
|
|
153
|
+
SDK's own payload builder, result parser and pairing rules still run unchanged.
|
|
154
|
+
|
|
155
|
+
### Mode C on a linked wallet
|
|
156
|
+
|
|
157
|
+
`src/modeCLinked.ts` (`@shuttle.technology/authorization/mode-c-linked`) is Mode C for the
|
|
158
|
+
wallets a person created through Authorization (owner decision of 14 October
|
|
159
|
+
2026). The person allows, once and with the passkey, transfers their app signs
|
|
160
|
+
and their app's backend forwards; each transfer then goes with no passkey.
|
|
161
|
+
|
|
162
|
+
1. `generateRequestSigningKey` makes the key; `buildLinkedRequestGrantEnrollment`
|
|
163
|
+
makes the closed `session.authorize_requests` body: the linked wallet, the
|
|
164
|
+
network the transfers are made on, one to three assets (`native`, `usdt`,
|
|
165
|
+
`usdc`) each with the most one transfer may move (`maxAmountRaw`), the most
|
|
166
|
+
the network fee may be (`maxFeeRaw`, compared with the Wallet's estimate when
|
|
167
|
+
the transfer is prepared) and up to ten recipients, none for any (each
|
|
168
|
+
compared as the provider compares addresses: EVM without case, TON in its
|
|
169
|
+
raw form). `client.createOperation()` admits it,
|
|
170
|
+
`challengeOperation()` and `completeOperationConfirmation()` take the passkey,
|
|
171
|
+
`executeOperation()` issues the grant and `operationResult()` returns it
|
|
172
|
+
(`result.grant`). `pairLinkedRequestGrant` keeps it beside its key and refuses
|
|
173
|
+
a grant on other bounds. The grant lives at most 15 minutes, never past the
|
|
174
|
+
session, and survives the person's later sign-ins and confirmations with the
|
|
175
|
+
same passkey; removing that passkey ends it.
|
|
176
|
+
2. `prepareLinkedMediatedTransfer(paired, {asset, recipient, amountRaw, comment?})`
|
|
177
|
+
is the transfer the app shows; what the grant does not cover is refused
|
|
178
|
+
locally. A comment is carried on TON only.
|
|
179
|
+
3. The app reads the Wallet's transport key (`linkedWallets().transportKey()`)
|
|
180
|
+
and reseals the device share for this transfer (`resealDeviceShare`), then
|
|
181
|
+
`signLinkedMediatedRequest({grant, prepared, deviceShare, crypto,
|
|
182
|
+
serverNowSeconds})` signs the 2.0 proof over the transfer and over the
|
|
183
|
+
SHA-256 of the share (`deviceShareDigest`), and returns `request`
|
|
184
|
+
(`{operation, user_proof, device_share}`) and `idempotencyKey` (the proof's
|
|
185
|
+
`jti`). The backend forwards `request` unchanged with its service key and
|
|
186
|
+
that Idempotency-Key, within the proof's minute, to
|
|
187
|
+
`POST /v1/service/mediated-operations`
|
|
188
|
+
(`examples/backend-client`: `cli.ts mediate <file>`).
|
|
189
|
+
|
|
190
|
+
The backend cannot change the transfer, swap the share or sign a new request.
|
|
191
|
+
The wallet's own limits apply to every transfer.
|
|
192
|
+
|
|
193
|
+
The person reads a forwarded transfer on their own session like any send:
|
|
194
|
+
`client.walletOperation(id)` and `linkedWallets().sends()` read it in the
|
|
195
|
+
mediated projection the provider serves for it (`src/mediatedOperation.ts`):
|
|
196
|
+
`authorizationBasis` `user_request_signed`, `linkedWallet` naming the wallet,
|
|
197
|
+
and a display with no result recipient (`MediatedLinkedSendDisplay`; a
|
|
198
|
+
transfer from a wallet bound in the provider has `walletId` and a
|
|
199
|
+
`MediatedSendDisplay`). `transferStatus()` and `replaceSend()` work for it as
|
|
200
|
+
for a session send.
|
|
201
|
+
|
|
202
|
+
## Creating a wallet and reaching it
|
|
203
|
+
|
|
204
|
+
`src/deviceKey.ts` is the package's device-key entry point
|
|
205
|
+
(`@shuttle.technology/authorization/device-key`). `generateDeviceKey` creates a
|
|
206
|
+
non-extractable ECDH P-256 key pair and returns its compressed public key, the
|
|
207
|
+
`recipientPublicKey` of `createWallet`. The module persists nothing: the
|
|
208
|
+
application keeps the private key handle and the sealed device share it later
|
|
209
|
+
receives in storage that can hold a non-extractable key.
|
|
210
|
+
|
|
211
|
+
A completed `wallet.create` releases a `wallet_created` result through
|
|
212
|
+
`walletOperationResult`: the public wallet identity and the device share
|
|
213
|
+
exactly as the own-wallet system sealed it. The provider never opens the share.
|
|
214
|
+
Where the server has no native Wallet profile the creation stays
|
|
215
|
+
`CAPABILITY_UNAVAILABLE`, as before.
|
|
216
|
+
|
|
217
|
+
`linkedWallets().refresh()` binds the account to the Wallet identity the
|
|
218
|
+
provider owns for it and synchronizes the public wallet inventory. It carries
|
|
219
|
+
no Wallet token; `challenge`, `connect` and `sync` with a caller-supplied token
|
|
220
|
+
remain for deployments without that profile.
|
|
221
|
+
|
|
222
|
+
Tracking a linked wallet needs no passkey: it signs with no wallet key, so the
|
|
223
|
+
account's session authorizes it. `linkedWallets().register()` comes back
|
|
224
|
+
`authorized` with the basis `session_authorized`, and
|
|
225
|
+
`executeRegistration(operation.id, operation.version, key)` dispatches it; no
|
|
226
|
+
hosted step takes part. A registration covers one network. An EVM wallet has
|
|
227
|
+
one address on every EVM network, so register it once per EVM network;
|
|
228
|
+
`events()` then returns the events of all of them, each with its
|
|
229
|
+
`networkId`.
|
|
230
|
+
|
|
231
|
+
`register()` takes an optional `activityTypes`: the kinds of activity events
|
|
232
|
+
asked for beside deposits, for example `"transaction"` or `"erc20.approval"`.
|
|
233
|
+
It accepts at most 16 distinct kinds, each matching
|
|
234
|
+
`^[a-z][a-z0-9]*(?:[._][a-z0-9]+)*$`, and refuses any other value with
|
|
235
|
+
`invalid_operation_create` before a request is sent. `activity_types` is sent
|
|
236
|
+
only when the list is not empty; omitted or empty means deposits only. The
|
|
237
|
+
server sorts the kinds. Streams decides which kinds a network has and refuses
|
|
238
|
+
the registration otherwise. `createRegistration()` accepts the same list as
|
|
239
|
+
`payload.activity_types`.
|
|
240
|
+
|
|
241
|
+
`linkedWallets().signals(linkedWalletIds, { onOpen, onWallets, signal })`
|
|
242
|
+
opens one signal stream for 1 to 32 linked wallets and reads it to its end. On
|
|
243
|
+
the stream Authorization names the wallets that have new events; it carries no
|
|
244
|
+
event, and the wallets named are read with `events()`. `onOpen` says the
|
|
245
|
+
stream is live: read every wallet once then, so that nothing falls between
|
|
246
|
+
that read and the first signal. `onWallets` receives the wallets to read now.
|
|
247
|
+
The promise resolves with why the stream ended: `expired`, `server_draining`
|
|
248
|
+
or `connection_closed`. A stream lives no longer than an access token, so the
|
|
249
|
+
caller opens the next one; the SDK renews the token when needed. The promise
|
|
250
|
+
rejects with the server's refusal, with the abort reason, or when the
|
|
251
|
+
connection fails or the answer is not this protocol. A signal is a hint: keep
|
|
252
|
+
a timed read, only slower, for the times no stream is open. `demo/live.js`
|
|
253
|
+
shows the loop.
|
|
254
|
+
|
|
255
|
+
A send from a linked wallet needs no passkey either (owner decision, 14
|
|
256
|
+
October 2026): the account's live session authorizes it. A passkey is still
|
|
257
|
+
asked at sign-in, when a wallet is created and when its limits change
|
|
258
|
+
(below). A send takes three calls, which the application makes one after
|
|
259
|
+
another; the demo page makes all three on one press of «Отправить», with no
|
|
260
|
+
confirmation step (Tony's decision, 15 October 2026), and says the fee
|
|
261
|
+
estimate once the transfer was sent:
|
|
262
|
+
|
|
263
|
+
1. `linkedWallets().send(input, key)` admits it. It comes back `authorized`
|
|
264
|
+
with the basis `session_authorized`, `nextAction` `none` and no
|
|
265
|
+
`confirmationLocation`; nothing is asked of the own-wallet system yet.
|
|
266
|
+
2. `linkedWallets().prepare(operation.id)` has the own-wallet system prepare
|
|
267
|
+
the transfer and name its estimate of the network fee, and answers the
|
|
268
|
+
operation with what was prepared. It takes no body, key or version; the
|
|
269
|
+
transfer is prepared once, and calling it again answers the same prepared
|
|
270
|
+
send. The display then carries `network`, `asset`, `amountRaw`,
|
|
271
|
+
`recipient`, `comment` where there is one, `preparedSummary` and the fee.
|
|
272
|
+
3. Read `transportKey()`, reseal the device share to it
|
|
273
|
+
(`resealDeviceShare`) and call `executeSend(operation.id,
|
|
274
|
+
operation.version, key, share)`.
|
|
275
|
+
|
|
276
|
+
The display carries `feeEstimate` from the preparation on: `estimated` with
|
|
277
|
+
`estimatedFeeRaw`, in minimal units of the network's own coin, or
|
|
278
|
+
`unavailable` where that system could estimate none, which is never a fee of
|
|
279
|
+
zero. The fee is paid on top of the amount, also for a token. It is an
|
|
280
|
+
estimate: it does not bound what the network takes. The preparation caps the
|
|
281
|
+
operation at 150 seconds: an `executeSend()` after that is refused with
|
|
282
|
+
`OPERATION_EXPIRED` and the send is started again with `send()`. An `executeSend()` of a send
|
|
283
|
+
that was never prepared is refused with `VERSION_CONFLICT` before anything is
|
|
284
|
+
claimed. A send an earlier version of the provider admitted that still waits
|
|
285
|
+
for its passkey is confirmed the old way (`challengeWalletOperation()`, the hosted
|
|
286
|
+
page, `completeWalletOperationConfirmation()`); `prepare()` refuses it with
|
|
287
|
+
`VERSION_CONFLICT`.
|
|
288
|
+
|
|
289
|
+
A send from a linked wallet (`send()`, `prepare()`, `executeSend()`) ends as
|
|
290
|
+
an operation when the own-wallet system
|
|
291
|
+
accepted the transfer and named its hash: the operation is `succeeded` and its
|
|
292
|
+
result carries the hash as `receiptId`. What the network then did with the
|
|
293
|
+
transfer is read with `linkedWallets().transferStatus(operationId)`. It answers
|
|
294
|
+
for the one transfer that operation prepared, with the own-wallet system's
|
|
295
|
+
state of it: `prepared`, `executing`, `broadcasting`, `submitted`, `confirmed`,
|
|
296
|
+
`failed` or `expired`. `final` is true for the last three. `transactionHash` is
|
|
297
|
+
present once the transfer is signed; `failureReason` is the network's reason
|
|
298
|
+
for a failure, when it gave one: its error code, or the own-wallet system's
|
|
299
|
+
`expired`, `nonce_used` or `bounced`. `expired` means the transfer was never
|
|
300
|
+
executed and moved no funds. Read it until `final`: while the state can still
|
|
301
|
+
change the own-wallet system is asked on every read; the first final state is
|
|
302
|
+
recorded by the provider and answered from the record afterwards. A send that
|
|
303
|
+
was never prepared has no transfer and answers `NOT_FOUND`; a transfer whose
|
|
304
|
+
fate was never read final before the 30-day material retention removed its
|
|
305
|
+
identifier answers `RESULT_EXPIRED`; an own-wallet system that did not answer
|
|
306
|
+
is `DEPENDENCY_UNAVAILABLE` and worth repeating.
|
|
307
|
+
|
|
308
|
+
The preparation of a send can be refused in two ways that pass by themselves;
|
|
309
|
+
both are `retryable` with `nextAction` `retry_same_request`, nothing is
|
|
310
|
+
prepared, and the same `prepare()` succeeds later:
|
|
311
|
+
|
|
312
|
+
- `TRANSFER_IN_FLIGHT` (409): an earlier transfer of the same wallet is not in
|
|
313
|
+
a block yet and holds the wallet's place in the network's sequence. It comes
|
|
314
|
+
on Ethereum, BNB Smart Chain and TON from an own-wallet system that sends one
|
|
315
|
+
transfer of a wallet at a time.
|
|
316
|
+
- `NETWORK_FEE_TOO_HIGH` (503): the network fee is above the ceiling the
|
|
317
|
+
own-wallet system sends at. The preparation of a replacement answers it too.
|
|
318
|
+
|
|
319
|
+
A preparation the own-wallet system or its network node could not serve in time
|
|
320
|
+
answers `DEPENDENCY_UNAVAILABLE` (503), also to be repeated. It no longer
|
|
321
|
+
answers `CAPABILITY_UNAVAILABLE`, which is left for a network the provider
|
|
322
|
+
itself sends nothing on.
|
|
323
|
+
|
|
324
|
+
An execution the own-wallet system refuses for good ends the operation
|
|
325
|
+
`failed` with `effectState` `no_effect`: another transfer of the wallet took
|
|
326
|
+
this one's place before it was signed, the place a replacement would take is
|
|
327
|
+
already used (the replaced transfer went through or was replaced), or the
|
|
328
|
+
node refused the transaction. Start the send again where it is still wanted.
|
|
329
|
+
|
|
330
|
+
`transferStatus()` also tells:
|
|
331
|
+
|
|
332
|
+
- `authorizationExpiresAt`, while the transfer is not final: the deadline of
|
|
333
|
+
the owner's authorization. Past it the own-wallet system signs and sends
|
|
334
|
+
nothing more for this transfer, but a transaction the network already has
|
|
335
|
+
can still be included. A transfer that is `submitted` past that instant is
|
|
336
|
+
neither done nor safely gone: it ends `confirmed`, or `failed` once another
|
|
337
|
+
transaction of the wallet takes its place. On an EVM network it can be sped
|
|
338
|
+
up or cancelled (`replaceSend()`).
|
|
339
|
+
- `replacedByOperationId`, with `failureReason` `nonce_used`: the account's own
|
|
340
|
+
send whose transaction the network included instead, its speed-up or
|
|
341
|
+
cancellation or the next transfer of the same wallet. The funds may have
|
|
342
|
+
moved by that send. It is absent when the place was taken from outside the
|
|
343
|
+
own-wallet system, or by a send older than the account's last hundred. A
|
|
344
|
+
listed send carries it in `transfer.replacedByOperationId`.
|
|
345
|
+
- `failureReason` `not_sent`: the transfer was signed only after its
|
|
346
|
+
authorization ran out and was never sent.
|
|
347
|
+
|
|
348
|
+
On Ethereum an own-wallet system may report `confirmed` only when the block is
|
|
349
|
+
twelve blocks deep, about two and a half minutes, and on BNB Smart Chain a
|
|
350
|
+
hundred blocks deep: keep reading `submitted` for that long.
|
|
351
|
+
|
|
352
|
+
A send on TON can carry a comment, the text an exchange or a custodian credits
|
|
353
|
+
a deposit by: `linkedWallets().send({ ..., comment })`. It is 1 to 120 bytes
|
|
354
|
+
of UTF-8 with no control character, line separator or character that reorders
|
|
355
|
+
text (`transferComment(value)` is the rule, `MaxTransferCommentBytes` the
|
|
356
|
+
bound), and it is sent exactly as given, spaces included. The own-wallet
|
|
357
|
+
system writes it into the signed transaction, for the coin and for a token
|
|
358
|
+
alike. The provider accepts the preparation only when it carries exactly that
|
|
359
|
+
text and freezes it with the transfer: the display carries `comment` and the
|
|
360
|
+
prepared summary quotes it. A comment on any other network is refused before any request by
|
|
361
|
+
the SDK and with `PAYLOAD_NOT_SUPPORTED` by the provider. Stellar memos are
|
|
362
|
+
not sent. A listed send (`sends()`) carries its `comment` too.
|
|
363
|
+
|
|
364
|
+
A send on an EVM network that the network has not included can be replaced:
|
|
365
|
+
`linkedWallets().replaceSend({ linkedWalletId, operationId, action }, key)`,
|
|
366
|
+
where `operationId` is the account's own send from that wallet and `action` is
|
|
367
|
+
`speed_up`, the same transfer again with a higher fee, or `cancel`, nothing
|
|
368
|
+
sent to the wallet's own address in its place. The request names nothing of
|
|
369
|
+
what is moved: the provider takes it from the transfer frozen in that
|
|
370
|
+
operation. The answer is a `wallet.send` operation of its own, whose display
|
|
371
|
+
carries `replacesOperationId` and `replaceAction`, and for a cancel
|
|
372
|
+
`amountRaw` `"0"` of `native` to the wallet's own address. It is authorized
|
|
373
|
+
by the session, prepared with `prepare()` and executed with `executeSend()`
|
|
374
|
+
exactly like a send (on the demo page, on one press of «Ускорить» or
|
|
375
|
+
«Отменить»); the own-wallet system prepares
|
|
376
|
+
the replacement at `prepare()`, and the display then carries its fee
|
|
377
|
+
estimate. Both transactions spend the same place
|
|
378
|
+
in the wallet's sequence, so the network includes at most one: read
|
|
379
|
+
`transferStatus()` of both, the other ends `failed`. The own-wallet system
|
|
380
|
+
counts a replacement against the wallet's limits like a new transfer. Refusals:
|
|
381
|
+
`NOT_FOUND` for an operation that is not the account's send from that wallet;
|
|
382
|
+
`VERSION_CONFLICT` for a send the own-wallet system has not accepted, one whose
|
|
383
|
+
final fate is recorded, and, at `prepare()`, one that system says no longer
|
|
384
|
+
waits; `PAYLOAD_NOT_SUPPORTED` for a network that is not EVM and for a
|
|
385
|
+
replacement that system refuses, for instance when the coin does not cover the
|
|
386
|
+
higher fee; `RESULT_EXPIRED` once the send's material retention has passed. A
|
|
387
|
+
replacement that waits can itself be replaced the same way.
|
|
388
|
+
|
|
389
|
+
### Limits of a linked wallet
|
|
390
|
+
|
|
391
|
+
Every send from a linked wallet obeys the wallet's own limits (owner decision
|
|
392
|
+
of 14 October 2026): for each network and asset (`native`, `usdt`, `usdc`) an
|
|
393
|
+
optional ceiling on one transfer (`perTransferRaw`) and on what the wallet's
|
|
394
|
+
sends of that asset on that network may move in 24 hours (`perDayRaw`), in
|
|
395
|
+
that network's minimal units, and a list of recipients, where an empty list
|
|
396
|
+
means any recipient. A ceiling names its network because a token's decimals
|
|
397
|
+
differ between networks (USDT has 6 on Ethereum and 18 on BNB Smart Chain);
|
|
398
|
+
a send is judged only by the ceiling of its own network and asset. A wallet
|
|
399
|
+
whose limits were never set (`revision` `"0"`) bounds nothing.
|
|
400
|
+
|
|
401
|
+
- `linkedWallets().limits(linkedWalletId)` reads them with the session alone.
|
|
402
|
+
- `linkedWallets().setLimits(linkedWalletId, { ceilings, recipients,
|
|
403
|
+
revision }, key)` asks to replace them as a whole; empty `ceilings` and
|
|
404
|
+
`recipients` remove every limit. `revision` is the revision of the limits
|
|
405
|
+
the change was edited from, as `limits()` read it: when the limits changed
|
|
406
|
+
since, in another tab for instance, the change is refused with
|
|
407
|
+
`VERSION_CONFLICT` when it is created or when it is executed, and never
|
|
408
|
+
overwrites them; read them again and redo the change. The answer is a local operation of kind `wallet_limits.set`,
|
|
409
|
+
`awaiting_confirmation` with `nextAction` `use_passkey`, whose display is
|
|
410
|
+
the wallet's address and the new limits as the provider will keep them
|
|
411
|
+
(recipients in the form the network compares addresses by). Nothing changes
|
|
412
|
+
until the owner confirms it with a passkey: `challengeOperation(id,
|
|
413
|
+
credentialId)` opens the provider's page, which says the new limits in
|
|
414
|
+
words, `completeOperationConfirmation(callback)` reads the confirmed
|
|
415
|
+
operation, and `executeOperation(id, version, key)` applies it; the result
|
|
416
|
+
kind is `wallet_limits`. A ceiling on a network the wallet does not send on,
|
|
417
|
+
a network and asset named twice, or a malformed amount is refused before
|
|
418
|
+
anything is admitted; another account's wallet is `NOT_FOUND`.
|
|
419
|
+
|
|
420
|
+
A send the limits do not allow is refused with `403` and one of
|
|
421
|
+
`WalletLimitRefusals`; nothing is sent, and the same send goes through only
|
|
422
|
+
after the limits change:
|
|
423
|
+
|
|
424
|
+
- `LIMIT_PER_TRANSFER_EXCEEDED`: the amount is over the ceiling of one
|
|
425
|
+
transfer of its asset on its network.
|
|
426
|
+
- `LIMIT_DAILY_EXCEEDED`: with what the wallet's sends of that asset on that
|
|
427
|
+
network moved, or may have moved, in the last 24 hours, the send would pass
|
|
428
|
+
the daily ceiling.
|
|
429
|
+
- `RECIPIENT_NOT_ALLOWED`: the wallet has a recipient list and the recipient
|
|
430
|
+
is not in it.
|
|
431
|
+
|
|
432
|
+
`send()` and `replaceSend()` are answered `LIMIT_PER_TRANSFER_EXCEEDED` or
|
|
433
|
+
`RECIPIENT_NOT_ALLOWED` at once, unless the same key repeats a send already
|
|
434
|
+
created, which answers that send whatever the limits say now;
|
|
435
|
+
`executeSend()` is judged again against the limits as they are then, with
|
|
436
|
+
the daily total, and may answer any of the three. A speed-up is judged by
|
|
437
|
+
the ceiling of one transfer and the list and adds nothing to the daily
|
|
438
|
+
total, where the transfer it replaces is counted, but is refused when the
|
|
439
|
+
day already holds more than the daily ceiling as it is now; a cancel is
|
|
440
|
+
judged by none. `isWalletLimitRefusal(code)` tells these refusals
|
|
441
|
+
from the others. The own-wallet system's own signing policies still apply on
|
|
442
|
+
top of them.
|
|
443
|
+
|
|
444
|
+
`linkedWallets().balances(linkedWalletId)` answers what a linked wallet holds:
|
|
445
|
+
for each network its key has an address on, the network's own coin and the
|
|
446
|
+
tokens the stand names there, in minimal units with their decimals. The
|
|
447
|
+
provider reads its own stand's nodes, so the application hands the address to
|
|
448
|
+
no node operator and needs no token list of its own. An EVM wallet has one
|
|
449
|
+
entry for each EVM network of the stand. A network whose node gave nothing is
|
|
450
|
+
in the answer with `status` `unavailable`, or `partial` when it gave some
|
|
451
|
+
amounts, and an unread asset has no `amountRaw`: it is never zero. The assets
|
|
452
|
+
are always named. Where the server has no nodes configured the read is
|
|
453
|
+
`CAPABILITY_UNAVAILABLE`. Stellar is not read.
|
|
454
|
+
|
|
455
|
+
`linkedWallets().sends({ limit, cursor })` is the account's history of sends,
|
|
456
|
+
newest first: one page of `GET /v1/operations?kind=wallet.send`. Each row
|
|
457
|
+
carries the send's state, its network, asset, recipient and amount in minimal
|
|
458
|
+
units, the sending address of a linked wallet, its `transactionHash` once the
|
|
459
|
+
own-wallet system accepted it, and `transfer`, the recorded final fate. A row
|
|
460
|
+
with a hash and no `transfer` has not been read final yet: `transferStatus()`
|
|
461
|
+
reads it and records it. A replacement is a row of its own with
|
|
462
|
+
`replacesOperationId` and `replaceAction`. Listing asks nothing of the own-wallet system. Past
|
|
463
|
+
the material retention a send stays listed with `materialPurged` and without
|
|
464
|
+
details. The route lists every kind of account operation and takes any one
|
|
465
|
+
`kind`; the SDK has a typed reader for sends only.
|
|
466
|
+
|
|
467
|
+
`sendEvents(send, events)` picks from wallet events those of a listed send's
|
|
468
|
+
transaction: its withdrawal and any activity of the same transaction.
|
|
469
|
+
`eventOfTransaction(event, networkId, transactionHash)` is the test it uses. It
|
|
470
|
+
answers on the EVM networks and TRON, where an event's `sourceReference` is the
|
|
471
|
+
transaction hash in hexadecimal, and on Solana, where both are the
|
|
472
|
+
transaction's signature in base58; it answers `undefined` elsewhere. On TON an
|
|
473
|
+
event names the transaction the network made and the own-wallet system names
|
|
474
|
+
the message it sent, so nothing ties the two yet.
|
|
475
|
+
|
|
476
|
+
Solana is tracked like every other network once the stand's Streams serves it:
|
|
477
|
+
`register({ linkedWalletId, network: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" })`
|
|
478
|
+
for a Solana wallet, whose address is taken as the own-wallet system gives it.
|
|
479
|
+
Its assets are `<network>/slip44:501` for SOL and `<network>/spl:<mint>` for a
|
|
480
|
+
token; its activity types are `transaction`, `native.transfer`, `spl.transfer`,
|
|
481
|
+
`spl.approval`, `contract.raw`, `contract.decoded` and `native.action`. Its
|
|
482
|
+
events are wallet events 3.0 with the transaction's signature as
|
|
483
|
+
`sourceReference`.
|
|
484
|
+
|
|
485
|
+
`demo/` is a static page that walks the whole path; see `demo/README.md`.
|
|
486
|
+
|
|
487
|
+
## Authorized Streams reads
|
|
488
|
+
|
|
489
|
+
`src/streams.ts` is the package's third entry point
|
|
490
|
+
(`@shuttle.technology/authorization/streams`). It owns the client half of the four served
|
|
491
|
+
reads and nothing else: it builds their exact `/v1` paths, enforces the page
|
|
492
|
+
bounds before a request leaves the client, and parses the two closed responses.
|
|
493
|
+
|
|
494
|
+
`AuthorizationClient.streams()` binds those reads to the current session, so an
|
|
495
|
+
account caller reuses one access-token and refresh path and addresses
|
|
496
|
+
`/v1/wallets/{wallet_id}/registrations` and `/events`. A service caller is a
|
|
497
|
+
customer backend holding a tenant-access service credential, which this package
|
|
498
|
+
never holds: it constructs `StreamsReads` with `scope: "service"` and its own
|
|
499
|
+
authorized GET, exactly as the Mode C customer backend supplies its own
|
|
500
|
+
forwarding, and addresses the `/v1/service/wallets/{wallet_id}` variants.
|
|
501
|
+
|
|
502
|
+
The registrations route releases the permitted set in one page and issues no
|
|
503
|
+
cursor, so the helper accepts only a limit and refuses a cursor locally. The
|
|
504
|
+
events route accepts a cursor, a limit and the filter `eventTypes`; a wrapper
|
|
505
|
+
carries its own original limit, so resuming omits the limit and repeating a
|
|
506
|
+
different one is the server's `CURSOR_INVALID`. Every limit outside `[1,100]` is refused before a request
|
|
507
|
+
exists, and the default page is 50.
|
|
508
|
+
|
|
509
|
+
A page that has no next page still tells where to read on from:
|
|
510
|
+
`window.latestCursor` is accepted as `cursor` and returns only what came after
|
|
511
|
+
the end of the stream as that read saw it. `resumeCursor(page)` is the cursor
|
|
512
|
+
to keep between rounds: `nextCursor` while `hasMore` is true, `latestCursor`
|
|
513
|
+
after. A client walks a history once and from then on asks only for what is
|
|
514
|
+
new. `window.earliestCursor` is never accepted as a cursor. Every cursor ends
|
|
515
|
+
with its session and with a change of the wallet's registrations; a refused
|
|
516
|
+
one is `CURSOR_INVALID` and the history is read from its start again. The same
|
|
517
|
+
holds for `linkedWallets().events()`.
|
|
518
|
+
|
|
519
|
+
The parsers are closed. An unknown or missing member, a noncanonical
|
|
520
|
+
`amount_raw`, a nonintegral `revision`, a non-UTC `observed_at`, a `next_cursor`
|
|
521
|
+
without `has_more` or the reverse, a `reverted` flag that disagrees with its
|
|
522
|
+
`reverted_by_event_id`, and a malformed window are all refused rather than
|
|
523
|
+
projected. `revision`, `project_cursor`, the reversion fields and every source
|
|
524
|
+
position are released exactly as Streams owns them; no balance or finality is
|
|
525
|
+
inferred and no event is reconstructed.
|
|
526
|
+
|
|
527
|
+
### Wallet events 3.0: one shape for deposits, withdrawals and activity
|
|
528
|
+
|
|
529
|
+
Events are read in one shape, the wallet event 3.0 (`WalletEvent`,
|
|
530
|
+
`parseWalletEvent`, in `src/walletEvents.ts`):
|
|
531
|
+
`client.linkedWallets().events(id, options)` for a linked wallet,
|
|
532
|
+
`events(walletId, options)` on the account and service reads. There is no
|
|
533
|
+
other read of events: the stored shapes the older `deposits()` released
|
|
534
|
+
(`"2.0"`, `"activity:1.0"`, `"activity:2.0"`) are gone, and a page that
|
|
535
|
+
carries one is refused.
|
|
536
|
+
|
|
537
|
+
`eventTypes` filters by kind: `["deposit"]`, `["withdrawal"]`,
|
|
538
|
+
`["deposit","withdrawal"]`, `["activity"]`, or `"activity:<type>"` for one
|
|
539
|
+
type of activity, in any combination; left out, every kind. The filter is
|
|
540
|
+
applied to the page that was read and does not change its cursor, so a
|
|
541
|
+
filtered page may hold fewer events than the limit, or none, and still
|
|
542
|
+
continue: follow `hasMore`, not the number of events.
|
|
543
|
+
|
|
544
|
+
```json
|
|
545
|
+
{"contract_version":"3.0","event_id":"project-event:v1:…:1","cursor":"2.1",
|
|
546
|
+
"kind":"deposit","status":"completed","revision":"1",
|
|
547
|
+
"subject_id":"deposit-id:v1:…","registration_id":"…","network_id":"eip155:1",
|
|
548
|
+
"address":"0x…","asset":"eip155:1/slip44:60","raw_amount":"10000000000000",
|
|
549
|
+
"source_reference":"0x…","observed_at":"2026-10-09T09:54:47Z"}
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
- `kind` is `deposit` (value came in), `withdrawal` (value went out) or
|
|
553
|
+
`activity` (anything else; it has `activity_type` and may have a body).
|
|
554
|
+
- `status` is `completed` (it is in a block), `finalized` (the block is final)
|
|
555
|
+
or `reverted` (the block was replaced; `invalidation_reason` may say why).
|
|
556
|
+
- `revision` counts a subject's statuses from `"1"`; `subject_id` is what they
|
|
557
|
+
are about. completed is always revision 1.
|
|
558
|
+
- `cursor` is the event's own place in the stream, an opaque text no two
|
|
559
|
+
events share. A stream of wallet events is continued after it.
|
|
560
|
+
|
|
561
|
+
Every subject is told at least twice, completed and then finalized, on every
|
|
562
|
+
network. Where a block is final as soon as it exists the two arrive together;
|
|
563
|
+
on Ethereum a deposit is completed within seconds and finalized thirteen to
|
|
564
|
+
nineteen minutes later. A client that acts once a transfer is in a block waits
|
|
565
|
+
for `completed` and may ignore what follows it, except `reverted`.
|
|
566
|
+
|
|
567
|
+
What is told today: a deposit, a withdrawal and an activity on every network.
|
|
568
|
+
On Ethereum all three are completed within seconds and finalized later; an
|
|
569
|
+
activity's completed event carries its payload up to 16 KiB. BNB Smart Chain,
|
|
570
|
+
TRON and TON are read as each block becomes final, so there both revisions of
|
|
571
|
+
any event arrive together.
|
|
572
|
+
|
|
573
|
+
The page is `WalletEventPage`. One stored event gives no event, one or two, so
|
|
574
|
+
a page holds at most twice the limit asked for.
|
|
575
|
+
|
|
576
|
+
Read a deposit's `status` as where the transfer stands, not as whether it
|
|
577
|
+
happened. `pending` is the fact: the transfer lies, successful, in a block of
|
|
578
|
+
the chain as it stands, and the recipient can already spend it; show it to the
|
|
579
|
+
user as received. `confirmed` comes after the fact and adds that the block is
|
|
580
|
+
final and cannot be replaced; it is the moment to credit a balance kept outside
|
|
581
|
+
the chain. `detected` precedes both, and `reverted` ends a deposit whose block
|
|
582
|
+
was replaced before it became final. On Ethereum a block becomes final thirteen
|
|
583
|
+
to nineteen minutes after it appears.
|
|
584
|
+
|
|
585
|
+
An activity event carries no status. On Ethereum it is released when its block
|
|
586
|
+
is final, so an outgoing transfer of a tracked wallet is seen that much later
|
|
587
|
+
than it happened; on BNB Smart Chain, TRON and TON the wait is seconds.
|
|
588
|
+
|
|
589
|
+
An expired boundary is the server's `RESYNC_REQUIRED`, which the SDK rethrows
|
|
590
|
+
unchanged and never retries. `streamsResync(error)` returns the typed authorized
|
|
591
|
+
window, and `parseAuthorizationError` already refuses a window on any other code
|
|
592
|
+
and a `RESYNC_REQUIRED` without one. Those two values are window wrappers, not
|
|
593
|
+
pagination wrappers: the server binds each released wrapper to its own boundary
|
|
594
|
+
and answers `CURSOR_INVALID` for a substituted one, so the restart is a fresh
|
|
595
|
+
cursorless read. The SDK never converts a window value into a cursor and never
|
|
596
|
+
invents a position.
|
|
597
|
+
|
|
598
|
+
`pollStreamsDeposits` follows a page until the server itself reports no further
|
|
599
|
+
retained work. It adds no bound of its own and delegates to the same
|
|
600
|
+
`pollAuthoritative` helper, so at-least-two-second intervals and the
|
|
601
|
+
sixty-second ceiling stay enforced in one place, and cancellation or timeout
|
|
602
|
+
returns the last observed page with its local disposition rather than a claimed
|
|
603
|
+
server outcome.
|
|
604
|
+
|
|
605
|
+
## A backend client of an organization (Mode A)
|
|
606
|
+
|
|
607
|
+
This package is the client of a person's session. A customer's backend that
|
|
608
|
+
holds a Tenant Access key acts for its organization instead (owner decision,
|
|
609
|
+
14 October 2026): it creates the organization's wallets with a device key it
|
|
610
|
+
keeps itself, tracks them, reads them, their balances, events and its own
|
|
611
|
+
operations, sets each wallet's limits (per asset a ceiling on one transfer
|
|
612
|
+
and on 24 hours, and a recipient list where the empty list means any
|
|
613
|
+
recipient) and sends the network coin, USDT and USDC from them with a
|
|
614
|
+
prepare and an execute that carries the device share, with no passkey. The
|
|
615
|
+
service routes are `/v1/service/operations` (with `/prepare`) and
|
|
616
|
+
`/v1/service/wallets` (with `/balances`, `/transport-key`, `/limits`,
|
|
617
|
+
`/registrations`, `/events`); see `api/authorization/openapi.yaml`.
|
|
618
|
+
|
|
619
|
+
`examples/backend-client` is that backend in a small Node program, the
|
|
620
|
+
backend counterpart of the demo page. It reuses this package's device share
|
|
621
|
+
crypto (`resealDeviceShare`) and keeps the device key, extractable, in a
|
|
622
|
+
private state file. Its README says how to run it and what the stand's
|
|
623
|
+
operator must set first. `test/backendClient.test.ts` covers its pure parts.
|
|
624
|
+
|
|
625
|
+
## Local verification
|
|
626
|
+
|
|
627
|
+
Use the package-local pinned toolchain:
|
|
628
|
+
|
|
629
|
+
```sh
|
|
630
|
+
pnpm install --frozen-lockfile
|
|
631
|
+
pnpm run verify
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
`verify` runs syntax checks, full TypeScript checking (including fixtures), a
|
|
635
|
+
clean JavaScript build, and package tests.
|
|
636
|
+
|
|
637
|
+
Release packaging is checked from an installed tarball with the reviewed npm
|
|
638
|
+
CLI. The smoke check imports every public entry point, compiles a TypeScript
|
|
639
|
+
consumer against the installed declarations, verifies that consumer installs
|
|
640
|
+
do not include TypeScript, and rejects unexpected tarball files:
|
|
641
|
+
|
|
642
|
+
```sh
|
|
643
|
+
NPM_CLI_JS=/absolute/path/to/npm-cli.js pnpm run package:smoke
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
The PostgreSQL-backed HTTP integration harness builds first, then invokes:
|
|
647
|
+
|
|
648
|
+
```sh
|
|
649
|
+
pnpm run build
|
|
650
|
+
pnpm run integration:http -- --fixture /absolute/path/to/sdk-fixture.json
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
The fixture runner accepts only local TLS endpoints rooted in its supplied CA.
|
|
654
|
+
It drives the compiled SDK through public initialization, Google signup,
|
|
655
|
+
handoff exchange, safe reads, logout and revoked-token denial, then logs in the
|
|
656
|
+
same signed provider subject and verifies that it resolves to the same account.
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
### Contract runners against the real Authorization server
|
|
660
|
+
|
|
661
|
+
`test/http-integration-{linked-send,limits,modec-linked,mode-a,demo}.mjs` drive
|
|
662
|
+
the compiled SDK (`dist/`), the backend simulator's own modules
|
|
663
|
+
(`examples/backend-client/`) and the demo page's (`demo/*.js`, without the DOM)
|
|
664
|
+
against the real Authorization HTTP server
|
|
665
|
+
over TLS, composed as the runtime composes it, with PostgreSQL, the decision
|
|
666
|
+
signer, claimer, worker and native Wallet adapter; only the Wallet, Tenant
|
|
667
|
+
Access's producer side (behind the shipped Tenant Access client) and the
|
|
668
|
+
stand's nodes are stand-ins. The Go tests `TestCompiledSDK*` and
|
|
669
|
+
`TestBackendSimulatorModeA*` in `internal/authorization/store/postgres` run
|
|
670
|
+
them, each compiling the SDK from the current source into a scratch directory
|
|
671
|
+
with the installed TypeScript compiler (`tests/authorization/tooling/sdkrunner`),
|
|
672
|
+
with the Node of `NODE_BIN` or of `PATH`:
|
|
673
|
+
|
|
674
|
+
```sh
|
|
675
|
+
pnpm install --frozen-lockfile
|
|
676
|
+
TEST_DATABASE_URL=... go test ../../internal/authorization/store/postgres -run 'TestCompiledSDK|TestBackendSimulator'
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
Each runner prints one summary line; the Go side checks that the runner
|
|
680
|
+
loads the SDK only from `dist/`, that the build it reports is the build it
|
|
681
|
+
compiled, and that every HTTP call it counted came from that build. Every
|
|
682
|
+
answer the server gives in these flows is also checked against the committed
|
|
683
|
+
OpenAPI document. The joined linked-wallet test
|
|
684
|
+
(`internal/authorization/wallethttp`, `test/http-integration-linked-wallets.mjs`)
|
|
685
|
+
runs the same way, with the Node of `AUTH_SDK_NODE_BIN` or of `PATH`, and uses
|
|
686
|
+
the build of `AUTH_SDK_WORKDIR` when the local acceptance below prepared one.
|
|
687
|
+
|
|
688
|
+
### Required local linked-wallet acceptance
|
|
689
|
+
|
|
690
|
+
From repository root, using already prepared offline tools/dependencies:
|
|
691
|
+
|
|
692
|
+
```sh
|
|
693
|
+
make authorization-linked-wallet-local GO="$GO_BIN" NODE="$NODE_BIN" \
|
|
694
|
+
PG_BINDIR="$PG18_BINDIR" AUTH_SDK_DEPENDENCY_ISOLATE="$SDK_DEPENDENCY_ISOLATE"
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
Provide Go1.26.7, Node24.19.0, PostgreSQL18.6 and an explicit separate SDK dependency
|
|
698
|
+
isolate prepared for the current package/lock. No install or download runs. The target
|
|
699
|
+
checks dependency manifest/installed lock/version provenance, copies current worktree
|
|
700
|
+
SDK source/config/runner to new scratch and builds fresh dist with the prepared tsc.
|
|
701
|
+
Unused isolate src/dist never supply execution code. Its dependency symlink is trusted
|
|
702
|
+
reuse, not a filesystem read-only sandbox; existing dependencies are not mutated.
|
|
703
|
+
|
|
704
|
+
The disposable-PG race run requires the actual SDK/TLS/Auth/Streams test plus direct
|
|
705
|
+
and lost-response/store-reopen cases, each present and passed with zero skips. Missing
|
|
706
|
+
inputs, stale dependency manifests, omitted tests or a skipped result fail the command.
|
|
707
|
+
Generic suites retain optional SDK behavior; this explicit target is mandatory local
|
|
708
|
+
proof. Source effects/events are synthetic and passkey is virtual crypto. It needs no
|
|
709
|
+
external pilot pins or lookup and makes no deployed-provider/browser/indexer claim.
|