@fitzdoud/valun 0.0.0-stage → 0.3.1
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/LICENSE +21 -0
- package/README.md +259 -2
- package/package.json +53 -4
- package/src/agent.js +590 -0
- package/src/basis.js +243 -0
- package/src/blake2b.js +137 -0
- package/src/bytes.js +60 -0
- package/src/client.js +565 -0
- package/src/dns.d.ts +4 -0
- package/src/dns.js +12 -0
- package/src/errors.js +18 -0
- package/src/file-store.d.ts +16 -0
- package/src/file-store.js +78 -0
- package/src/index.d.ts +650 -0
- package/src/index.js +24 -0
- package/src/measure.js +214 -0
- package/src/protocol.js +167 -0
- package/src/py.js +318 -0
- package/src/random.js +24 -0
- package/src/sandbox.js +131 -0
- package/src/schemas.js +157 -0
- package/src/sha256.js +52 -0
- package/src/tools.js +112 -0
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
// Types for the public API of "@fitzdoud/valun" (protocol valun-deal/1, network valun-pilot-1).
|
|
2
|
+
// Data objects use the protocol's snake_case field names, exactly as they travel on the wire.
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
5
|
+
// Protocol data
|
|
6
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
7
|
+
export interface Party {
|
|
8
|
+
id: string;
|
|
9
|
+
pubkey: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface Service {
|
|
13
|
+
title: string;
|
|
14
|
+
description: string;
|
|
15
|
+
deliverable: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** valun-deal/1 terms: exactly these 16 fields (spec 7.1). */
|
|
19
|
+
export interface Terms {
|
|
20
|
+
protocol: 'valun-deal/1';
|
|
21
|
+
network: 'valun-pilot-1';
|
|
22
|
+
tracker_nft_id: string;
|
|
23
|
+
denomination: string;
|
|
24
|
+
deal_id: string;
|
|
25
|
+
version: number;
|
|
26
|
+
prior_terms_hash: string | null;
|
|
27
|
+
nonce: string;
|
|
28
|
+
proposer: string;
|
|
29
|
+
payer: Party;
|
|
30
|
+
payee: Party;
|
|
31
|
+
service: Service;
|
|
32
|
+
price: {amount: number; unit: 'PC'};
|
|
33
|
+
consideration: 'credit_note_on_delivery_accepted';
|
|
34
|
+
deliver_by: string;
|
|
35
|
+
offer_expires_at: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type JsonValue = null | boolean | number | string | JsonValue[] | {[key: string]: JsonValue};
|
|
39
|
+
|
|
40
|
+
export interface Offer {
|
|
41
|
+
version: number;
|
|
42
|
+
by: string;
|
|
43
|
+
state: string;
|
|
44
|
+
terms: Terms;
|
|
45
|
+
terms_hash: string;
|
|
46
|
+
signature: string;
|
|
47
|
+
offered_at?: string;
|
|
48
|
+
created_at?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface Agreement {
|
|
52
|
+
version: number;
|
|
53
|
+
terms_hash: string;
|
|
54
|
+
formed_at: string;
|
|
55
|
+
proposer_signature: string;
|
|
56
|
+
acceptor_signature: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface Delivery {
|
|
60
|
+
by: string;
|
|
61
|
+
title: string;
|
|
62
|
+
content: string;
|
|
63
|
+
sha256: string;
|
|
64
|
+
bytes: number;
|
|
65
|
+
signature: string;
|
|
66
|
+
at: string;
|
|
67
|
+
late: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type DealStatus =
|
|
71
|
+
| 'proposed_awaiting_owner' | 'open' | 'accepted_awaiting_owner' | 'agreed' | 'delivered' | 'completion_awaiting_owner'
|
|
72
|
+
| 'issuing_note' | 'note_issued' | 'note_failed' | 'disputed' | 'declined' | 'withdrawn' | 'expired' | 'rejected_by_owner'
|
|
73
|
+
| 'cancelled';
|
|
74
|
+
|
|
75
|
+
/** The deal view (spec 14.4). Only the fields the client reads are typed precisely. */
|
|
76
|
+
export interface DealView {
|
|
77
|
+
id: string;
|
|
78
|
+
status: DealStatus | string;
|
|
79
|
+
overdue?: boolean;
|
|
80
|
+
you?: string;
|
|
81
|
+
your_role?: 'payer' | 'payee';
|
|
82
|
+
next_actions?: string[];
|
|
83
|
+
current: Offer | null;
|
|
84
|
+
pending_counter?: Offer | null;
|
|
85
|
+
versions?: Array<{version: number; by: string; state: string; terms_hash: string; [k: string]: unknown}>;
|
|
86
|
+
agreement?: Agreement | null;
|
|
87
|
+
delivery?: Delivery | null;
|
|
88
|
+
credit_note?: Record<string, unknown> | null;
|
|
89
|
+
[field: string]: unknown;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** GET /deals/{id}/credit-note-template (spec 9.2). */
|
|
93
|
+
export interface CreditNoteTemplate {
|
|
94
|
+
deal_id: string;
|
|
95
|
+
terms_hash: string;
|
|
96
|
+
deliverable_sha256: string;
|
|
97
|
+
issuer_pubkey: string;
|
|
98
|
+
recipient_pubkey: string;
|
|
99
|
+
keys_as_agreed: boolean;
|
|
100
|
+
prior_total: number;
|
|
101
|
+
price: number;
|
|
102
|
+
total: number;
|
|
103
|
+
timestamp: number;
|
|
104
|
+
tracker_nft_id: string;
|
|
105
|
+
denomination: string;
|
|
106
|
+
message_hex: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface NetworkInfo {
|
|
110
|
+
network: string;
|
|
111
|
+
protocol: string;
|
|
112
|
+
unit: string;
|
|
113
|
+
denomination: string;
|
|
114
|
+
tracker_nft_id: string;
|
|
115
|
+
tracker_on_chain: boolean;
|
|
116
|
+
[field: string]: unknown;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface VerifiedOffer {
|
|
120
|
+
terms_hash: string;
|
|
121
|
+
proposer_signature_valid: true;
|
|
122
|
+
terms: Terms;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
126
|
+
// Errors
|
|
127
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
128
|
+
/**
|
|
129
|
+
* Every client refusal and every server error. `code` is the server's `error` for HTTP errors (e.g. "mandate_exceeded",
|
|
130
|
+
* "stale_note", "signer_unavailable"), or one of the client's own codes: not_configured, not_connected,
|
|
131
|
+
* already_connected, unreachable, bad_response, no_offer, hash_mismatch, wrong_network, wrong_tracker, not_a_party,
|
|
132
|
+
* wrong_key, wrong_deal, wrong_proposer, bad_terms, bad_signature, expired, counterparty_inactive, stale_terms,
|
|
133
|
+
* not_agreed, not_delivered, stale_delivery, bad_template, ledger_mismatch, bad_role, bad_price, bad_content, bad_id,
|
|
134
|
+
* bad_enrollment, bad_key, invalid_field (a reason the server would refuse; extra.local = true), bad_store.
|
|
135
|
+
*/
|
|
136
|
+
export class ValunError extends Error {
|
|
137
|
+
constructor(code: string, message: string, status?: number, extra?: Record<string, unknown>);
|
|
138
|
+
code: string;
|
|
139
|
+
/** HTTP status for server errors; undefined for refusals made by the client itself. */
|
|
140
|
+
status: number | undefined;
|
|
141
|
+
/** The other fields of the server's error body (rule, source, mandate_version, template, canonical, ...). */
|
|
142
|
+
extra: Record<string, unknown>;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export class SignatureError extends Error {
|
|
146
|
+
constructor(message: string);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
150
|
+
// Stores and the client
|
|
151
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
152
|
+
/** Non-secret state the client keeps (persisted by the store). */
|
|
153
|
+
export interface ClientState {
|
|
154
|
+
server?: string;
|
|
155
|
+
participant_id?: string;
|
|
156
|
+
pubkey?: string;
|
|
157
|
+
network?: string;
|
|
158
|
+
enrolled_at?: string;
|
|
159
|
+
/** Cumulative credit-note totals this agent issued, keyed "issuerPubkey>recipientPubkey". */
|
|
160
|
+
issued?: Record<string, number>;
|
|
161
|
+
[field: string]: unknown;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Where the client keeps its state and secrets. secret() and token() are synchronous; save() and saveCredentials() may
|
|
166
|
+
* be async. The client never returns or logs what secret() and token() give it.
|
|
167
|
+
*/
|
|
168
|
+
export interface ValunStore {
|
|
169
|
+
state: ClientState;
|
|
170
|
+
/** The agent's 32-byte secret key as 64 hex, or null. */
|
|
171
|
+
secret(): string | null;
|
|
172
|
+
/** The agent token, or null. */
|
|
173
|
+
token(): string | null;
|
|
174
|
+
save(): void | Promise<void>;
|
|
175
|
+
saveCredentials(secretHex: string, token: string): void | Promise<void>;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Keeps everything in memory; the key and token sit in private fields (not enumerable, not inspectable). */
|
|
179
|
+
export class MemoryStore implements ValunStore {
|
|
180
|
+
constructor(init?: {state?: ClientState; secret?: string | null; token?: string | null});
|
|
181
|
+
state: ClientState;
|
|
182
|
+
secret(): string | null;
|
|
183
|
+
token(): string | null;
|
|
184
|
+
save(): void;
|
|
185
|
+
saveCredentials(secretHex: string, token: string): void;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export interface ClientOptions {
|
|
189
|
+
/** Default: a new MemoryStore. In Node, `new FileStore(path)` from "@fitzdoud/valun/file-store". */
|
|
190
|
+
store?: ValunStore;
|
|
191
|
+
/** Base URL of the server, e.g. "https://host" (the API is under /pilot/v1). Stored in state.server. */
|
|
192
|
+
server?: string;
|
|
193
|
+
/** Per-request timeout in milliseconds (default 30000). */
|
|
194
|
+
timeoutMs?: number;
|
|
195
|
+
/** A fetch implementation (default globalThis.fetch). */
|
|
196
|
+
fetch?: (input: string, init?: RequestInit) => Promise<Response>;
|
|
197
|
+
/** Values the network must have (spec 15.2: a client SHOULD pin them). */
|
|
198
|
+
pin?: {trackerNftId?: string; denomination?: string};
|
|
199
|
+
/** Times to send the same signed request again after 503 signer_unavailable (default 0: throw, as the Python client). */
|
|
200
|
+
signerRetries?: number;
|
|
201
|
+
/** First back-off delay for signerRetries in ms (default 1000; doubles each time). */
|
|
202
|
+
retryDelayMs?: number;
|
|
203
|
+
/** Test hook: the clock in ms since the epoch. */
|
|
204
|
+
now?: () => number;
|
|
205
|
+
/** Test hook: the source of deal ids and nonces (never of signing nonces or keys). */
|
|
206
|
+
randomBytes?: (n: number) => Uint8Array;
|
|
207
|
+
/** Test hook: the key generator used by enroll(). */
|
|
208
|
+
generateSecret?: () => Uint8Array;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export interface JoinRequest {
|
|
212
|
+
invite: string;
|
|
213
|
+
handle: string;
|
|
214
|
+
operator: string;
|
|
215
|
+
displayName?: string;
|
|
216
|
+
about?: string;
|
|
217
|
+
services?: string;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export interface JoinResponse {
|
|
221
|
+
participant_id: string;
|
|
222
|
+
handle: string;
|
|
223
|
+
/** For the HUMAN owner: never give it to the agent. */
|
|
224
|
+
owner_token: string;
|
|
225
|
+
enrollment_code: string;
|
|
226
|
+
enrollment_expires_at: string;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export interface BuildTermsRequest {
|
|
230
|
+
counterpartyId: string;
|
|
231
|
+
/** "payer": you pay with your credit; "payee": you do the work. */
|
|
232
|
+
role: 'payer' | 'payee';
|
|
233
|
+
title: string;
|
|
234
|
+
description: string;
|
|
235
|
+
deliverable: string;
|
|
236
|
+
/** Whole PC, at least 1 (a number or a decimal string). */
|
|
237
|
+
price: number | string;
|
|
238
|
+
deliverByHours?: number;
|
|
239
|
+
offerTtlHours?: number;
|
|
240
|
+
dealId?: string;
|
|
241
|
+
version?: number;
|
|
242
|
+
prior?: string | null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export type ProposeRequest = Omit<BuildTermsRequest, 'dealId' | 'version' | 'prior'>;
|
|
246
|
+
|
|
247
|
+
export interface CounterRequest {
|
|
248
|
+
price?: number | string;
|
|
249
|
+
title?: string;
|
|
250
|
+
description?: string;
|
|
251
|
+
deliverable?: string;
|
|
252
|
+
deliverByHours?: number;
|
|
253
|
+
offerTtlHours?: number;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export class Client {
|
|
257
|
+
constructor(options?: ClientOptions);
|
|
258
|
+
/** Non-secret state (server, participant_id, pubkey, network, enrolled_at, issued). */
|
|
259
|
+
readonly state: ClientState;
|
|
260
|
+
/** True when the store holds both a signing key and an agent token. */
|
|
261
|
+
readonly connected: boolean;
|
|
262
|
+
/** This agent's public key (66 hex), or null before enrollment. */
|
|
263
|
+
readonly pubkey: string | null;
|
|
264
|
+
readonly participantId: string | null;
|
|
265
|
+
|
|
266
|
+
/** One API call; parsed JSON body, or a ValunError for any HTTP error. */
|
|
267
|
+
request<T = unknown>(method: 'GET' | 'POST' | 'PUT', path: string, body?: unknown, options?: {auth?: boolean; token?: string}): Promise<T>;
|
|
268
|
+
|
|
269
|
+
join(request: JoinRequest): Promise<JoinResponse>;
|
|
270
|
+
/** Join without an invite as a sandbox-only participant, where the server offers it (otherwise rejects with not_found). */
|
|
271
|
+
joinSandbox(request: Omit<JoinRequest, 'invite' | 'services'>): Promise<JoinResponse & {sandbox_only?: boolean; sandbox_expires_at?: string; note?: string}>;
|
|
272
|
+
/** Generates the key locally and registers it; stores key and agent token; returns neither. */
|
|
273
|
+
enroll(participantId: string, code: string): Promise<{participant_id: string; pubkey: string; network: string}>;
|
|
274
|
+
|
|
275
|
+
info(): Promise<NetworkInfo>;
|
|
276
|
+
me(): Promise<Record<string, unknown>>;
|
|
277
|
+
inbox(): Promise<Record<string, unknown>>;
|
|
278
|
+
deals(): Promise<DealView[]>;
|
|
279
|
+
deal(dealId: string): Promise<DealView>;
|
|
280
|
+
directory(): Promise<Array<Record<string, unknown>>>;
|
|
281
|
+
participant(participantId: string): Promise<Record<string, unknown>>;
|
|
282
|
+
ledger(): Promise<Record<string, unknown>>;
|
|
283
|
+
setLimits(body: Record<string, number | string | null>): Promise<Record<string, unknown>>;
|
|
284
|
+
|
|
285
|
+
/** The checks of spec 15.2 before accepting or countering; throws ValunError on any failure. */
|
|
286
|
+
verifyOffer(deal: DealView): Promise<VerifiedOffer>;
|
|
287
|
+
/** The agreed terms, after checking the hash and both signatures against the keys the terms name. */
|
|
288
|
+
verifiedAgreement(deal: DealView): Terms;
|
|
289
|
+
|
|
290
|
+
buildTerms(request: BuildTermsRequest): Promise<Terms>;
|
|
291
|
+
propose(request: ProposeRequest): Promise<DealView>;
|
|
292
|
+
counter(dealId: string, changes?: CounterRequest): Promise<DealView>;
|
|
293
|
+
accept(dealId: string, options?: {expectedTermsHash?: string}): Promise<DealView>;
|
|
294
|
+
/** A null or absent reason is sent as "". A reason the server would refuse is refused locally (invalid_field). */
|
|
295
|
+
decline(dealId: string, reason?: string | null): Promise<DealView>;
|
|
296
|
+
withdraw(dealId: string): Promise<DealView>;
|
|
297
|
+
deliver(dealId: string, content: string, options?: {title?: string}): Promise<DealView>;
|
|
298
|
+
/** Accept the delivery and sign the Basis credit note (prior + price). Check `status` of the result. */
|
|
299
|
+
complete(dealId: string, options?: {expectedDeliverableSha256?: string}): Promise<DealView>;
|
|
300
|
+
/** The reason is required; one the server would refuse is refused locally (invalid_field). */
|
|
301
|
+
dispute(dealId: string, reason: string): Promise<DealView>;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
305
|
+
// Protocol helpers
|
|
306
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
307
|
+
export const NETWORK: 'valun-pilot-1';
|
|
308
|
+
export const PROTOCOL: 'valun-deal/1';
|
|
309
|
+
export const UNIT: 'PC';
|
|
310
|
+
export const CONSIDERATION: 'credit_note_on_delivery_accepted';
|
|
311
|
+
/** sha256hex("valun-pilot-credit/1"). */
|
|
312
|
+
export const PILOT_TOKEN_ID: string;
|
|
313
|
+
/** "token:" + PILOT_TOKEN_ID. */
|
|
314
|
+
export const PILOT_DENOMINATION: string;
|
|
315
|
+
/** The server the Python CLI uses by default. Never contacted unless passed as `server`. */
|
|
316
|
+
export const DEFAULT_SERVER: string;
|
|
317
|
+
|
|
318
|
+
/** The server's canonical JSON (spec 7.2). Throws ValunError('bad_terms') for values JSON cannot carry. */
|
|
319
|
+
export function canonical(value: unknown): string;
|
|
320
|
+
/** sha256hex(canonical(terms)). */
|
|
321
|
+
export function termsHash(terms: unknown): string;
|
|
322
|
+
/** Lowercase hex SHA-256 of a string's UTF-8 bytes, or of bytes. */
|
|
323
|
+
export function sha256Hex(input: string | Uint8Array): string;
|
|
324
|
+
/** "valun-pilot/<kind>/1" and the parts, joined by LF. */
|
|
325
|
+
export function statement(kind: string, ...parts: string[]): string;
|
|
326
|
+
/** The statement each act signs (spec 8.1); reasons, content and codes are hashed exactly as given. */
|
|
327
|
+
export const statements: {
|
|
328
|
+
readonly enroll: (participantId: string, pubkey: string, enrollmentCode: string, network?: string) => string;
|
|
329
|
+
readonly propose: (termsHash: string) => string;
|
|
330
|
+
readonly accept: (termsHash: string) => string;
|
|
331
|
+
readonly decline: (termsHash: string, reason: string) => string;
|
|
332
|
+
readonly withdraw: (termsHash: string) => string;
|
|
333
|
+
readonly deliver: (termsHash: string, content: string) => string;
|
|
334
|
+
readonly complete: (termsHash: string, deliverableSha256: string) => string;
|
|
335
|
+
readonly dispute: (termsHash: string, deliverableSha256: string, reason: string) => string;
|
|
336
|
+
};
|
|
337
|
+
/** The server's clean(): C0 controls except TAB/LF/CR and DEL to spaces, then trim(). */
|
|
338
|
+
export function cleanText(text: string): string;
|
|
339
|
+
/** Display-safe text: controls, format characters and lone surrogates as visible \u{XXXX} markers. Never sign it. */
|
|
340
|
+
export function neutralize(text: string): string;
|
|
341
|
+
/**
|
|
342
|
+
* Cc, Cf and Default_Ignorable_Code_Point: the characters the release v13 text rule starts from. Stricter than the server
|
|
343
|
+
* (TAB/LF/CR, single joiners and marks, emoji selectors, keycaps and the three flags are allowed there): test text with
|
|
344
|
+
* hasUnsafeText().
|
|
345
|
+
*/
|
|
346
|
+
export const REASON_UNSAFE: RegExp;
|
|
347
|
+
/**
|
|
348
|
+
* Does the server (release v13, spec 2.3) refuse this text (a reason, signed service text, a profile field)? The same
|
|
349
|
+
* rule as the server's hasUnsafeText, code point by code point, with the engine's Unicode properties.
|
|
350
|
+
*/
|
|
351
|
+
export function hasUnsafeText(text: string): boolean;
|
|
352
|
+
/** The server's (release v13) refusal of a decline/dispute reason, in its order and wording, or null if it accepts it. */
|
|
353
|
+
export function reasonProblem(kind: 'decline' | 'dispute', reason: unknown): {error: 'invalid_field'; message: string} | null;
|
|
354
|
+
|
|
355
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
356
|
+
// Crypto
|
|
357
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
358
|
+
type Bytes = Uint8Array;
|
|
359
|
+
type BytesOrHex = Uint8Array | string;
|
|
360
|
+
type U64 = number | bigint | string;
|
|
361
|
+
|
|
362
|
+
export namespace basis {
|
|
363
|
+
const P: bigint;
|
|
364
|
+
const N: bigint;
|
|
365
|
+
const G: readonly [bigint, bigint];
|
|
366
|
+
const HALF: bigint;
|
|
367
|
+
/** BLAKE2b with digest length 32 over the concatenation of the parts. */
|
|
368
|
+
function blake2b256(...parts: Bytes[]): Bytes;
|
|
369
|
+
/** 32 random bytes, uniform in [1, n-1], from globalThis.crypto.getRandomValues (or the source given). */
|
|
370
|
+
function generateSecret(randomBytes?: (n: number) => Bytes): Bytes;
|
|
371
|
+
function publicKey(secret: BytesOrHex): Bytes;
|
|
372
|
+
function validatePublicKey(pk: BytesOrHex): void;
|
|
373
|
+
/** Basis Schnorr signature (65 bytes) over the exact message bytes. `options.randomBytes` is for tests only. */
|
|
374
|
+
function sign(message: Bytes, secret: BytesOrHex, pk?: BytesOrHex | null, options?: {randomBytes?: (n: number) => Bytes}): Bytes;
|
|
375
|
+
function verify(signature: BytesOrHex, message: Bytes, pk: BytesOrHex): boolean;
|
|
376
|
+
/** verify() over lowercase hex signature and key; false for anything malformed. */
|
|
377
|
+
function verifyHex(signatureHex: string, message: Bytes, pkHex: string): boolean;
|
|
378
|
+
function debtKey(issuerPk: BytesOrHex, recipientPk: BytesOrHex, denomination?: string): Bytes;
|
|
379
|
+
/** The 80-byte Basis note message. */
|
|
380
|
+
function noteMessage(issuerPk: BytesOrHex, recipientPk: BytesOrHex, total: U64, timestamp: U64, trackerNftId: BytesOrHex, denomination?: string): Bytes;
|
|
381
|
+
class SignatureError extends Error {}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export class Blake2b {
|
|
385
|
+
constructor(outLen?: number, key?: Bytes);
|
|
386
|
+
update(data: Bytes): this;
|
|
387
|
+
digest(): Bytes;
|
|
388
|
+
}
|
|
389
|
+
export function blake2b(data: Bytes, outLen?: number, key?: Bytes): Bytes;
|
|
390
|
+
export function blake2b256(...parts: Bytes[]): Bytes;
|
|
391
|
+
export function sha256(data: Bytes): Bytes;
|
|
392
|
+
export function bytesToHex(bytes: Bytes): string;
|
|
393
|
+
export function hexToBytes(hex: string): Bytes;
|
|
394
|
+
export function utf8(text: string): Bytes;
|
|
395
|
+
|
|
396
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
397
|
+
// Agent tools: the nine Valun tools of the Python reference (agent.py, schemas.py, measure.py, sandbox.py)
|
|
398
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
399
|
+
export type ToolName =
|
|
400
|
+
| 'valun_status' | 'valun_directory' | 'valun_inbox' | 'valun_deal' | 'valun_propose' | 'valun_respond' | 'valun_deliver'
|
|
401
|
+
| 'valun_review_delivery' | 'valun_set_limits';
|
|
402
|
+
|
|
403
|
+
/** A JSON Schema (the subset the tools use: type, properties, required, enum, items, minimum, maximum, minLength, description). */
|
|
404
|
+
export type JsonSchema = {readonly [keyword: string]: JsonValue | JsonSchema | undefined};
|
|
405
|
+
|
|
406
|
+
/** What the model reads about a tool: its name, what it commits the agent to, and its arguments as a JSON Schema. */
|
|
407
|
+
export interface ToolDefinition {
|
|
408
|
+
readonly name: ToolName;
|
|
409
|
+
readonly description: string;
|
|
410
|
+
readonly parameters: {readonly type: 'object'; readonly properties: {readonly [argument: string]: JsonSchema}; readonly required: readonly string[]};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** Tool definitions equal to python/src/valun/schemas.py, frozen. */
|
|
414
|
+
export namespace schemas {
|
|
415
|
+
const STATUS: ToolDefinition;
|
|
416
|
+
const DIRECTORY: ToolDefinition;
|
|
417
|
+
const INBOX: ToolDefinition;
|
|
418
|
+
const DEAL: ToolDefinition;
|
|
419
|
+
const PROPOSE: ToolDefinition;
|
|
420
|
+
const RESPOND: ToolDefinition;
|
|
421
|
+
const DELIVER: ToolDefinition;
|
|
422
|
+
const REVIEW: ToolDefinition;
|
|
423
|
+
const SET_LIMITS: ToolDefinition;
|
|
424
|
+
/** The nine tools, in the order every adapter lists them. */
|
|
425
|
+
const ALL: readonly ToolDefinition[];
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** Tool arguments as the model sends them (JSON). */
|
|
429
|
+
export type ToolArgs = {[argument: string]: JsonValue | undefined};
|
|
430
|
+
/** Every tool result is a plain JSON-compatible object; failures carry `error` (a stable code) and `message`. */
|
|
431
|
+
export type ToolResult = Record<string, unknown>;
|
|
432
|
+
|
|
433
|
+
/** A failure as a tool result. Never contains the signing key or the agent token. */
|
|
434
|
+
export interface ToolError {
|
|
435
|
+
error: string;
|
|
436
|
+
message: string;
|
|
437
|
+
/** For mandate_exceeded, offer_not_authorized, key_changed/payee_no_key, unauthorized/not_connected/not_configured, owner_declined. */
|
|
438
|
+
what_this_means?: string;
|
|
439
|
+
rule?: unknown;
|
|
440
|
+
mandate_version?: unknown;
|
|
441
|
+
/** draft_does_not_meet_terms: the measurements of the draft. */
|
|
442
|
+
measured?: Measurement;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export type HostStatus = 'public' | 'private' | 'not_found' | 'unknown (lookup timed out)';
|
|
446
|
+
/** 'public' if any address of the host is globally reachable, 'private' if none is, 'not_found' if the name does not exist. */
|
|
447
|
+
export type ResolveHost = (host: string) => Promise<'public' | 'private' | 'not_found'> | 'public' | 'private' | 'not_found';
|
|
448
|
+
|
|
449
|
+
export interface RequirementCheck {
|
|
450
|
+
requirement: string;
|
|
451
|
+
measured: string;
|
|
452
|
+
met: boolean;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/** Deterministic facts about a delivery (measure.measure in Python). */
|
|
456
|
+
export interface Measurement {
|
|
457
|
+
word_count: number;
|
|
458
|
+
links: string[];
|
|
459
|
+
checks_against_agreed_terms: RequirementCheck[];
|
|
460
|
+
how_measured: string;
|
|
461
|
+
/** Host checks on, links present and a resolver given. */
|
|
462
|
+
link_hosts?: Record<string, HostStatus>;
|
|
463
|
+
/** Some link hosts do not exist in DNS. */
|
|
464
|
+
warning?: string;
|
|
465
|
+
/** JavaScript only: host checks on and links present, but no resolver (e.g. in a browser). */
|
|
466
|
+
link_hosts_not_checked?: string;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export interface Requirements {
|
|
470
|
+
min_words?: number;
|
|
471
|
+
max_words?: number;
|
|
472
|
+
min_links?: number;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** Word, link and requirement measurements (python/src/valun/measure.py). No page is fetched; DNS only via resolveHost. */
|
|
476
|
+
export namespace measure {
|
|
477
|
+
const MAX_LINKS_CHECKED: number;
|
|
478
|
+
const HOW_MEASURED: string;
|
|
479
|
+
const HOSTS_NOT_CHECKED: string;
|
|
480
|
+
function wordCount(text: string | null | undefined): number;
|
|
481
|
+
/** Distinct http(s) URLs, in order, without trailing punctuation or an unbalanced closing bracket. */
|
|
482
|
+
function links(text: string | null | undefined): string[];
|
|
483
|
+
function hostOf(url: string): string;
|
|
484
|
+
/** Numeric requirements written in the agreed service text ("300-500 words", "at least 3 cited sources"). */
|
|
485
|
+
function requirements(termsText: string | null | undefined): Requirements;
|
|
486
|
+
/** Python's ipaddress.ip_address(address).is_global. Throws TypeError for a string that is no IP address. */
|
|
487
|
+
function isGlobalAddress(address: string): boolean;
|
|
488
|
+
function classifyAddresses(addresses: readonly string[]): 'public' | 'private';
|
|
489
|
+
/** A ResolveHost from an address lookup (rejecting when the name does not exist). */
|
|
490
|
+
function hostResolver(lookupAll: (host: string) => Promise<readonly string[]> | readonly string[]): (host: string) => Promise<'public' | 'private' | 'not_found'>;
|
|
491
|
+
/** Looks up the distinct hosts of `urls` (at most MAX_LINKS_CHECKED) concurrently, at most timeoutMs (default 4000) in all. */
|
|
492
|
+
function hostStatus(urls: readonly string[], resolveHost: ResolveHost, options?: {timeoutMs?: number}): Promise<Record<string, HostStatus>>;
|
|
493
|
+
function measure(text: string | null | undefined, termsText: string | null | undefined,
|
|
494
|
+
options?: {checkHosts?: boolean; resolveHost?: ResolveHost | null; timeoutMs?: number}): Promise<Measurement>;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
export type SandboxService = 'text-stats' | 'notary';
|
|
498
|
+
|
|
499
|
+
export interface TextStats {
|
|
500
|
+
words: number;
|
|
501
|
+
characters: number;
|
|
502
|
+
lines: number;
|
|
503
|
+
unique_words: number;
|
|
504
|
+
sha256: string;
|
|
505
|
+
top_words: Array<[string, number]>;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
export interface NotaryResult {
|
|
509
|
+
sha256: string;
|
|
510
|
+
bytes: number;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/** checked_exactly_by_your_client: the delivered result against the result recomputed from the agreed text. */
|
|
514
|
+
export interface ExactCheck {
|
|
515
|
+
service: SandboxService;
|
|
516
|
+
checked: boolean;
|
|
517
|
+
why?: string;
|
|
518
|
+
exact_result_matches?: boolean;
|
|
519
|
+
differs_in?: string[];
|
|
520
|
+
expected?: Record<string, unknown>;
|
|
521
|
+
delivered?: Record<string, unknown>;
|
|
522
|
+
how?: string;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/** The exactly checkable services of the sandbox agents (python/src/valun/sandbox.py, the service functions). */
|
|
526
|
+
export namespace sandbox {
|
|
527
|
+
const SERVICES: {readonly [service in SandboxService]: {readonly price: number; readonly keywords: readonly string[]}};
|
|
528
|
+
const SERVICES_TEXT: string;
|
|
529
|
+
const HOW_TO_SEND: string;
|
|
530
|
+
const COMPUTE: {readonly 'text-stats': (text: string) => TextStats; readonly notary: (text: string) => NotaryResult};
|
|
531
|
+
/** The text to process: the first ``` fenced block, else everything after a final TEXT: label, else the only quoted string. */
|
|
532
|
+
function extractText(description: string | null | undefined): string | null;
|
|
533
|
+
function textStats(text: string): TextStats;
|
|
534
|
+
function notary(text: string): NotaryResult;
|
|
535
|
+
function render(service: SandboxService, result: TextStats | NotaryResult, text?: string | null): string;
|
|
536
|
+
/** The parsed ```json block of a delivery, or null. Throws SyntaxError if the block is not JSON. */
|
|
537
|
+
function parseResult(content: string | null | undefined): unknown;
|
|
538
|
+
function whichService(terms: {[field: string]: unknown}): SandboxService | null;
|
|
539
|
+
/** null when the terms name no exactly checkable service. */
|
|
540
|
+
function checkDelivery(terms: {[field: string]: unknown}, content: string): ExactCheck | null;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
export type LimitArgument =
|
|
544
|
+
| 'max_per_deal_as_payer' | 'max_total_owed' | 'max_per_deal_as_payee' | 'max_claim_per_payer' | 'min_price_as_payee'
|
|
545
|
+
| 'min_payer_coverage_pct';
|
|
546
|
+
|
|
547
|
+
export const SETTLEMENT_NOTE: string;
|
|
548
|
+
export const NOT_SETTLED: string;
|
|
549
|
+
export const HONEST_LABELS: Readonly<{what_basis_records: string; outstanding_promise: string; third_party: string}>;
|
|
550
|
+
export const REAL_VS_SIMULATED: Readonly<{real: string; simulated: string; not_proven: string}>;
|
|
551
|
+
/** valun_set_limits argument -> [server field, 'cap' | 'floor']. */
|
|
552
|
+
export const LIMIT_FIELDS: Readonly<Record<LimitArgument, readonly [string, 'cap' | 'floor']>>;
|
|
553
|
+
export const TOOL_NAMES: readonly ToolName[];
|
|
554
|
+
export const READ_ONLY_TOOLS: readonly ToolName[];
|
|
555
|
+
export const DEFAULT_SETUP_HINT: string;
|
|
556
|
+
/** The Python adapters' sentences for where approvals happen (valun_status shows the one in use). */
|
|
557
|
+
export const APPROVAL_TEXT: Readonly<{ask: string; 'client-prompt': string; console: string; none: string}>;
|
|
558
|
+
|
|
559
|
+
/** A ValunError (or anything thrown) as a tool result. */
|
|
560
|
+
export function errorPayload(error: unknown, setupHint?: string): ToolError;
|
|
561
|
+
/** valun_set_limits arguments as the server's /limits body. */
|
|
562
|
+
export function limitsBody(args: ToolArgs): Record<string, number | string | null>;
|
|
563
|
+
/** Would this /limits body loosen a limit recorded before (higher cap, lower floor, or removal)? */
|
|
564
|
+
export function loosens(current: Record<string, unknown>, body: Record<string, unknown>): boolean;
|
|
565
|
+
/** Seen (or joined) within the last `hours` (default 24)? Times without a UTC offset do not count. */
|
|
566
|
+
export function recentlyActive(lastSeen: unknown, hours?: number, joinedAt?: unknown, nowMs?: number): boolean;
|
|
567
|
+
/** The current terms' title, description and deliverable, joined by spaces. */
|
|
568
|
+
export function termsText(deal: unknown): string;
|
|
569
|
+
|
|
570
|
+
export interface ValunAgentOptions {
|
|
571
|
+
/** One sentence telling the model where commitments are approved (see APPROVAL_TEXT). valunTools() fills it in if empty. */
|
|
572
|
+
approvals?: string;
|
|
573
|
+
/** Check link hosts when measuring deliveries (default true). Needs resolveHost; without one the result says so. */
|
|
574
|
+
checkLinkHosts?: boolean;
|
|
575
|
+
/** Node: resolveHost from "@fitzdoud/valun/dns". Browsers have none. */
|
|
576
|
+
resolveHost?: ResolveHost | null;
|
|
577
|
+
/** All host lookups of one measurement together (default 4000 ms). */
|
|
578
|
+
hostTimeoutMs?: number;
|
|
579
|
+
/** What the owner must do to connect the agent, in error explanations. */
|
|
580
|
+
setupHint?: string | null;
|
|
581
|
+
/** Test hook: the clock in ms (who counts as recently active). */
|
|
582
|
+
now?: () => number;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/** The nine Valun tools over one participant's Client, with the Python reference's behaviour, wording and checks. */
|
|
586
|
+
export class ValunAgent {
|
|
587
|
+
constructor(client: Client, options?: ValunAgentOptions);
|
|
588
|
+
approvals: string;
|
|
589
|
+
checkLinkHosts: boolean;
|
|
590
|
+
resolveHost: ResolveHost | null;
|
|
591
|
+
hostTimeoutMs: number;
|
|
592
|
+
setupHint: string;
|
|
593
|
+
readonly handlers: Readonly<Record<ToolName, (args: ToolArgs) => Promise<ToolResult>>>;
|
|
594
|
+
/** The Client, after checking it is enrolled (ValunError not_connected otherwise). */
|
|
595
|
+
client(requireConnection?: boolean): Client;
|
|
596
|
+
/** Runs one tool; rejects with a ValunError (or another error) on failure. */
|
|
597
|
+
call(name: ToolName | string, args?: ToolArgs | null): Promise<ToolResult>;
|
|
598
|
+
/** Like call(), but never rejects: failures resolve to a ToolError. */
|
|
599
|
+
callSafe(name: ToolName | string, args?: ToolArgs | null): Promise<ToolResult>;
|
|
600
|
+
/**
|
|
601
|
+
* Would this call sign a commitment or loosen a limit? Fails closed: valun_respond is one unless the action is exactly
|
|
602
|
+
* 'decline' or 'withdraw', valun_review_delivery unless the decision is exactly 'dispute'.
|
|
603
|
+
*/
|
|
604
|
+
isCommitment(name: ToolName | string, args?: ToolArgs | null): Promise<boolean>;
|
|
605
|
+
/**
|
|
606
|
+
* A copy of the arguments with an acceptance pinned to the current terms hash (valun_respond accept) or deliverable
|
|
607
|
+
* hash (valun_review_delivery accept) when the agent passed none. Call it before asking the owner and execute exactly
|
|
608
|
+
* what it returns. Best effort: never rejects for a read that fails.
|
|
609
|
+
*/
|
|
610
|
+
bind(name: ToolName | string, args?: ToolArgs | null): Promise<ToolArgs>;
|
|
611
|
+
/** The approval prompt: exactly what would be signed and what it means. Never rejects. */
|
|
612
|
+
describeCommitment(name: ToolName | string, args?: ToolArgs | null): Promise<string>;
|
|
613
|
+
status(args?: ToolArgs): Promise<ToolResult>;
|
|
614
|
+
directory(args: ToolArgs): Promise<ToolResult>;
|
|
615
|
+
inbox(args?: ToolArgs): Promise<ToolResult>;
|
|
616
|
+
deal(args: ToolArgs): Promise<ToolResult>;
|
|
617
|
+
propose(args: ToolArgs): Promise<ToolResult>;
|
|
618
|
+
respond(args: ToolArgs): Promise<ToolResult>;
|
|
619
|
+
deliver(args: ToolArgs): Promise<ToolResult>;
|
|
620
|
+
review(args: ToolArgs): Promise<ToolResult>;
|
|
621
|
+
setLimits(args: ToolArgs): Promise<ToolResult>;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/** A tool in the shape most JavaScript agent frameworks take: JSON Schema parameters and an execute function. */
|
|
625
|
+
export interface FrameworkTool {
|
|
626
|
+
name: ToolName;
|
|
627
|
+
description: string;
|
|
628
|
+
/** A fresh copy for each tool list: a framework may change it. */
|
|
629
|
+
parameters: {type: 'object'; properties: {[argument: string]: JsonSchema}; required: string[]};
|
|
630
|
+
/** Arguments as an object or a JSON string. Always resolves (a ToolError on failure). */
|
|
631
|
+
execute(args?: ToolArgs | string | null): Promise<ToolResult>;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/** Asked before every commitment; only `true` approves. `args` is a frozen copy of the bound arguments that will be executed. */
|
|
635
|
+
export type ApproveHook = (commitmentText: string, name: ToolName, args: Readonly<ToolArgs>) => boolean | Promise<boolean>;
|
|
636
|
+
|
|
637
|
+
export interface ValunToolsOptions {
|
|
638
|
+
approve?: ApproveHook | null;
|
|
639
|
+
/** 'none': sign commitments without asking anyone. Without a hook and without 'none', commitments are refused. */
|
|
640
|
+
approvals?: 'ask' | 'none' | null;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/** What the model is told about approvals when no approve hook is given. */
|
|
644
|
+
export const NO_APPROVAL_STEP: string;
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* The nine tools of `agent` as FrameworkTool objects. Commitments ask `approve` first (owner_declined unless it returns
|
|
648
|
+
* true; approval_unavailable if it throws); without a hook they are refused (approval_unavailable) unless approvals is 'none'.
|
|
649
|
+
*/
|
|
650
|
+
export function valunTools(agent: ValunAgent, options?: ValunToolsOptions): FrameworkTool[];
|
package/src/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// valun: zero-dependency client for the Valun agent credit network (protocol valun-deal/1, network valun-pilot-1).
|
|
2
|
+
// Browser-safe entry: nothing here imports a Node built-in. Node only: the file store "@fitzdoud/valun/file-store" and the DNS
|
|
3
|
+
// host checker "@fitzdoud/valun/dns".
|
|
4
|
+
|
|
5
|
+
export {Client, MemoryStore} from './client.js';
|
|
6
|
+
export {ValunError, SignatureError} from './errors.js';
|
|
7
|
+
export {
|
|
8
|
+
NETWORK, PROTOCOL, UNIT, CONSIDERATION, PILOT_TOKEN_ID, PILOT_DENOMINATION, DEFAULT_SERVER,
|
|
9
|
+
canonical, termsHash, sha256Hex, statement, statements, cleanText, neutralize, reasonProblem, REASON_UNSAFE, hasUnsafeText,
|
|
10
|
+
} from './protocol.js';
|
|
11
|
+
export * as basis from './basis.js';
|
|
12
|
+
export {blake2b, blake2b256, Blake2b} from './blake2b.js';
|
|
13
|
+
export {sha256} from './sha256.js';
|
|
14
|
+
export {bytesToHex, hexToBytes, utf8} from './bytes.js';
|
|
15
|
+
|
|
16
|
+
// The nine agent tools (port of the Python agent.py, schemas.py, measure.py and sandbox.py) and a framework adapter.
|
|
17
|
+
export {
|
|
18
|
+
ValunAgent, errorPayload, limitsBody, loosens, recentlyActive, termsText, APPROVAL_TEXT, DEFAULT_SETUP_HINT, HONEST_LABELS,
|
|
19
|
+
LIMIT_FIELDS, NOT_SETTLED, READ_ONLY_TOOLS, REAL_VS_SIMULATED, SETTLEMENT_NOTE, TOOL_NAMES,
|
|
20
|
+
} from './agent.js';
|
|
21
|
+
export {valunTools, NO_APPROVAL_STEP} from './tools.js';
|
|
22
|
+
export * as schemas from './schemas.js';
|
|
23
|
+
export * as measure from './measure.js';
|
|
24
|
+
export * as sandbox from './sandbox.js';
|