@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,282 @@
1
+ // Authorized Streams reads (source §1.7, §1.11, §2.2, §2.6).
2
+ //
3
+ // This module is the package's third entry point
4
+ // (`@shuttle.technology/authorization/streams`). It owns exactly the client half of the
5
+ // served reads: it builds their exact `/v1` paths, enforces the page bounds
6
+ // of source §2.8 before a request leaves the client, and parses the two
7
+ // closed response shapes. Events are read in one shape, wallet events 3.0,
8
+ // owned by `./walletEvents.ts` and re-exported here.
9
+ //
10
+ // It holds no credential and performs no HTTP itself. The account caller binds
11
+ // it to its own session through `AuthorizationClient.streams()`; a service
12
+ // caller is a customer backend that holds the tenant-access service credential,
13
+ // so it supplies its own authorized GET exactly as the Mode C customer backend
14
+ // supplies its own forwarding (source §2.2, AUTH-009-A05).
15
+ //
16
+ // Streams owns registration admission, event identity, lifecycle revisions,
17
+ // cursors, replay and reversions. This module therefore renumbers nothing,
18
+ // reconstructs nothing, infers no balance or finality, and never invents a
19
+ // cursor: an expired boundary is handed back as the authorized window and the
20
+ // caller decides whether to restart from it.
21
+ import { AuthorizationError, ClientFlowError, integer, opaqueId, record, } from "./auth.js";
22
+ import { pollAuthoritative } from "./poll.js";
23
+ import { parseWalletEvent } from "./walletEvents.js";
24
+ export { pollAuthoritative } from "./poll.js";
25
+ export { AuthorizationError, ClientFlowError } from "./auth.js";
26
+ export { MaxWalletEventPayloadLength, parseWalletEvent } from "./walletEvents.js";
27
+ export { TrackedWallets, TrackedWalletsPath, parseTrackedWalletPage, trackedWalletsPath } from "./trackedWallets.js";
28
+ /** DefaultStreamsPageLimit is the served page size when no limit is supplied (source §2.8). */
29
+ export const DefaultStreamsPageLimit = 50;
30
+ /** MaxStreamsPageLimit is the largest page the deposit route accepts (source §2.8). */
31
+ export const MaxStreamsPageLimit = 100;
32
+ /** MaxStreamsCursorLength bounds one opaque scope-bound wrapper. */
33
+ export const MaxStreamsCursorLength = 4096;
34
+ /** MaxStreamsAssets bounds one registration's validated asset list. */
35
+ export const MaxStreamsAssets = 32;
36
+ /** MaxStreamsActivityTypes bounds the activity kinds one registration may ask for. */
37
+ export const MaxStreamsActivityTypes = 16;
38
+ /** MaxStreamsAddressLength bounds one released address. */
39
+ export const MaxStreamsAddressLength = 256;
40
+ /**
41
+ * resumeCursor is the cursor to pass to the next read of the same wallet so
42
+ * that it returns only what this page did not: the next page while there is
43
+ * one, and at the end of the stream what comes after it. Keep it between
44
+ * rounds instead of reading a history again from its start. It ends with the
45
+ * session, like every cursor; a refused one means the history is read anew.
46
+ */
47
+ export function resumeCursor(page) {
48
+ return page.hasMore ? page.nextCursor : page.window.latestCursor;
49
+ }
50
+ /** MaxWalletEventTypes is how many selectors a filter may name. */
51
+ export const MaxWalletEventTypes = 64;
52
+ const eventTypePattern = /^(deposit|withdrawal|activity|activity:[a-z0-9_.]{1,64})$/;
53
+ /**
54
+ * appendEventTypes writes the filter by kind into a query. Each selector is
55
+ * named once; what the server makes of a combination is the server's to say.
56
+ */
57
+ export function appendEventTypes(query, value) {
58
+ if (value === undefined)
59
+ return;
60
+ if (!Array.isArray(value) || value.length < 1 || value.length > MaxWalletEventTypes ||
61
+ value.some((selector) => typeof selector !== "string" || !eventTypePattern.test(selector)) ||
62
+ new Set(value).size !== value.length) {
63
+ throw new ClientFlowError("invalid_streams_request");
64
+ }
65
+ query.set("event_types", value.join(","));
66
+ }
67
+ // Streams admits its legacy binding spelling and the canonical CAIP-2 form
68
+ // validated by the Authorization Streams port. Keep this local to Streams
69
+ // reads; generic wallet network contracts have different bounds.
70
+ const networkPattern = /^([a-z][a-z0-9-]{0,31}|[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32})$/;
71
+ const assetPattern = /^[a-z][a-z0-9_.:-]{0,63}$/;
72
+ const sourcePositionPattern = /^[A-Za-z0-9_.:-]{1,256}$/;
73
+ const sourceVersionPattern = /^[0-9]+\.[0-9]+$/;
74
+ /**
75
+ * streamsRegistrationsPath builds the exact registrations path. The route
76
+ * declares no cursor at all, so this builder accepts none: a cursor member is
77
+ * refused here rather than sent and answered INVALID_REQUEST.
78
+ */
79
+ export function streamsRegistrationsPath(scope, walletId, options = {}) {
80
+ const declared = closedOptions(options, ["limit", "signal"]);
81
+ const query = new URLSearchParams();
82
+ appendLimit(query, declared.limit);
83
+ return suffixed(walletPrefix(scope, walletId), "/registrations", query);
84
+ }
85
+ /** streamsEventsPath builds the exact wallet events path with its closed query. */
86
+ export function streamsEventsPath(scope, walletId, options = {}) {
87
+ const declared = closedOptions(options, ["cursor", "limit", "eventTypes", "signal"]);
88
+ const query = new URLSearchParams();
89
+ if (declared.cursor !== undefined) {
90
+ if (!cursorValue(declared.cursor))
91
+ throw new ClientFlowError("invalid_streams_request");
92
+ query.set("cursor", declared.cursor);
93
+ }
94
+ appendLimit(query, declared.limit);
95
+ appendEventTypes(query, declared.eventTypes);
96
+ return suffixed(walletPrefix(scope, walletId), "/events", query);
97
+ }
98
+ /** parseStreamsRegistrationList parses the closed registrations response. */
99
+ export function parseStreamsRegistrationList(payload) {
100
+ const root = record(payload, ["items", "has_more"]);
101
+ if (!Array.isArray(root.items) || typeof root.has_more !== "boolean")
102
+ throw new ClientFlowError("invalid_server_response");
103
+ return Object.freeze({ items: Object.freeze(root.items.map(parseStreamsRegistration)), hasMore: root.has_more });
104
+ }
105
+ /** parseWalletEventPage parses the closed page of wallet events 3.0. */
106
+ export function parseWalletEventPage(payload) {
107
+ const root = record(payload, ["events", "has_more", "window"], ["next_cursor"]);
108
+ if (!Array.isArray(root.events) || root.events.length > 2 * MaxStreamsPageLimit || typeof root.has_more !== "boolean") {
109
+ throw new ClientFlowError("invalid_server_response");
110
+ }
111
+ // next_cursor is present exactly when has_more is true. Both directions are
112
+ // refused, so a page can neither hide its continuation nor offer one it did
113
+ // not declare.
114
+ const declaresCursor = Object.prototype.hasOwnProperty.call(root, "next_cursor");
115
+ if (root.has_more !== declaresCursor)
116
+ throw new ClientFlowError("invalid_server_response");
117
+ if (declaresCursor && !cursorValue(root.next_cursor))
118
+ throw new ClientFlowError("invalid_server_response");
119
+ const window = parseStreamsWindow(root.window);
120
+ const events = Object.freeze(root.events.map(parseWalletEvent));
121
+ if (!root.has_more)
122
+ return Object.freeze({ events, hasMore: false, window });
123
+ return Object.freeze({ events, nextCursor: root.next_cursor, hasMore: true, window });
124
+ }
125
+ /** parseStreamsWindow parses the required two-wrapper authorized window. */
126
+ export function parseStreamsWindow(payload) {
127
+ const window = record(payload, ["earliest_cursor", "latest_cursor"]);
128
+ if (!cursorValue(window.earliest_cursor) || !cursorValue(window.latest_cursor))
129
+ throw new ClientFlowError("invalid_server_response");
130
+ return Object.freeze({ earliestCursor: window.earliest_cursor, latestCursor: window.latest_cursor });
131
+ }
132
+ /**
133
+ * streamsResync reports the authorized window of an expired boundary and
134
+ * nothing else. `parseAuthorizationError` already refuses a RESYNC_REQUIRED
135
+ * without its window and a window on any other code, so this returns undefined
136
+ * for every other failure instead of guessing a recovery position.
137
+ */
138
+ export function streamsResync(error) {
139
+ if (!(error instanceof AuthorizationError) || error.code !== "RESYNC_REQUIRED" || error.window === undefined)
140
+ return undefined;
141
+ return Object.freeze({
142
+ requestId: error.requestId,
143
+ window: Object.freeze({ earliestCursor: error.window.earliestCursor, latestCursor: error.window.latestCursor }),
144
+ });
145
+ }
146
+ /**
147
+ * StreamsReads is the two authorized reads bound to one caller's own authorized
148
+ * GET. It owns no credential: the account caller receives one already bound to
149
+ * its session from `AuthorizationClient.streams()`, and a service caller
150
+ * supplies a GET that carries its own tenant-access service credential. Each
151
+ * read requires the caller's stable key; reuse it only for retries of the same
152
+ * route and query because the server conflicts when a key is bound to changed
153
+ * request material.
154
+ */
155
+ export class StreamsReads {
156
+ scope;
157
+ #get;
158
+ constructor(dependencies) {
159
+ const declared = closedOptions(dependencies, ["scope", "get"]);
160
+ if ((declared.scope !== "account" && declared.scope !== "service") || typeof declared.get !== "function") {
161
+ throw new ClientFlowError("invalid_streams_request");
162
+ }
163
+ this.scope = declared.scope;
164
+ this.#get = declared.get;
165
+ }
166
+ /** registrations reads the wallet's authorized registrations in one page. */
167
+ async registrations(walletId, options) {
168
+ const declared = closedOptions(options, ["limit", "signal", "idempotencyKey"]);
169
+ if (!validStreamsReadIdempotencyKey(declared.idempotencyKey))
170
+ throw new ClientFlowError("invalid_streams_request");
171
+ const path = streamsRegistrationsPath(this.scope, walletId, { limit: declared.limit });
172
+ return parseStreamsRegistrationList(await this.#get(path, declared.signal, declared.idempotencyKey));
173
+ }
174
+ /**
175
+ * events reads one page of the wallet's retained events as wallet events
176
+ * 3.0: deposits, withdrawals and other activity in one shape with a status,
177
+ * those of the kinds `eventTypes` names. A denial is thrown exactly as the
178
+ * server classified it: an expired boundary raises the RESYNC_REQUIRED
179
+ * AuthorizationError carrying its authorized window, which this method
180
+ * never catches, retries or converts into an empty success.
181
+ */
182
+ async events(walletId, options) {
183
+ const declared = closedOptions(options, ["cursor", "limit", "eventTypes", "signal", "idempotencyKey"]);
184
+ if (!validStreamsReadIdempotencyKey(declared.idempotencyKey))
185
+ throw new ClientFlowError("invalid_streams_request");
186
+ const path = streamsEventsPath(this.scope, walletId, {
187
+ cursor: declared.cursor,
188
+ limit: declared.limit,
189
+ eventTypes: declared.eventTypes,
190
+ });
191
+ return parseWalletEventPage(await this.#get(path, declared.signal, declared.idempotencyKey));
192
+ }
193
+ }
194
+ /**
195
+ * pollStreamsEvents follows a page of wallet events until the server itself reports no
196
+ * further retained work. It adds no bound of its own: it delegates to the one
197
+ * `pollAuthoritative` helper, so the source §2.8 bounds — at least two seconds
198
+ * between polls and at most sixty seconds in total — are enforced in a single
199
+ * place, and cancellation or timeout returns the last observed page with its
200
+ * local disposition rather than a claimed server outcome.
201
+ */
202
+ export function pollStreamsEvents(options) {
203
+ const declared = closedOptions(options, ["read", "signal", "intervalMs", "timeoutMs", "now", "sleep"]);
204
+ if (typeof declared.read !== "function")
205
+ throw new ClientFlowError("invalid_streams_request");
206
+ return pollAuthoritative({
207
+ read: declared.read,
208
+ terminal: (page) => page.hasMore === false,
209
+ signal: declared.signal,
210
+ intervalMs: declared.intervalMs,
211
+ timeoutMs: declared.timeoutMs,
212
+ now: declared.now,
213
+ sleep: declared.sleep,
214
+ });
215
+ }
216
+ function parseStreamsRegistration(value) {
217
+ const item = record(value, [
218
+ "registration_id", "wallet_id", "source_registration_id", "network", "address",
219
+ "assets", "status", "effective_from", "revision", "source_version",
220
+ ]);
221
+ if (!opaqueId(item.registration_id) || !opaqueId(item.wallet_id) || !opaqueId(item.source_registration_id) ||
222
+ !matches(networkPattern, item.network) || !bounded(item.address, MaxStreamsAddressLength) || !assetList(item.assets) ||
223
+ (item.status !== "pending" && item.status !== "active") || !matches(sourcePositionPattern, item.effective_from) ||
224
+ !positive(item.revision) || !matches(sourceVersionPattern, item.source_version)) {
225
+ throw new ClientFlowError("invalid_server_response");
226
+ }
227
+ return Object.freeze({
228
+ registrationId: item.registration_id, walletId: item.wallet_id, sourceRegistrationId: item.source_registration_id,
229
+ network: item.network, address: item.address, assets: Object.freeze([...item.assets]),
230
+ status: item.status, effectiveFrom: item.effective_from, revision: item.revision, sourceVersion: item.source_version,
231
+ });
232
+ }
233
+ function walletPrefix(scope, walletId) {
234
+ if ((scope !== "account" && scope !== "service") || !opaqueId(walletId))
235
+ throw new ClientFlowError("invalid_streams_request");
236
+ const base = scope === "account" ? "/v1/wallets/" : "/v1/service/wallets/";
237
+ return base + encodeURIComponent(walletId);
238
+ }
239
+ function suffixed(prefix, suffix, query) {
240
+ const path = prefix + suffix;
241
+ return query.size === 0 ? path : `${path}?${query.toString()}`;
242
+ }
243
+ /**
244
+ * appendLimit enforces the source §2.8 page bound before any request. The value
245
+ * must be a safe integer in [1,100]; every other value, including 0, 101 and a
246
+ * noninteger, is refused locally rather than sent.
247
+ */
248
+ function appendLimit(query, value) {
249
+ if (value === undefined)
250
+ return;
251
+ if (!integer(value) || value < 1 || value > MaxStreamsPageLimit)
252
+ throw new ClientFlowError("invalid_streams_request");
253
+ query.set("limit", String(value));
254
+ }
255
+ function closedOptions(value, allowed) {
256
+ if (value === null || typeof value !== "object" || Array.isArray(value))
257
+ throw new ClientFlowError("invalid_streams_request");
258
+ const result = value;
259
+ const permitted = new Set(allowed);
260
+ if (Object.keys(result).some((member) => !permitted.has(member)))
261
+ throw new ClientFlowError("invalid_streams_request");
262
+ return result;
263
+ }
264
+ function cursorValue(value) {
265
+ return typeof value === "string" && value.length > 0 && value.length <= MaxStreamsCursorLength;
266
+ }
267
+ function validStreamsReadIdempotencyKey(value) {
268
+ return typeof value === "string" && value.length >= 1 && value.length <= 128 &&
269
+ [...value].every((character) => character.charCodeAt(0) >= 33 && character.charCodeAt(0) <= 126);
270
+ }
271
+ function bounded(value, maximum) {
272
+ return typeof value === "string" && value.length > 0 && value.length <= maximum;
273
+ }
274
+ function matches(pattern, value) {
275
+ return typeof value === "string" && pattern.test(value);
276
+ }
277
+ function positive(value) {
278
+ return integer(value) && value >= 1;
279
+ }
280
+ function assetList(value) {
281
+ return Array.isArray(value) && value.length <= MaxStreamsAssets && value.every((asset) => matches(assetPattern, asset));
282
+ }
@@ -0,0 +1,53 @@
1
+ /** The served path of the read. */
2
+ export declare const TrackedWalletsPath = "/v1/service/registrations";
3
+ /** `wallet` is a wallet Authorization created, `linked_wallet` one its owner connected. */
4
+ export type TrackedWalletKind = "wallet" | "linked_wallet";
5
+ /** TrackedWallet is one tracked wallet and the registration its events carry. */
6
+ export interface TrackedWallet {
7
+ readonly registrationId: string;
8
+ /** The registration identifier Streams puts on every event of this wallet. */
9
+ readonly sourceRegistrationId: string;
10
+ readonly walletKind: TrackedWalletKind;
11
+ /** An identifier of the kind `walletKind` names. */
12
+ readonly walletId: string;
13
+ readonly ownerType: "organization" | "account";
14
+ readonly ownerId: string;
15
+ readonly network: string;
16
+ readonly address: string;
17
+ readonly status: "pending" | "active";
18
+ }
19
+ /** TrackedWalletPage is one page, ordered by `registrationId`. */
20
+ export interface TrackedWalletPage {
21
+ readonly items: readonly TrackedWallet[];
22
+ /** Present exactly when `hasMore` is true. */
23
+ readonly nextCursor?: string;
24
+ readonly hasMore: boolean;
25
+ }
26
+ /** TrackedWalletPageOptions is the closed input of `TrackedWallets.page`. */
27
+ export interface TrackedWalletPageOptions {
28
+ readonly cursor?: string;
29
+ readonly limit?: number;
30
+ readonly signal?: AbortSignal;
31
+ }
32
+ /** TrackedWalletsGet is the backend's own authorized GET of one `/v1` path. */
33
+ export type TrackedWalletsGet = (path: string, signal: AbortSignal | undefined) => Promise<unknown>;
34
+ /** trackedWalletsPath builds the exact path of one read and refuses a request the server would. */
35
+ export declare function trackedWalletsPath(options?: {
36
+ readonly sourceRegistrationId?: string;
37
+ readonly cursor?: string;
38
+ readonly limit?: number;
39
+ }): string;
40
+ /** parseTrackedWalletPage parses the closed response of the read. */
41
+ export declare function parseTrackedWalletPage(payload: unknown): TrackedWalletPage;
42
+ /** TrackedWallets is the read bound to one backend's own authorized GET. */
43
+ export declare class TrackedWallets {
44
+ #private;
45
+ constructor(get: TrackedWalletsGet);
46
+ /** page reads one page of every tracked wallet of the backend's environment. */
47
+ page(options?: TrackedWalletPageOptions): Promise<TrackedWalletPage>;
48
+ /**
49
+ * forEvent returns the wallet behind the registration an event names, or
50
+ * undefined when the environment tracks no such registration.
51
+ */
52
+ forEvent(sourceRegistrationId: string, signal?: AbortSignal): Promise<TrackedWallet | undefined>;
53
+ }
@@ -0,0 +1,104 @@
1
+ // The read a customer backend resolves a Streams event with.
2
+ //
3
+ // A backend takes events straight from Streams. An event names the Streams
4
+ // registration, its network and its address; which wallet that is, and whose,
5
+ // only Authorization knows. `TrackedWallets` asks it: for one event, or for
6
+ // every tracked wallet of the backend's own environment.
7
+ //
8
+ // Like the other service reads of this package it holds no credential and
9
+ // performs no HTTP itself: the backend supplies a GET that carries its own
10
+ // tenant-access service credential (`Authorization: Service …`). That key
11
+ // never belongs in a browser.
12
+ import { ClientFlowError, integer, opaqueId, record } from "./auth.js";
13
+ /** The served path of the read. */
14
+ export const TrackedWalletsPath = "/v1/service/registrations";
15
+ const maxPageLimit = 100;
16
+ const maxCursorLength = 4096;
17
+ const maxAddressLength = 256;
18
+ const networkPattern = /^([a-z][a-z0-9-]{0,31}|[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32})$/;
19
+ /** trackedWalletsPath builds the exact path of one read and refuses a request the server would. */
20
+ export function trackedWalletsPath(options = {}) {
21
+ const declared = closed(options, ["sourceRegistrationId", "cursor", "limit"]);
22
+ const query = new URLSearchParams();
23
+ if (declared.sourceRegistrationId !== undefined) {
24
+ if (!opaqueId(declared.sourceRegistrationId) || declared.cursor !== undefined)
25
+ throw new ClientFlowError("invalid_streams_request");
26
+ query.set("source_registration_id", declared.sourceRegistrationId);
27
+ }
28
+ if (declared.cursor !== undefined) {
29
+ if (typeof declared.cursor !== "string" || declared.cursor.length === 0 || declared.cursor.length > maxCursorLength) {
30
+ throw new ClientFlowError("invalid_streams_request");
31
+ }
32
+ query.set("cursor", declared.cursor);
33
+ }
34
+ if (declared.limit !== undefined) {
35
+ if (!integer(declared.limit) || declared.limit < 1 || declared.limit > maxPageLimit)
36
+ throw new ClientFlowError("invalid_streams_request");
37
+ query.set("limit", String(declared.limit));
38
+ }
39
+ return query.size === 0 ? TrackedWalletsPath : `${TrackedWalletsPath}?${query.toString()}`;
40
+ }
41
+ /** parseTrackedWalletPage parses the closed response of the read. */
42
+ export function parseTrackedWalletPage(payload) {
43
+ const page = record(payload, ["items", "has_more"], ["next_cursor"]);
44
+ if (!Array.isArray(page.items) || page.items.length > maxPageLimit || typeof page.has_more !== "boolean" ||
45
+ (page.next_cursor !== undefined) !== page.has_more ||
46
+ (page.next_cursor !== undefined && (typeof page.next_cursor !== "string" || page.next_cursor.length === 0 || page.next_cursor.length > maxCursorLength))) {
47
+ throw new ClientFlowError("invalid_server_response");
48
+ }
49
+ const items = Object.freeze(page.items.map(parseTrackedWallet));
50
+ return Object.freeze(page.has_more ? { items, nextCursor: page.next_cursor, hasMore: true } : { items, hasMore: false });
51
+ }
52
+ /** TrackedWallets is the read bound to one backend's own authorized GET. */
53
+ export class TrackedWallets {
54
+ #get;
55
+ constructor(get) {
56
+ if (typeof get !== "function")
57
+ throw new ClientFlowError("invalid_streams_request");
58
+ this.#get = get;
59
+ }
60
+ /** page reads one page of every tracked wallet of the backend's environment. */
61
+ async page(options = {}) {
62
+ const declared = closed(options, ["cursor", "limit", "signal"]);
63
+ const path = trackedWalletsPath({ cursor: declared.cursor, limit: declared.limit });
64
+ return parseTrackedWalletPage(await this.#get(path, declared.signal));
65
+ }
66
+ /**
67
+ * forEvent returns the wallet behind the registration an event names, or
68
+ * undefined when the environment tracks no such registration.
69
+ */
70
+ async forEvent(sourceRegistrationId, signal) {
71
+ const page = parseTrackedWalletPage(await this.#get(trackedWalletsPath({ sourceRegistrationId }), signal));
72
+ if (page.items.length > 1 || page.hasMore || (page.items.length === 1 && page.items[0].sourceRegistrationId !== sourceRegistrationId)) {
73
+ throw new ClientFlowError("invalid_server_response");
74
+ }
75
+ return page.items[0];
76
+ }
77
+ }
78
+ function parseTrackedWallet(value) {
79
+ const item = record(value, [
80
+ "registration_id", "source_registration_id", "wallet_kind", "wallet_id", "owner_type", "owner_id", "network", "address", "status",
81
+ ]);
82
+ if (!opaqueId(item.registration_id) || !opaqueId(item.source_registration_id) || !opaqueId(item.wallet_id) || !opaqueId(item.owner_id) ||
83
+ (item.wallet_kind !== "wallet" && item.wallet_kind !== "linked_wallet") ||
84
+ (item.owner_type !== "organization" && item.owner_type !== "account") ||
85
+ typeof item.network !== "string" || !networkPattern.test(item.network) ||
86
+ typeof item.address !== "string" || item.address.length === 0 || item.address.length > maxAddressLength ||
87
+ (item.status !== "pending" && item.status !== "active")) {
88
+ throw new ClientFlowError("invalid_server_response");
89
+ }
90
+ return Object.freeze({
91
+ registrationId: item.registration_id, sourceRegistrationId: item.source_registration_id, walletKind: item.wallet_kind,
92
+ walletId: item.wallet_id, ownerType: item.owner_type, ownerId: item.owner_id, network: item.network, address: item.address,
93
+ status: item.status,
94
+ });
95
+ }
96
+ function closed(value, allowed) {
97
+ if (value === null || typeof value !== "object" || Array.isArray(value))
98
+ throw new ClientFlowError("invalid_streams_request");
99
+ const result = value;
100
+ const permitted = new Set(allowed);
101
+ if (Object.keys(result).some((member) => !permitted.has(member)))
102
+ throw new ClientFlowError("invalid_streams_request");
103
+ return result;
104
+ }
@@ -0,0 +1,45 @@
1
+ /** The own-wallet system's states of a prepared transfer, in the order a transfer passes them. */
2
+ export declare const TransferStates: readonly ['prepared', 'executing', 'broadcasting', 'submitted', 'confirmed', 'failed', 'expired'];
3
+ export type TransferState = (typeof TransferStates)[number];
4
+ /**
5
+ * What became of a transfer. confirmed: in a block with a successful result.
6
+ * failed: refused by the network, failed in a block, replaced or dropped;
7
+ * failureReason carries the network's reason when it gave one. expired: never
8
+ * executed, no funds moved. Every other state is still on its way.
9
+ */
10
+ export interface TransferStatus {
11
+ readonly requestId: string;
12
+ readonly operationId: string;
13
+ readonly state: TransferState;
14
+ /** True for confirmed, failed and expired: the state can no longer change. */
15
+ readonly final: boolean;
16
+ /** The network's transaction hash, present once the transfer is signed. */
17
+ readonly transactionHash?: string;
18
+ /**
19
+ * The network's reason for a failed transfer: its error code, or the
20
+ * own-wallet system's expired, nonce_used, bounced or not_sent (signed only
21
+ * after its authorization ran out, so never sent).
22
+ */
23
+ readonly failureReason?: string;
24
+ /**
25
+ * The account's own send whose transaction the network included in this
26
+ * transfer's place: its speed-up or cancellation, or the next transfer of
27
+ * the same wallet. Present only with failureReason nonce_used, and absent
28
+ * when the place was taken from outside the own-wallet system or by none of
29
+ * the account's last hundred sends. The funds may have moved by that send.
30
+ */
31
+ readonly replacedByOperationId?: string;
32
+ /**
33
+ * The deadline of the owner's authorization, present while the transfer is
34
+ * not final. Past it the own-wallet system signs and sends nothing more for
35
+ * this transfer, but a transaction the network already has can still be
36
+ * included: a transfer that is submitted past this instant is neither done
37
+ * nor safely gone.
38
+ */
39
+ readonly authorizationExpiresAt?: string;
40
+ }
41
+ /** Reports that a transfer in this state can no longer change. */
42
+ export declare function transferFinal(state: TransferState): boolean;
43
+ export declare function transferStatusPath(operationId: string): string;
44
+ /** Parses the closed transfer status answer of the operation that was asked about. */
45
+ export declare function parseTransferStatus(payload: unknown, expectedOperationId: string): TransferStatus;
@@ -0,0 +1,43 @@
1
+ // The fate of the one transfer a send from a linked wallet prepared. A send's
2
+ // operation ends when the own-wallet system accepted the transfer and named
3
+ // its hash; what the network then did with it is read here, for that same
4
+ // transfer and no other.
5
+ import { ClientFlowError, opaqueId, record, rfc3339UTC } from './auth.js';
6
+ /** The own-wallet system's states of a prepared transfer, in the order a transfer passes them. */
7
+ export const TransferStates = ['prepared', 'executing', 'broadcasting', 'submitted', 'confirmed', 'failed', 'expired'];
8
+ /** Reports that a transfer in this state can no longer change. */
9
+ export function transferFinal(state) {
10
+ return state === 'confirmed' || state === 'failed' || state === 'expired';
11
+ }
12
+ export function transferStatusPath(operationId) {
13
+ if (!opaqueId(operationId))
14
+ throw new ClientFlowError('invalid_linked_wallet_request');
15
+ return `/v1/operations/${operationId}/transfer`;
16
+ }
17
+ /** Parses the closed transfer status answer of the operation that was asked about. */
18
+ export function parseTransferStatus(payload, expectedOperationId) {
19
+ const root = record(payload, ['request_id', 'transfer']);
20
+ const t = record(root.transfer, ['operation_id', 'state'], ['transaction_hash', 'failure_reason', 'replaced_by_operation_id', 'authorization_expires_at']);
21
+ const state = t.state;
22
+ const hash = t.transaction_hash, reason = t.failure_reason, replacedBy = t.replaced_by_operation_id, deadline = t.authorization_expires_at;
23
+ if (!opaqueId(root.request_id) || t.operation_id !== expectedOperationId || typeof t.state !== 'string' || !TransferStates.includes(state) ||
24
+ (hash !== undefined && (typeof hash !== 'string' || !/^[A-Za-z0-9_.:-]{1,128}$/.test(hash))) ||
25
+ ((state === 'submitted' || state === 'confirmed') && hash === undefined) ||
26
+ (reason !== undefined && (state !== 'failed' || typeof reason !== 'string' || reason.length === 0 || new TextEncoder().encode(reason).length > 1024 || /[\x00-\x1f\x7f-\x9f]/.test(reason))) ||
27
+ // Another send replaced it only by using its nonce, and no send replaces itself.
28
+ (replacedBy !== undefined && (reason !== 'nonce_used' || !opaqueId(replacedBy) || replacedBy === expectedOperationId)) ||
29
+ // The deadline is told only while the transfer can still change.
30
+ (deadline !== undefined && (!rfc3339UTC(deadline) || transferFinal(state)))) {
31
+ throw new ClientFlowError('invalid_server_response');
32
+ }
33
+ const out = { requestId: root.request_id, operationId: expectedOperationId, state, final: transferFinal(state) };
34
+ if (hash !== undefined)
35
+ out.transactionHash = hash;
36
+ if (reason !== undefined)
37
+ out.failureReason = reason;
38
+ if (replacedBy !== undefined)
39
+ out.replacedByOperationId = replacedBy;
40
+ if (deadline !== undefined)
41
+ out.authorizationExpiresAt = deadline;
42
+ return Object.freeze(out);
43
+ }
@@ -0,0 +1,15 @@
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;
@@ -0,0 +1,43 @@
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
+ }
@@ -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;