@funyard-org/cli 0.0.0-stage → 0.6.3

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.
@@ -0,0 +1,843 @@
1
+ const str = { type: 'string' };
2
+ const integer = { type: 'integer' };
3
+ const nonnegative = { type: 'integer', minimum: 0 };
4
+ const bool = { type: 'boolean' };
5
+ const points = { const: 'test_point' };
6
+ export const requestIdSchema = {
7
+ type: 'string',
8
+ pattern: '^[a-z0-9][a-z0-9_-]{0,63}$',
9
+ maxLength: 64,
10
+ description: 'Save this stable business ID before submitting. After a timeout use get_call with the same ID; never replace it to retry.',
11
+ };
12
+ export const callIdSchema = { type: 'string', pattern: '^call_[A-Za-z0-9_-]{1,64}$' };
13
+ export const REQUEST_ID = new RegExp(requestIdSchema.pattern);
14
+ export const CALL_ID = new RegExp(callIdSchema.pattern);
15
+ export const CAPABILITY_IDS = [
16
+ 'finance.quote',
17
+ 'web.search',
18
+ 'news.search',
19
+ 'web.read',
20
+ 'company.discover',
21
+ 'research.search',
22
+ 'finance.get',
23
+ 'crypto.get',
24
+ 'economy.get',
25
+ 'tiktok.get',
26
+ 'instagram.get',
27
+ 'youtube.get',
28
+ 'linkedin.get',
29
+ 'social.trending',
30
+ 'news.get',
31
+ ];
32
+ /** Canonical field id: category.group.name. The group is the public billing unit. */
33
+ export const FIELD_ID = /^[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*){2}$/;
34
+ /** Field-level capabilities: the argument naming the subject, and the field category they serve. */
35
+ export const FIELD_CAPABILITIES = {
36
+ 'finance.get': { subject: 'symbol', category: 'finance' },
37
+ 'crypto.get': { subject: 'coin', category: 'crypto' },
38
+ 'economy.get': { subject: 'series', category: 'economy' },
39
+ 'tiktok.get': { subject: 'handle', category: 'tiktok' },
40
+ 'instagram.get': { subject: 'handle', category: 'instagram' },
41
+ 'youtube.get': { subject: 'handle', category: 'youtube' },
42
+ 'linkedin.get': { subject: 'company', category: 'linkedin' },
43
+ 'news.get': { subject: 'symbols', category: 'news' },
44
+ };
45
+ export function fieldCapability(id) {
46
+ return Object.hasOwn(FIELD_CAPABILITIES, id) ? FIELD_CAPABILITIES[id] : undefined;
47
+ }
48
+ export const ARGUMENT_DESCRIPTIONS = {
49
+ sources: '可选 all:获取全部可用来源,每个来源分别按字段组扣费;省略时只按最低上游成本取数。先用 fields-quote 估价。',
50
+ symbol: '股票代码,例如 AAPL;会自动去掉首尾空格并转为大写。',
51
+ coin: '加密货币 ID,例如 bitcoin、ethereum、usd-coin;会自动转为小写。',
52
+ series: '经济指标序列 ID,例如 UNRATE(美国失业率)、CPIAUCSL(美国 CPI);会自动转为大写。',
53
+ handle: '公开账号的用户名,例如 natgeo;可带开头的 @,会自动去掉并转为小写。',
54
+ platform: '热榜所在平台,目前支持 weibo(微博热搜)。',
55
+ symbols: '股票代码,逗号分隔,例如 AAPL,MSFT。',
56
+ company: 'LinkedIn 公司主页的标识,例如 stripe,或完整链接 https://www.linkedin.com/company/stripe/。',
57
+ fields: '要获取的字段 ID,逗号分隔,例如 finance.quote.price,finance.company.ceo。可用字段和每个字段组的计费标准见 /funyard/v1/fields 目录;同一字段组内取多少字段都只按该组计一次。',
58
+ query: '要搜索的关键词或问题。',
59
+ language: '搜索语言代码,例如 en;不传则使用默认语言。',
60
+ region: '搜索地区代码,例如 us;不传则使用默认地区。',
61
+ domain: '企业网站域名,例如 example.com,不含协议、端口或路径。',
62
+ url: '要读取的完整 HTTPS 网址;网站须在允许访问的范围内。',
63
+ };
64
+ export const argumentFields = {
65
+ sources: { type: 'string', enum: ['all'], minLength: 3, maxLength: 3, description: ARGUMENT_DESCRIPTIONS.sources },
66
+ symbol: { type: 'string', maxLength: 32, description: ARGUMENT_DESCRIPTIONS.symbol },
67
+ coin: { type: 'string', maxLength: 100, description: ARGUMENT_DESCRIPTIONS.coin },
68
+ series: { type: 'string', maxLength: 64, description: ARGUMENT_DESCRIPTIONS.series },
69
+ handle: { type: 'string', maxLength: 101, description: ARGUMENT_DESCRIPTIONS.handle },
70
+ company: { type: 'string', maxLength: 200, description: ARGUMENT_DESCRIPTIONS.company },
71
+ symbols: { type: 'string', maxLength: 500, description: ARGUMENT_DESCRIPTIONS.symbols },
72
+ platform: { type: 'string', enum: ['weibo'], description: ARGUMENT_DESCRIPTIONS.platform },
73
+ fields: { type: 'string', minLength: 1, maxLength: 2048, description: ARGUMENT_DESCRIPTIONS.fields },
74
+ query: { type: 'string', minLength: 1, maxLength: 1024, description: ARGUMENT_DESCRIPTIONS.query },
75
+ domain: { type: 'string', maxLength: 253, description: ARGUMENT_DESCRIPTIONS.domain },
76
+ url: { type: 'string', format: 'uri', maxLength: 2048, description: ARGUMENT_DESCRIPTIONS.url },
77
+ language: {
78
+ type: 'string',
79
+ enum: ['en', 'en-us', 'en-gb', 'zh-cn', 'zh-tw', 'ja', 'ko', 'de', 'fr', 'es', 'it', 'pt', 'ru', 'ar'],
80
+ description: ARGUMENT_DESCRIPTIONS.language,
81
+ },
82
+ region: {
83
+ type: 'string',
84
+ enum: ['us', 'gb', 'ca', 'au', 'de', 'fr', 'es', 'it', 'jp', 'kr', 'tw', 'hk', 'cn', 'sg', 'in', 'br', 'mx'],
85
+ description: ARGUMENT_DESCRIPTIONS.region,
86
+ },
87
+ };
88
+ export const capabilityArguments = {
89
+ 'finance.quote': { required: ['symbol'], optional: [] },
90
+ 'web.search': { required: ['query'], optional: ['language', 'region'] },
91
+ 'news.search': { required: ['query'], optional: ['language', 'region'] },
92
+ 'web.read': { required: ['url'], optional: [] },
93
+ 'company.discover': { required: ['domain'], optional: [] },
94
+ 'research.search': { required: ['query'], optional: ['language'] },
95
+ 'finance.get': { required: ['fields'], optional: ['symbol', 'sources'] },
96
+ 'crypto.get': { required: ['fields'], optional: ['coin', 'sources'] },
97
+ 'economy.get': { required: ['fields'], optional: ['series', 'sources'] },
98
+ 'tiktok.get': { required: ['fields'], optional: ['handle', 'sources'] },
99
+ 'instagram.get': { required: ['fields'], optional: ['handle', 'sources'] },
100
+ 'youtube.get': { required: ['fields'], optional: ['handle', 'sources'] },
101
+ 'linkedin.get': { required: ['fields'], optional: ['company', 'sources'] },
102
+ 'social.trending': { required: ['platform'], optional: [] },
103
+ 'news.get': { required: ['fields'], optional: ['symbols', 'sources'] },
104
+ };
105
+ /** Route policy may narrow fields; it never adds an independently maintained schema. */
106
+ export function capabilityInputSchema(id, internalSearch = false) {
107
+ const spec = capabilityArguments[id], required = [...spec.required], optional = spec.optional.filter(name => !(internalSearch && name === 'region'));
108
+ return {
109
+ type: 'object',
110
+ properties: Object.fromEntries([...required, ...optional].map(name => [
111
+ name,
112
+ internalSearch && name === 'language'
113
+ ? { type: 'string', maxLength: 8, description: ARGUMENT_DESCRIPTIONS.language }
114
+ : argumentFields[name],
115
+ ])),
116
+ required,
117
+ additionalProperties: false,
118
+ };
119
+ }
120
+ export function mcpInputSchema(input) {
121
+ return {
122
+ ...input,
123
+ properties: { ...input.properties, request_id: requestIdSchema },
124
+ required: [...(input.required ?? []), 'request_id'],
125
+ };
126
+ }
127
+ export const lookupSchema = {
128
+ type: 'object',
129
+ properties: { call_id: callIdSchema, request_id: requestIdSchema },
130
+ additionalProperties: false,
131
+ oneOf: [{ required: ['call_id'] }, { required: ['request_id'] }],
132
+ };
133
+ export const cancelSchema = {
134
+ type: 'object',
135
+ properties: { call_id: callIdSchema },
136
+ required: ['call_id'],
137
+ additionalProperties: false,
138
+ };
139
+ export const emptySchema = { type: 'object', properties: {}, additionalProperties: false };
140
+ /** MCP/SDK inputs for field discovery and dry-run pricing. */
141
+ export const fieldsLookupSchema = {
142
+ type: 'object',
143
+ properties: { category: { type: 'string', minLength: 1, maxLength: 32 } },
144
+ additionalProperties: false,
145
+ };
146
+ const exploreTerm = {
147
+ type: 'string',
148
+ minLength: 1,
149
+ maxLength: 100,
150
+ pattern: '^[^&#?=%/\\\\\\u0000-\\u001f]+$',
151
+ };
152
+ /** Generic call for arguments a capability's published schema leaves out; the server validates them. */
153
+ export const genericCallRequestSchema = {
154
+ type: 'object',
155
+ properties: {
156
+ capability: { type: 'string', pattern: '^[a-z][a-z0-9_]{0,31}\\.[a-z][a-z0-9_]{0,31}$' },
157
+ arguments: { type: 'object', additionalProperties: { type: 'string', maxLength: 2048 } },
158
+ request_id: requestIdSchema,
159
+ },
160
+ required: ['capability', 'arguments', 'request_id'],
161
+ additionalProperties: false,
162
+ };
163
+ /** At most one of these per request: the level to open, or a keyword to search. */
164
+ export const exploreRequestSchema = {
165
+ type: 'object',
166
+ properties: { domain: exploreTerm, capability: exploreTerm, group: exploreTerm, q: exploreTerm },
167
+ additionalProperties: false,
168
+ };
169
+ export const fieldsQuoteRequestSchema = {
170
+ type: 'object',
171
+ properties: {
172
+ fields: { type: 'string', minLength: 1, maxLength: 2048 },
173
+ sources: { const: 'all' },
174
+ capability: { type: 'string', pattern: '^[a-z][a-z0-9_]{0,31}\\.[a-z][a-z0-9_]{0,31}$' },
175
+ arguments: { type: 'object', additionalProperties: { type: 'string', maxLength: 200 } },
176
+ },
177
+ required: ['fields'],
178
+ additionalProperties: false,
179
+ };
180
+ export const submissionSchema = {
181
+ type: 'object',
182
+ properties: {
183
+ request_id: requestIdSchema,
184
+ capability: str,
185
+ arguments: { type: 'object', additionalProperties: str },
186
+ },
187
+ required: ['request_id', 'capability', 'arguments'],
188
+ additionalProperties: false,
189
+ };
190
+ export const callIdentitySchema = {
191
+ type: 'object',
192
+ properties: {
193
+ call_id: callIdSchema,
194
+ request_id: requestIdSchema,
195
+ capability: str,
196
+ status: { enum: ['queued', 'running', 'pending', 'succeeded', 'empty', 'failed'] },
197
+ },
198
+ required: ['call_id', 'request_id', 'status'],
199
+ };
200
+ const balanceSchema = {
201
+ type: 'object',
202
+ properties: { available: nonnegative, held: nonnegative, spent: nonnegative },
203
+ required: ['available', 'held', 'spent'],
204
+ };
205
+ const errorDetail = {
206
+ type: 'object',
207
+ properties: { code: str, message: str, retry_at: nonnegative, cause_code: str },
208
+ required: ['code', 'message'],
209
+ };
210
+ const nil = { type: 'null' };
211
+ const timestamp = { type: 'string', pattern: '^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?Z$' };
212
+ const resultUrl = { type: 'string', format: 'uri', maxLength: 4096 };
213
+ export const quoteResultSchema = {
214
+ type: 'object',
215
+ properties: {
216
+ symbol: { type: 'string', pattern: '^\\^?[A-Z0-9][A-Z0-9.=-]{0,30}$' },
217
+ name: { type: 'string', maxLength: 500 },
218
+ price: { type: 'string', pattern: '^(?:0|[1-9]\\d{0,42})(?:\\.\\d{1,36})?$' },
219
+ currency: nil,
220
+ exchange: { type: 'string', maxLength: 100 },
221
+ data_time: { oneOf: [timestamp, nil] },
222
+ },
223
+ required: ['symbol', 'name', 'price', 'currency', 'exchange', 'data_time'],
224
+ additionalProperties: false,
225
+ };
226
+ export const searchItemSchema = {
227
+ type: 'object',
228
+ properties: {
229
+ title: { type: 'string', maxLength: 512 },
230
+ url: resultUrl,
231
+ snippet: { type: 'string', maxLength: 4096 },
232
+ source: { type: 'string', maxLength: 256 },
233
+ published_at: timestamp,
234
+ },
235
+ required: ['title', 'url', 'snippet'],
236
+ additionalProperties: false,
237
+ };
238
+ export const searchResultSchema = {
239
+ type: 'object',
240
+ properties: {
241
+ items: { type: 'array', items: searchItemSchema, minItems: 1, maxItems: 10 },
242
+ omitted_links: { type: 'integer', minimum: 1 },
243
+ },
244
+ required: ['items'],
245
+ additionalProperties: false,
246
+ };
247
+ /** One trending topic: its rank on the list, the search link, and the platform's own heat score. */
248
+ export const trendingItemSchema = {
249
+ type: 'object',
250
+ properties: {
251
+ rank: { type: 'integer', minimum: 1, maximum: 100 },
252
+ title: { type: 'string', minLength: 1, maxLength: 512 },
253
+ url: resultUrl,
254
+ heat: { oneOf: [nonnegative, nil] },
255
+ label: { oneOf: [{ type: 'string', maxLength: 32 }, nil] },
256
+ },
257
+ required: ['rank', 'title', 'url', 'heat', 'label'],
258
+ additionalProperties: false,
259
+ };
260
+ export const trendingResultSchema = {
261
+ type: 'object',
262
+ properties: { items: { type: 'array', items: trendingItemSchema, minItems: 1, maxItems: 50 } },
263
+ required: ['items'],
264
+ additionalProperties: false,
265
+ };
266
+ export const readResultSchema = {
267
+ type: 'object',
268
+ properties: {
269
+ url: resultUrl,
270
+ title: { type: 'string', maxLength: 1000 },
271
+ markdown: { type: 'string', maxLength: 49152, pattern: '\\S' },
272
+ omitted_links: nonnegative,
273
+ },
274
+ required: ['url', 'title', 'markdown', 'omitted_links'],
275
+ additionalProperties: false,
276
+ };
277
+ const nullableCount = { oneOf: [nonnegative, nil] };
278
+ export const companyItemSchema = {
279
+ type: 'object',
280
+ properties: {
281
+ domain: { type: 'string', maxLength: 253 },
282
+ company_name: { type: 'string', minLength: 1, maxLength: 500 },
283
+ email_counts: {
284
+ oneOf: [
285
+ {
286
+ type: 'object',
287
+ properties: { personal: nullableCount, generic: nullableCount, total: nullableCount },
288
+ required: ['personal', 'generic', 'total'],
289
+ additionalProperties: false,
290
+ },
291
+ nil,
292
+ ],
293
+ },
294
+ },
295
+ required: ['domain', 'company_name', 'email_counts'],
296
+ additionalProperties: false,
297
+ };
298
+ export const companyResultSchema = {
299
+ type: 'object',
300
+ properties: { items: { type: 'array', items: companyItemSchema, minItems: 1, maxItems: 1 } },
301
+ required: ['items'],
302
+ additionalProperties: false,
303
+ };
304
+ /** Field-level outcome. A group is charged only when every requested field in it is succeeded or empty. */
305
+ export const fieldStatusSchema = { enum: ['succeeded', 'empty', 'pending', 'failed'] };
306
+ const fieldScalar = { oneOf: [str, { type: 'number' }, bool, nil] };
307
+ /** A scalar, or a column of scalars: fields of a list-shaped group (statements, histories, posts)
308
+ * hold one value per row, and every field of that group lists its rows in the same order. */
309
+ export const fieldValueSchema = {
310
+ oneOf: [str, { type: 'number' }, bool, nil, { type: 'array', maxItems: 1000, items: fieldScalar }],
311
+ };
312
+ /** With sources=all, each source's own outcome under an anonymous label that is stable per source. */
313
+ const fieldSourceSchema = {
314
+ type: 'object',
315
+ properties: {
316
+ source: { type: 'string', pattern: '^s[1-9][0-9]?$' },
317
+ status: fieldStatusSchema,
318
+ value: fieldValueSchema,
319
+ },
320
+ required: ['source', 'status', 'value'],
321
+ additionalProperties: false,
322
+ };
323
+ const fieldsMap = {
324
+ type: 'object',
325
+ additionalProperties: {
326
+ type: 'object',
327
+ properties: {
328
+ status: fieldStatusSchema,
329
+ value: fieldValueSchema,
330
+ sources: { type: 'array', minItems: 1, maxItems: 20, items: fieldSourceSchema },
331
+ },
332
+ required: ['status', 'value'],
333
+ additionalProperties: false,
334
+ },
335
+ };
336
+ export const fieldsResultSchema = {
337
+ type: 'object',
338
+ properties: { symbol: quoteResultSchema.properties.symbol, fields: fieldsMap },
339
+ required: ['fields'],
340
+ // Echo of every argument the request carried.
341
+ additionalProperties: { type: 'string', maxLength: 200 },
342
+ };
343
+ export const cryptoFieldsResultSchema = {
344
+ type: 'object',
345
+ properties: { coin: { type: 'string', pattern: '^[a-z0-9][a-z0-9-]{0,99}$' }, fields: fieldsMap },
346
+ required: ['fields'],
347
+ // Echo of every argument the request carried.
348
+ additionalProperties: { type: 'string', maxLength: 200 },
349
+ };
350
+ export const economyFieldsResultSchema = {
351
+ type: 'object',
352
+ properties: { series: { type: 'string', pattern: '^[A-Z0-9][A-Z0-9_]{0,63}$' }, fields: fieldsMap },
353
+ required: ['fields'],
354
+ // Echo of every argument the request carried.
355
+ additionalProperties: { type: 'string', maxLength: 200 },
356
+ };
357
+ export const socialFieldsResultSchema = {
358
+ type: 'object',
359
+ properties: { handle: { type: 'string', pattern: '^[a-z0-9_][a-z0-9._-]{0,99}$' }, fields: fieldsMap },
360
+ required: ['fields'],
361
+ // Echo of every argument the request carried.
362
+ additionalProperties: { type: 'string', maxLength: 200 },
363
+ };
364
+ /** Field results whose capability has no normalized subject: the field map plus echoed arguments. */
365
+ export const echoFieldsResultSchema = {
366
+ type: 'object',
367
+ properties: { fields: fieldsMap },
368
+ required: ['fields'],
369
+ additionalProperties: { type: 'string', maxLength: 500 },
370
+ };
371
+ export const linkedinFieldsResultSchema = {
372
+ type: 'object',
373
+ properties: { company: { type: 'string', pattern: '^[a-z0-9][a-z0-9_-]{0,99}$' }, fields: fieldsMap },
374
+ required: ['fields'],
375
+ // Echo of every argument the request carried.
376
+ additionalProperties: { type: 'string', maxLength: 200 },
377
+ };
378
+ export const resultSchemas = {
379
+ 'finance.quote': quoteResultSchema,
380
+ 'web.search': searchResultSchema,
381
+ 'news.search': searchResultSchema,
382
+ 'research.search': searchResultSchema,
383
+ 'web.read': readResultSchema,
384
+ 'company.discover': companyResultSchema,
385
+ 'finance.get': fieldsResultSchema,
386
+ 'crypto.get': cryptoFieldsResultSchema,
387
+ 'economy.get': economyFieldsResultSchema,
388
+ 'tiktok.get': socialFieldsResultSchema,
389
+ 'instagram.get': socialFieldsResultSchema,
390
+ 'youtube.get': socialFieldsResultSchema,
391
+ 'linkedin.get': linkedinFieldsResultSchema,
392
+ 'social.trending': trendingResultSchema,
393
+ 'news.get': echoFieldsResultSchema,
394
+ };
395
+ const emptyItems = {
396
+ type: 'object',
397
+ properties: { items: { type: 'array', items: {}, maxItems: 0 } },
398
+ required: ['items'],
399
+ additionalProperties: false,
400
+ };
401
+ export const emptyResultSchemas = {
402
+ 'finance.quote': nil,
403
+ 'web.search': emptyItems,
404
+ 'news.search': emptyItems,
405
+ 'research.search': emptyItems,
406
+ 'company.discover': emptyItems,
407
+ 'social.trending': emptyItems,
408
+ 'web.read': {
409
+ ...readResultSchema,
410
+ properties: { ...readResultSchema.properties, markdown: { type: 'string', maxLength: 49152, pattern: '^\\s*$' } },
411
+ },
412
+ 'finance.get': fieldsResultSchema,
413
+ 'crypto.get': cryptoFieldsResultSchema,
414
+ 'economy.get': economyFieldsResultSchema,
415
+ 'tiktok.get': socialFieldsResultSchema,
416
+ 'instagram.get': socialFieldsResultSchema,
417
+ 'youtube.get': socialFieldsResultSchema,
418
+ 'linkedin.get': linkedinFieldsResultSchema,
419
+ 'news.get': echoFieldsResultSchema,
420
+ };
421
+ /** Only field-level capabilities deliver confirmed fields while others are still pending. */
422
+ export const PARTIAL_RESULT_CAPABILITIES = Object.keys(FIELD_CAPABILITIES);
423
+ /** Shape shared by every field-level result: the field map plus an echo of the request's arguments. Lets a client
424
+ * accept field capabilities the server added after it was built, without trusting arbitrary data. */
425
+ export function fieldsResultMatches(status, value) {
426
+ if (value === null)
427
+ return ['queued', 'running', 'pending', 'failed'].includes(status);
428
+ if (!['succeeded', 'empty', 'pending'].includes(status) || typeof value !== 'object' || Array.isArray(value))
429
+ return false;
430
+ const record = value, echoed = Object.keys(record).filter(key => key !== 'fields');
431
+ return (echoed.length <= 16 &&
432
+ echoed.every(key => /^[a-z][a-z0-9_]{0,31}$/.test(key) && typeof record[key] === 'string' && record[key].length <= 200) &&
433
+ matches(fieldsMap, record.fields));
434
+ }
435
+ export function resultMatches(capability, status, value) {
436
+ if (!Object.hasOwn(resultSchemas, capability))
437
+ return false;
438
+ if (status === 'succeeded')
439
+ return matches(resultSchemas[capability], value);
440
+ if (status === 'empty')
441
+ return matches(emptyResultSchemas[capability], value);
442
+ if (status === 'pending' && PARTIAL_RESULT_CAPABILITIES.includes(capability))
443
+ return value === null || matches(resultSchemas[capability], value);
444
+ return ['queued', 'running', 'pending', 'failed'].includes(status) && value === null;
445
+ }
446
+ export const billingLineSchema = {
447
+ type: 'object',
448
+ properties: {
449
+ group: str,
450
+ source: { type: 'string', pattern: '^s[1-9][0-9]?$' },
451
+ title: str,
452
+ weight: nonnegative,
453
+ fields: { type: 'array', items: str },
454
+ status: { enum: ['queued', 'running', 'pending', 'succeeded', 'empty', 'failed'] },
455
+ charged: nonnegative,
456
+ settled_at: nonnegative,
457
+ },
458
+ required: ['group', 'weight', 'fields', 'status', 'charged'],
459
+ };
460
+ export const callEnvelopeSchema = {
461
+ type: 'object',
462
+ properties: {
463
+ ...callIdentitySchema.properties,
464
+ capability_version: { const: 1 },
465
+ replayed: bool,
466
+ created_at: nonnegative,
467
+ idempotency_expires_at: nonnegative,
468
+ trace_id: str,
469
+ source: { type: 'object', properties: { requested_at: nonnegative, fetched_at: str }, required: ['requested_at'] },
470
+ billing: {
471
+ type: 'object',
472
+ properties: {
473
+ unit: points,
474
+ rate_version: str,
475
+ price: nonnegative,
476
+ held: nonnegative,
477
+ charged: nonnegative,
478
+ balance_snapshot: balanceSchema,
479
+ /** One line per field group; present for field-level capabilities. */
480
+ lines: { type: 'array', items: billingLineSchema },
481
+ },
482
+ required: ['unit', 'rate_version', 'price', 'held', 'charged', 'balance_snapshot'],
483
+ },
484
+ retention_deferred: bool,
485
+ poll_after_ms: nonnegative,
486
+ error: errorDetail,
487
+ },
488
+ required: [
489
+ 'call_id',
490
+ 'request_id',
491
+ 'capability',
492
+ 'capability_version',
493
+ 'status',
494
+ 'replayed',
495
+ 'created_at',
496
+ 'idempotency_expires_at',
497
+ 'trace_id',
498
+ 'source',
499
+ 'billing',
500
+ ],
501
+ };
502
+ const resultBranches = CAPABILITY_IDS.flatMap(capability => [
503
+ {
504
+ type: 'object',
505
+ properties: { capability: { const: capability }, status: { const: 'succeeded' }, data: resultSchemas[capability] },
506
+ },
507
+ {
508
+ type: 'object',
509
+ properties: { capability: { const: capability }, status: { const: 'empty' }, data: emptyResultSchemas[capability] },
510
+ },
511
+ ...(PARTIAL_RESULT_CAPABILITIES.includes(capability)
512
+ ? [
513
+ {
514
+ type: 'object',
515
+ properties: {
516
+ capability: { const: capability },
517
+ status: { const: 'pending' },
518
+ data: { oneOf: [resultSchemas[capability], nil] },
519
+ },
520
+ },
521
+ {
522
+ type: 'object',
523
+ properties: {
524
+ capability: { const: capability },
525
+ status: { enum: ['queued', 'running', 'failed'] },
526
+ data: nil,
527
+ },
528
+ },
529
+ ]
530
+ : [
531
+ {
532
+ type: 'object',
533
+ properties: {
534
+ capability: { const: capability },
535
+ status: { enum: ['queued', 'running', 'pending', 'failed'] },
536
+ data: nil,
537
+ },
538
+ },
539
+ ]),
540
+ ]);
541
+ // Summary list deliberately omits data; a full submission/read response must have it.
542
+ export const callSummarySchema = { ...callEnvelopeSchema, oneOf: resultBranches };
543
+ export const callSchema = { ...callSummarySchema, required: [...callEnvelopeSchema.required, 'data'] };
544
+ export const capabilitySchema = {
545
+ type: 'object',
546
+ properties: {
547
+ name: { enum: CAPABILITY_IDS },
548
+ title: str,
549
+ description: str,
550
+ category: str,
551
+ version: { const: 1 },
552
+ price: nonnegative,
553
+ unit: points,
554
+ /** per_call: price is charged per request. per_field_group: price is the smallest charge; see /fields/quote. */
555
+ price_model: { enum: ['per_call', 'per_field_group'] },
556
+ price_description: str,
557
+ input_schema: { type: 'object' },
558
+ enabled: bool,
559
+ },
560
+ required: ['name', 'title', 'description', 'category', 'version', 'price', 'unit', 'input_schema', 'enabled'],
561
+ };
562
+ export const catalogSchema = {
563
+ type: 'object',
564
+ properties: { capabilities: { type: 'array', items: capabilitySchema } },
565
+ required: ['capabilities'],
566
+ };
567
+ export const accountSchema = {
568
+ type: 'object',
569
+ properties: {
570
+ user_id: nonnegative,
571
+ enabled: bool,
572
+ capabilities: str,
573
+ ...balanceSchema.properties,
574
+ unit: points,
575
+ created_at: nonnegative,
576
+ limits: {
577
+ type: 'object',
578
+ properties: {
579
+ rpm: nonnegative,
580
+ daily_calls: nonnegative,
581
+ daily_points: nonnegative,
582
+ concurrency: nonnegative,
583
+ queue: nonnegative,
584
+ held_points: nonnegative,
585
+ },
586
+ required: ['rpm', 'daily_calls', 'daily_points', 'concurrency', 'queue', 'held_points'],
587
+ },
588
+ preferences: { type: 'object', properties: { language: { enum: ['zh', 'en'] } }, required: ['language'] },
589
+ },
590
+ required: [
591
+ 'user_id',
592
+ 'enabled',
593
+ 'capabilities',
594
+ 'available',
595
+ 'held',
596
+ 'spent',
597
+ 'unit',
598
+ 'created_at',
599
+ 'limits',
600
+ 'preferences',
601
+ ],
602
+ };
603
+ export const meSchema = {
604
+ type: 'object',
605
+ properties: { account: accountSchema, key_id: str },
606
+ required: ['account', 'key_id'],
607
+ };
608
+ export const usageSchema = {
609
+ type: 'object',
610
+ properties: {
611
+ unit: points,
612
+ entries: {
613
+ type: 'array',
614
+ items: {
615
+ type: 'object',
616
+ properties: {
617
+ call_id: str,
618
+ kind: str,
619
+ available_delta: integer,
620
+ held_delta: integer,
621
+ spent_delta: integer,
622
+ created_at: nonnegative,
623
+ /** Field group this settlement belongs to; absent for whole-call settlements. */
624
+ line: str,
625
+ },
626
+ required: ['call_id', 'kind', 'available_delta', 'held_delta', 'spent_delta', 'created_at'],
627
+ },
628
+ },
629
+ },
630
+ required: ['unit', 'entries'],
631
+ };
632
+ export const errorSchema = {
633
+ type: 'object',
634
+ properties: { error: errorDetail, trace_id: str },
635
+ required: ['error', 'trace_id'],
636
+ };
637
+ const fieldGroupSchema = {
638
+ type: 'object',
639
+ properties: { id: str, category: str, title: str, price: nonnegative, unit: points, rate_version: str },
640
+ required: ['id', 'category', 'title', 'price', 'unit', 'rate_version'],
641
+ };
642
+ export const fieldDefinitionSchema = {
643
+ type: 'object',
644
+ properties: {
645
+ id: { type: 'string', pattern: FIELD_ID.source },
646
+ group: str,
647
+ category: str,
648
+ title: str,
649
+ type: { enum: ['string', 'number', 'integer', 'boolean', 'decimal', 'timestamp', 'scalar'] },
650
+ /** Points charged for the whole group when any of its fields is requested. */
651
+ price: nonnegative,
652
+ unit: points,
653
+ rate_version: str,
654
+ arguments: { type: 'array', items: str },
655
+ /** Optional arguments its sources forward (period, limit, from...). */
656
+ options: { type: 'array', items: str },
657
+ /** Delivered as a column: one value per row, aligned with the other fields of its group. */
658
+ list: bool,
659
+ },
660
+ required: ['id', 'group', 'category', 'title', 'type', 'price', 'unit', 'rate_version', 'arguments'],
661
+ };
662
+ export const fieldsCatalogSchema = {
663
+ type: 'object',
664
+ properties: {
665
+ groups: { type: 'array', items: fieldGroupSchema },
666
+ fields: { type: 'array', items: fieldDefinitionSchema },
667
+ },
668
+ required: ['groups', 'fields'],
669
+ };
670
+ const exploreGroupSchema = {
671
+ type: 'object',
672
+ properties: { id: str, title: str, price: nonnegative, fields: nonnegative },
673
+ required: ['id', 'title', 'price', 'fields'],
674
+ };
675
+ const exploreCapabilitySchema = {
676
+ type: 'object',
677
+ properties: {
678
+ name: str,
679
+ title: str,
680
+ description: str,
681
+ domain: str,
682
+ /** fields: pick field ids and pay per field group. call: one fixed result per request. */
683
+ kind: { enum: ['fields', 'call'] },
684
+ /** Callable now with this key: in its scope and served by an enabled route. */
685
+ available: bool,
686
+ price: nonnegative,
687
+ unit: points,
688
+ price_model: { enum: ['per_call', 'per_field_group'] },
689
+ input_schema: { type: 'object' },
690
+ groups: { type: 'array', items: exploreGroupSchema },
691
+ example: { type: 'object' },
692
+ },
693
+ required: ['name', 'title', 'description', 'domain', 'kind', 'available', 'price', 'unit', 'price_model'],
694
+ };
695
+ /** Progressive discovery: root -> domain -> capability -> group -> fields, or a keyword search. */
696
+ export const exploreSchema = {
697
+ type: 'object',
698
+ properties: {
699
+ level: { enum: ['root', 'domain', 'capability', 'group', 'search'] },
700
+ domains: {
701
+ type: 'array',
702
+ items: {
703
+ type: 'object',
704
+ properties: { id: str, title: str, description: str, capabilities: nonnegative, fields: nonnegative },
705
+ required: ['id', 'title', 'description', 'capabilities', 'fields'],
706
+ },
707
+ },
708
+ domain: {
709
+ type: 'object',
710
+ properties: { id: str, title: str, description: str },
711
+ required: ['id', 'title', 'description'],
712
+ },
713
+ capabilities: { type: 'array', items: exploreCapabilitySchema },
714
+ capability: exploreCapabilitySchema,
715
+ group: {
716
+ type: 'object',
717
+ properties: {
718
+ ...exploreGroupSchema.properties,
719
+ capability: str,
720
+ unit: points,
721
+ fields: { type: 'array', items: fieldDefinitionSchema },
722
+ },
723
+ required: ['id', 'title', 'price', 'capability', 'unit', 'fields'],
724
+ },
725
+ matches: { type: 'array', items: fieldDefinitionSchema },
726
+ next: str,
727
+ },
728
+ required: ['level', 'next'],
729
+ };
730
+ /** Dry-run price for a field list; nothing is reserved or charged. */
731
+ export const fieldsQuoteSchema = {
732
+ type: 'object',
733
+ properties: {
734
+ sources: { const: 'all' },
735
+ unit: points,
736
+ rate_version: str,
737
+ total: nonnegative,
738
+ lines: {
739
+ type: 'array',
740
+ items: {
741
+ type: 'object',
742
+ properties: {
743
+ group: str,
744
+ source: { type: 'string', pattern: '^s[1-9][0-9]?$' },
745
+ title: str,
746
+ weight: nonnegative,
747
+ fields: { type: 'array', items: str },
748
+ },
749
+ required: ['group', 'weight', 'fields'],
750
+ },
751
+ },
752
+ unknown: { type: 'array', items: str },
753
+ },
754
+ required: ['unit', 'rate_version', 'total', 'lines', 'unknown'],
755
+ };
756
+ export const schemas = {
757
+ submission: submissionSchema,
758
+ callIdentity: callIdentitySchema,
759
+ call: callSchema,
760
+ catalog: catalogSchema,
761
+ account: accountSchema,
762
+ me: meSchema,
763
+ usage: usageSchema,
764
+ error: errorSchema,
765
+ lookup: lookupSchema,
766
+ cancel: cancelSchema,
767
+ fieldsCatalog: fieldsCatalogSchema,
768
+ fieldsQuote: fieldsQuoteSchema,
769
+ explore: exploreSchema,
770
+ };
771
+ /** Validate own data properties only; never invoke getters supplied by SDK callers. */
772
+ export function matches(schema, value) {
773
+ if (schema.const !== undefined && value !== schema.const)
774
+ return false;
775
+ if (schema.enum && !schema.enum.includes(value))
776
+ return false;
777
+ if (schema.oneOf && schema.oneOf.filter(part => matches(part, value)).length !== 1)
778
+ return false;
779
+ if (schema.type === 'null' && value !== null)
780
+ return false;
781
+ if (schema.type === 'string') {
782
+ if (typeof value !== 'string')
783
+ return false;
784
+ if ((schema.minLength !== undefined && [...value].length < schema.minLength) ||
785
+ (schema.maxLength !== undefined && [...value].length > schema.maxLength) ||
786
+ (schema.pattern && !new RegExp(schema.pattern).test(value)))
787
+ return false;
788
+ if (schema.format === 'uri') {
789
+ try {
790
+ new URL(value);
791
+ }
792
+ catch {
793
+ return false;
794
+ }
795
+ }
796
+ }
797
+ else if (schema.type === 'boolean' && typeof value !== 'boolean')
798
+ return false;
799
+ else if (schema.type === 'integer' || schema.type === 'number') {
800
+ if (typeof value !== 'number' ||
801
+ !Number.isFinite(value) ||
802
+ (schema.type === 'integer' && !Number.isSafeInteger(value)) ||
803
+ (schema.minimum !== undefined && value < schema.minimum))
804
+ return false;
805
+ }
806
+ else if (schema.type === 'array') {
807
+ if (!Array.isArray(value) ||
808
+ (schema.minItems !== undefined && value.length < schema.minItems) ||
809
+ (schema.maxItems !== undefined && value.length > schema.maxItems) ||
810
+ (schema.items && !value.every(item => matches(schema.items, item))))
811
+ return false;
812
+ }
813
+ if (schema.type === 'object' || schema.required) {
814
+ if (!value ||
815
+ typeof value !== 'object' ||
816
+ Array.isArray(value) ||
817
+ ![Object.prototype, null].includes(Object.getPrototypeOf(value)))
818
+ return false;
819
+ if (schema.required?.some(key => !Object.hasOwn(value, key)))
820
+ return false;
821
+ for (const key of Reflect.ownKeys(value)) {
822
+ if (typeof key !== 'string')
823
+ return false;
824
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
825
+ if (!descriptor || !('value' in descriptor))
826
+ return false;
827
+ const field = schema.properties && Object.hasOwn(schema.properties, key) ? schema.properties[key] : undefined;
828
+ if ((field && !matches(field, descriptor.value)) || (!field && schema.additionalProperties === false))
829
+ return false;
830
+ if (!field &&
831
+ schema.additionalProperties &&
832
+ typeof schema.additionalProperties === 'object' &&
833
+ !matches(schema.additionalProperties, descriptor.value))
834
+ return false;
835
+ }
836
+ }
837
+ return true;
838
+ }
839
+ export function assertSchema(schema, value) {
840
+ if (!matches(schema, value))
841
+ throw new Error('PROTOCOL_CONTRACT_INVALID');
842
+ return value;
843
+ }