forked from Lilly-Protocol/lily-sdk
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathvalidation.ts
More file actions
367 lines (325 loc) · 11.4 KB
/
Copy pathvalidation.ts
File metadata and controls
367 lines (325 loc) · 11.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
import { LilyValidationError } from './errors/sdk-error';
import type { MoneyAmount, ResourceStatus } from './models/common';
import type {
CreateAgentRequest,
UpdateAgentRequest,
} from './models/agent';
import type { ProvisionWalletRequest } from './models/wallet';
import type {
ExecutePaymentRequest,
PaymentQuoteRequest,
} from './models/payment';
import type { ResolveIdentityRequest } from './models/identity';
import type { CreateAgentRequest, UpdateAgentRequest } from './models/agent';
import type { ProvisionWalletRequest } from './models/wallet';
const VALID_NETWORKS = new Set(['stellar-testnet', 'stellar-mainnet']);
const VALID_RESOURCE_STATUSES = new Set<ResourceStatus>([
'pending',
'active',
'inactive',
'failed',
'paused',
]);
const NON_EMPTY_STRING_PATTERN = /\S/;
const DECIMAL_AMOUNT_PATTERN = /^\d+(\.\d+)?$/;
const STELLAR_ASSET_CODE_PATTERN = /^[A-Za-z0-9]{1,12}$/;
const MAX_STELLAR_FRACTIONAL_DIGITS = 7;
const MAX_MEMO_TEXT_LENGTH = 28;
const MEMO_HEX_PATTERN = /^(?:[0-9a-fA-F]{2})*$/;
const MAX_MEMO_HEX_LENGTH = 64;
/**
* Stellar native asset code. XLM has no issuer; passing `assetIssuer` alongside
* it is documented as invalid in the SDK README and `MoneyAmount` model.
*/
const NATIVE_ASSET_CODE = 'XLM';
/**
* Stellar public-key account IDs (G-addresses) used to identify asset issuers.
* They are 56 characters of base32 alphabet (A-Z, 2-7) and must start with 'G'.
* This matches the format documented for issued-asset `assetIssuer` values in
* the SDK README "Native vs. Issued Assets" section.
*/
const STELLAR_PUBLIC_KEY_PATTERN = /^G[A-Z2-7]{55}$/;
export function validateNonEmptyString(
value: unknown,
fieldName: string,
): void {
if (typeof value !== 'string' || !NON_EMPTY_STRING_PATTERN.test(value)) {
throw new LilyValidationError(
`\`${fieldName}\` must be a non-empty string.`,
);
}
}
export function validateMoneyAmount(
amount: MoneyAmount,
context = 'MoneyAmount',
): void {
if (!amount || typeof amount !== 'object') {
throw new LilyValidationError(`${context}: \`amount\` is required.`);
}
if (
typeof amount.assetCode !== 'string' ||
!STELLAR_ASSET_CODE_PATTERN.test(amount.assetCode)
) {
throw new LilyValidationError(
`${context}: \`assetCode\` must be a 1-12 character alphanumeric Stellar asset code.`,
);
}
if (
typeof amount.amount !== 'string' ||
!DECIMAL_AMOUNT_PATTERN.test(amount.amount)
) {
throw new LilyValidationError(
`${context}: \`amount\` must be a non-negative decimal string (e.g. "10.50").`,
);
}
const dotIndex = amount.amount.indexOf('.');
if (dotIndex !== -1) {
const fractionalDigits = amount.amount.length - dotIndex - 1;
if (fractionalDigits > MAX_STELLAR_FRACTIONAL_DIGITS) {
throw new LilyValidationError(
`${context}: \`amount\` must have at most ${MAX_STELLAR_FRACTIONAL_DIGITS} fractional digits (Stellar limit). Got ${fractionalDigits}.`,
);
}
}
if (amount.assetIssuer !== undefined) {
if (
typeof amount.assetIssuer !== 'string' ||
!NON_EMPTY_STRING_PATTERN.test(amount.assetIssuer)
) {
throw new LilyValidationError(
`${context}: \`assetIssuer\` must be a non-empty string when provided.`,
);
}
if (!STELLAR_PUBLIC_KEY_PATTERN.test(amount.assetIssuer)) {
throw new LilyValidationError(
`${context}: \`assetIssuer\` must be a Stellar public key (G-address, 56 characters starting with "G"). Got ${amount.assetIssuer.length} characters.`,
);
}
}
// Native vs. issued-asset rules from the MoneyAmount model + README
// "Native vs. Issued Assets" section:
// - XLM (native) must omit assetIssuer; carrying one is invalid.
// - Non-XLM (issued) assets must carry an assetIssuer; omitting it is invalid.
if (
amount.assetCode === NATIVE_ASSET_CODE &&
amount.assetIssuer !== undefined
) {
throw new LilyValidationError(
`${context}: native asset \`XLM\` must not have an \`assetIssuer\`. Issued assets are required to identify an anchor account; the native asset has none.`,
);
}
if (
amount.assetCode !== NATIVE_ASSET_CODE &&
amount.assetIssuer === undefined
) {
throw new LilyValidationError(
`${context}: issued asset \`${amount.assetCode}\` requires an \`assetIssuer\` to identify the anchor account.`,
);
}
// Native asset semantics: `XLM` is built into the ledger and has no
// issuing account, so `assetIssuer` must be omitted for it (see the
// `MoneyAmount` model docs and the README "Native vs. Issued Assets"
// section). Case-sensitive: only the exact code `XLM` is native.
if (amount.assetCode === 'XLM' && amount.assetIssuer !== undefined) {
throw new LilyValidationError(
`${context}: \`assetIssuer\` must be omitted for the native asset (XLM has no issuing account).`,
);
}
// Issued credit assets are uniquely identified by the (assetCode,
// assetIssuer) pair, so a non-native asset code requires an issuer.
if (amount.assetCode !== 'XLM' && amount.assetIssuer === undefined) {
throw new LilyValidationError(
`${context}: \`assetIssuer\` is required for issued assets (only the native asset XLM may omit it).`,
);
}
}
export function validateMemo(memo: unknown, context = 'Payment'): void {
if (memo === undefined || memo === null) {
return;
}
if (typeof memo !== 'string') {
throw new LilyValidationError(
`${context}: \`memo\` must be a string when provided.`,
);
}
if (MEMO_HEX_PATTERN.test(memo) && memo.length > 0) {
if (memo.length > MAX_MEMO_HEX_LENGTH) {
throw new LilyValidationError(
`${context}: \`memo\` hex string must be at most ${MAX_MEMO_HEX_LENGTH} characters. Got ${memo.length}.`,
);
}
return;
}
const memoBytes = new TextEncoder().encode(memo).length;
if (memoBytes > MAX_MEMO_TEXT_LENGTH) {
throw new LilyValidationError(
`${context}: \`memo\` text must be at most ${MAX_MEMO_TEXT_LENGTH} bytes (Stellar limit). Got ${memoBytes}.`,
);
}
}
export function validateResolveIdentityRequest(
request: ResolveIdentityRequest,
): void {
const keys = [request.agentId, request.stellarAddress, request.domain].filter(
(v) => v !== undefined && v !== null,
);
if (keys.length === 0) {
throw new LilyValidationError(
'`ResolveIdentityRequest` requires exactly one of `agentId`, `stellarAddress`, or `domain`.',
);
}
if (keys.length > 1) {
throw new LilyValidationError(
'`ResolveIdentityRequest` accepts only one resolver key at a time. Provide exactly one of `agentId`, `stellarAddress`, or `domain`.',
);
}
const providedKey =
request.agentId !== undefined
? 'agentId'
: request.stellarAddress !== undefined
? 'stellarAddress'
: 'domain';
const providedValue = request[providedKey as keyof ResolveIdentityRequest];
if (
typeof providedValue !== 'string' ||
!NON_EMPTY_STRING_PATTERN.test(providedValue)
) {
throw new LilyValidationError(
`\`${providedKey}\` must be a non-empty string.`,
);
}
}
export function validateExecutePaymentRequest(
request: ExecutePaymentRequest,
): void {
validateNonEmptyString(request.fromWalletId, 'fromWalletId');
validateNonEmptyString(request.toAddress, 'toAddress');
validateMoneyAmount(request.amount, 'ExecutePaymentRequest');
validateMemo(request.memo, 'ExecutePaymentRequest');
}
export function validatePaymentQuoteRequest(
request: PaymentQuoteRequest,
): void {
validateNonEmptyString(request.fromWalletId, 'fromWalletId');
validateNonEmptyString(request.toAddress, 'toAddress');
validateMoneyAmount(request.amount, 'PaymentQuoteRequest');
}
export function validateProvisionWalletRequest(
request: ProvisionWalletRequest,
): void {
if (!request || typeof request !== 'object') {
throw new LilyValidationError(
'ProvisionWalletRequest: request body is required.',
);
}
validateNonEmptyString(request.agentId, 'agentId');
if (!VALID_NETWORKS.has(request.network)) {
throw new LilyValidationError(
"ProvisionWalletRequest: `network` must be 'stellar-testnet' or 'stellar-mainnet'.",
);
}
if (request.fundingAsset !== undefined && request.fundingAsset !== null) {
validateMoneyAmount(
request.fundingAsset as MoneyAmount,
'ProvisionWalletRequest.fundingAsset',
);
}
}
export function validateCreateAgentRequest(
request: CreateAgentRequest,
): void {
if (!request || typeof request !== 'object') {
throw new LilyValidationError(
'CreateAgentRequest: request body is required.',
);
}
validateNonEmptyString(request.name, 'name');
if (!VALID_NETWORKS.has(request.network)) {
throw new LilyValidationError(
"CreateAgentRequest: `network` must be 'stellar-testnet' or 'stellar-mainnet'.",
);
}
if (
request.description !== undefined &&
request.description !== null &&
typeof request.description !== 'string'
) {
throw new LilyValidationError(
'CreateAgentRequest: `description` must be a string when provided.',
);
}
if (request.capabilities !== undefined && request.capabilities !== null) {
if (!Array.isArray(request.capabilities)) {
throw new LilyValidationError(
'CreateAgentRequest: `capabilities` must be an array when provided.',
);
}
for (const cap of request.capabilities) {
if (typeof cap !== 'string' || !NON_EMPTY_STRING_PATTERN.test(cap)) {
throw new LilyValidationError(
'CreateAgentRequest: each capability must be a non-empty string.',
);
}
}
}
if (request.metadata !== undefined && request.metadata !== null) {
if (typeof request.metadata !== 'object' || Array.isArray(request.metadata)) {
throw new LilyValidationError(
'CreateAgentRequest: `metadata` must be an object when provided.',
);
}
}
}
export function validateUpdateAgentRequest(
request: UpdateAgentRequest,
): void {
if (!request || typeof request !== 'object' || Array.isArray(request)) {
throw new LilyValidationError(
'UpdateAgentRequest: request body must be an object.',
);
}
const hasUpdate =
request.name !== undefined ||
request.description !== undefined ||
request.capabilities !== undefined ||
request.status !== undefined;
if (!hasUpdate) {
throw new LilyValidationError(
'UpdateAgentRequest: at least one update field must be provided.',
);
}
if (request.name !== undefined) {
validateNonEmptyString(request.name, 'name');
}
if (
request.description !== undefined &&
request.description !== null &&
typeof request.description !== 'string'
) {
throw new LilyValidationError(
'UpdateAgentRequest: `description` must be a string when provided.',
);
}
if (request.capabilities !== undefined && request.capabilities !== null) {
if (!Array.isArray(request.capabilities)) {
throw new LilyValidationError(
'UpdateAgentRequest: `capabilities` must be an array when provided.',
);
}
for (const cap of request.capabilities) {
if (typeof cap !== 'string' || !NON_EMPTY_STRING_PATTERN.test(cap)) {
throw new LilyValidationError(
'UpdateAgentRequest: each capability must be a non-empty string.',
);
}
}
}
if (
request.status !== undefined &&
!VALID_RESOURCE_STATUSES.has(request.status)
) {
throw new LilyValidationError(
"UpdateAgentRequest: `status` must be one of: 'pending', 'active', 'inactive', 'failed', 'paused'.",
);
}
}