@routedock/nulth-sdk 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 RouteDock
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,17 @@
1
+ # @routedock/nulth-sdk — Stub / Proof-of-Concept
2
+
3
+ **Status: STUB — not ready for production.**
4
+
5
+ This package provides an **insecure mock prover** (`insecureMockProof`) that computes a
6
+ SHA-512 digest as a placeholder. No real zero-knowledge proof is generated.
7
+
8
+ - The mock prover is only safe for **testnet** development.
9
+ - **`NulthClient` construction throws unless `network` is `'testnet'` and the prover
10
+ is `'mock'`.** A missing or misspelled network (for example `'pubnet'`) and any
11
+ other prover backend are rejected, because this package does not currently
12
+ implement a production prover backend.
13
+ - The `mock` backend is deliberately not a cryptographic proof and must never be
14
+ used for custody or policy enforcement.
15
+
16
+ See [Issue #143](https://github.com/winsznx/routedock/issues/143) for context.
17
+
package/dist/index.cjs ADDED
@@ -0,0 +1,357 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ DEFAULT_PROVER: () => DEFAULT_PROVER,
24
+ NulthClient: () => NulthClient,
25
+ NulthPolicyError: () => NulthPolicyError,
26
+ USDC_DECIMALS: () => USDC_DECIMALS,
27
+ assertAuthEntryMatchesContext: () => assertAuthEntryMatchesContext,
28
+ authDigestFromEntry: () => authDigestFromEntry,
29
+ commitAllowlist: () => commitAllowlist,
30
+ commitDailyCap: () => commitDailyCap,
31
+ createNulthSigner: () => createNulthSigner,
32
+ createPolicyState: () => createPolicyState,
33
+ dayBucket: () => dayBucket,
34
+ decodeAuthSignature: () => decodeAuthSignature,
35
+ encodeAuthSignature: () => encodeAuthSignature,
36
+ hashPayee: () => hashPayee,
37
+ insecureMockProof: () => insecureMockProof,
38
+ paymentContextFromManifest: () => paymentContextFromManifest,
39
+ setNulthPaymentContext: () => setNulthPaymentContext,
40
+ sha256Hex: () => sha256Hex,
41
+ usdcToStroops: () => usdcToStroops
42
+ });
43
+ module.exports = __toCommonJS(index_exports);
44
+
45
+ // src/types.ts
46
+ var NulthPolicyError = class extends Error {
47
+ code;
48
+ constructor(code, message) {
49
+ super(message);
50
+ this.name = "NulthPolicyError";
51
+ this.code = code;
52
+ }
53
+ };
54
+
55
+ // src/proof.ts
56
+ var import_node_crypto = require("crypto");
57
+ var import_stellar_sdk = require("@stellar/stellar-sdk");
58
+
59
+ // src/usdc.ts
60
+ var USDC_DECIMALS = 7;
61
+ var USDC_SCALE = 10n ** BigInt(USDC_DECIMALS);
62
+ function usdcToStroops(amount) {
63
+ const match = /^(\d+)(?:\.(\d+))?$/.exec(amount.trim());
64
+ if (!match) {
65
+ throw new RangeError(`Invalid USDC amount: "${amount}"`);
66
+ }
67
+ const whole = match[1];
68
+ const frac = match[2] ?? "";
69
+ if (frac.length > USDC_DECIMALS) {
70
+ throw new RangeError(
71
+ `USDC amount "${amount}" exceeds ${USDC_DECIMALS} decimals of precision`
72
+ );
73
+ }
74
+ const fracUnits = BigInt(frac.padEnd(USDC_DECIMALS, "0"));
75
+ const units = BigInt(whole) * USDC_SCALE + fracUnits;
76
+ if (units > BigInt(Number.MAX_SAFE_INTEGER)) {
77
+ throw new RangeError(`USDC amount "${amount}" is too large to represent exactly`);
78
+ }
79
+ return units;
80
+ }
81
+
82
+ // src/proof.ts
83
+ function sha256Hex(data) {
84
+ const buf = typeof data === "string" ? Buffer.from(data, "utf8") : data;
85
+ return (0, import_node_crypto.createHash)("sha256").update(buf).digest("hex");
86
+ }
87
+ function commitDailyCap(dailyCapStroops, witnessSecret) {
88
+ return sha256Hex(`${witnessSecret}:cap:${dailyCapStroops.toString()}`);
89
+ }
90
+ function commitAllowlist(payees, witnessSecret) {
91
+ const sorted = [...payees].sort().join(",");
92
+ return sha256Hex(`${witnessSecret}:allowlist:${sorted}`);
93
+ }
94
+ function hashPayee(payee) {
95
+ return sha256Hex(`payee:${payee}`);
96
+ }
97
+ function dayBucket(ledgerSequence) {
98
+ return Math.floor(ledgerSequence / 17280);
99
+ }
100
+ function authDigestFromEntry(authEntryBase64) {
101
+ const entryHash = (0, import_stellar_sdk.hash)(Buffer.from(authEntryBase64, "base64"));
102
+ return sha256Hex(entryHash);
103
+ }
104
+ function assertAuthEntryMatchesContext(authEntry, from, ctx) {
105
+ let preimage;
106
+ try {
107
+ preimage = import_stellar_sdk.xdr.HashIdPreimage.fromXDR(authEntry, "base64");
108
+ } catch {
109
+ throw new NulthPolicyError("auth_entry_mismatch", "authEntry does not decode as a HashIdPreimage");
110
+ }
111
+ if (preimage.switch() !== import_stellar_sdk.xdr.EnvelopeType.envelopeTypeSorobanAuthorization()) {
112
+ throw new NulthPolicyError(
113
+ "auth_entry_mismatch",
114
+ "authEntry is not a Soroban authorization preimage"
115
+ );
116
+ }
117
+ const inv = preimage.sorobanAuthorization().invocation();
118
+ if (inv.subInvocations().length !== 0) {
119
+ throw new NulthPolicyError("auth_entry_mismatch", "authEntry has unexpected sub-invocations");
120
+ }
121
+ if (inv.function().switch() !== import_stellar_sdk.xdr.SorobanAuthorizedFunctionType.sorobanAuthorizedFunctionTypeContractFn()) {
122
+ throw new NulthPolicyError("auth_entry_mismatch", "authEntry does not invoke a contract function");
123
+ }
124
+ const fn = inv.function().contractFn();
125
+ const contractAddress = import_stellar_sdk.Address.fromScAddress(fn.contractAddress()).toString();
126
+ if (contractAddress !== ctx.assetContract) {
127
+ throw new NulthPolicyError(
128
+ "auth_entry_mismatch",
129
+ `authEntry contract ${contractAddress} does not match assetContract ${ctx.assetContract}`
130
+ );
131
+ }
132
+ const functionName = fn.functionName().toString();
133
+ if (functionName !== "transfer") {
134
+ throw new NulthPolicyError(
135
+ "auth_entry_mismatch",
136
+ `authEntry function ${functionName} does not match expected transfer`
137
+ );
138
+ }
139
+ const args = fn.args();
140
+ if (args.length !== 3) {
141
+ throw new NulthPolicyError("auth_entry_mismatch", `authEntry transfer has ${args.length} args, expected 3`);
142
+ }
143
+ const entryFrom = (0, import_stellar_sdk.scValToNative)(args[0]);
144
+ if (entryFrom !== from) {
145
+ throw new NulthPolicyError(
146
+ "auth_entry_mismatch",
147
+ `authEntry from ${entryFrom} does not match nulthAccount ${from}`
148
+ );
149
+ }
150
+ const entryTo = (0, import_stellar_sdk.scValToNative)(args[1]);
151
+ if (entryTo !== ctx.payee) {
152
+ throw new NulthPolicyError(
153
+ "auth_entry_mismatch",
154
+ `authEntry to ${entryTo} does not match paymentContext.payee ${ctx.payee}`
155
+ );
156
+ }
157
+ const entryAmount = (0, import_stellar_sdk.scValToNative)(args[2]);
158
+ if (entryAmount !== ctx.amountStroops) {
159
+ throw new NulthPolicyError(
160
+ "auth_entry_mismatch",
161
+ `authEntry amount ${entryAmount} does not match paymentContext.amountStroops ${ctx.amountStroops}`
162
+ );
163
+ }
164
+ }
165
+ var DEFAULT_PROVER = "mock";
166
+ function insecureMockProof(preimage) {
167
+ return (0, import_node_crypto.createHash)("sha512").update(`nulth:${preimage}`).digest("base64");
168
+ }
169
+ function encodeAuthSignature(proof) {
170
+ const payload = {
171
+ nulth: "zk-v1",
172
+ proof
173
+ };
174
+ return Buffer.from(JSON.stringify(payload), "utf8").toString("base64");
175
+ }
176
+ function decodeAuthSignature(encoded) {
177
+ const raw = JSON.parse(Buffer.from(encoded, "base64").toString("utf8"));
178
+ if (typeof raw !== "object" || raw === null || raw.nulth !== "zk-v1") {
179
+ throw new Error("Invalid Nulth ZK auth signature");
180
+ }
181
+ return raw;
182
+ }
183
+
184
+ // src/NulthClient.ts
185
+ var NulthClient = class {
186
+ constructor(config) {
187
+ this.config = config;
188
+ this.policy = { ...config.policy };
189
+ this.prover = config.prover ?? DEFAULT_PROVER;
190
+ if (this.prover !== "mock") {
191
+ throw new Error(
192
+ `NulthClient: unknown prover backend ${String(config.prover)}; only 'mock' is implemented`
193
+ );
194
+ }
195
+ if (config.network !== "testnet") {
196
+ throw new Error(
197
+ `NulthClient cannot use the insecure mock prover on mainnet; a production prover is required (network must be 'testnet', got ${String(config.network)})`
198
+ );
199
+ }
200
+ console.warn("NulthClient is using the insecure mock prover; proofs are not cryptographically sound");
201
+ }
202
+ config;
203
+ policy;
204
+ prover;
205
+ get nulthAccount() {
206
+ return this.config.nulthAccount;
207
+ }
208
+ get allowlistCommitment() {
209
+ return this.policy.allowlistCommitment;
210
+ }
211
+ get capCommitment() {
212
+ return commitDailyCap(this.policy.dailyCapStroops, this.policy.witnessSecret);
213
+ }
214
+ /** Current off-chain daily spend (never published) */
215
+ get dailySpendStroops() {
216
+ return this.policy.dailySpendStroops;
217
+ }
218
+ /** Prover backend selected for this client */
219
+ get proverBackend() {
220
+ return this.prover;
221
+ }
222
+ /**
223
+ * Build a ZK proof that the payment satisfies off-chain policy.
224
+ * Called by RouteDock before attaching the auth signature to the tx.
225
+ */
226
+ buildPaymentAuthProof(context) {
227
+ this.enforcePolicy(context);
228
+ const bucket = dayBucket(context.ledgerSequence);
229
+ if (bucket !== this.policy.dayBucket) {
230
+ this.policy.dayBucket = bucket;
231
+ this.policy.dailySpendStroops = 0n;
232
+ }
233
+ const authDigest = authDigestFromEntry(context.authEntry);
234
+ const capCommitment = this.capCommitment;
235
+ const payeeHash = hashPayee(context.payee);
236
+ const preimage = [
237
+ authDigest,
238
+ this.policy.allowlistCommitment,
239
+ capCommitment,
240
+ payeeHash,
241
+ context.amountStroops.toString(),
242
+ bucket.toString(),
243
+ this.config.nulthAccount
244
+ ].join("|");
245
+ const proof = {
246
+ version: 1,
247
+ proof: insecureMockProof(preimage),
248
+ publicInputs: {
249
+ authDigest,
250
+ allowlistCommitment: this.policy.allowlistCommitment,
251
+ capCommitment,
252
+ payeeHash,
253
+ amountStroops: context.amountStroops.toString(),
254
+ dayBucket: bucket.toString()
255
+ }
256
+ };
257
+ this.policy.dailySpendStroops += context.amountStroops;
258
+ return proof;
259
+ }
260
+ /** Encode proof for SorobanAddressCredentials.signature */
261
+ encodeProof(proof) {
262
+ return encodeAuthSignature(proof);
263
+ }
264
+ enforcePolicy(context) {
265
+ if (this.policy.expiryLedger !== void 0 && context.ledgerSequence > this.policy.expiryLedger) {
266
+ throw new NulthPolicyError("session_expired", "Nulth session expired");
267
+ }
268
+ if (!this.policy.allowedPayees.includes(context.payee)) {
269
+ throw new NulthPolicyError(
270
+ "payee_not_allowed",
271
+ `Payee ${context.payee} not in off-chain allowlist`
272
+ );
273
+ }
274
+ const bucket = dayBucket(context.ledgerSequence);
275
+ const spend = bucket === this.policy.dayBucket ? this.policy.dailySpendStroops : 0n;
276
+ const projected = spend + context.amountStroops;
277
+ if (projected > this.policy.dailyCapStroops) {
278
+ throw new NulthPolicyError("daily_cap_exceeded", "Nulth daily cap exceeded");
279
+ }
280
+ }
281
+ };
282
+
283
+ // src/signer.ts
284
+ function createNulthSigner(config) {
285
+ const client = new NulthClient(config);
286
+ const nulthAccount = config.nulthAccount;
287
+ return {
288
+ address: nulthAccount,
289
+ signAuthEntry: async (authEntry) => {
290
+ const ctx = config.paymentContext;
291
+ if (!ctx) {
292
+ throw new Error(
293
+ "Nulth paymentContext must be set before signing \u2014 call setNulthPaymentContext()"
294
+ );
295
+ }
296
+ assertAuthEntryMatchesContext(authEntry, nulthAccount, ctx);
297
+ const proof = client.buildPaymentAuthProof({
298
+ authEntry,
299
+ ...ctx
300
+ });
301
+ return {
302
+ signedAuthEntry: encodeAuthSignature(proof),
303
+ signerAddress: nulthAccount
304
+ };
305
+ }
306
+ };
307
+ }
308
+ function setNulthPaymentContext(config, context) {
309
+ config.paymentContext = context;
310
+ }
311
+ function paymentContextFromManifest(manifest, mode, ledgerSequence) {
312
+ const pricing = manifest.pricing[mode];
313
+ if (!pricing) {
314
+ throw new Error(`manifest.pricing.${mode} missing`);
315
+ }
316
+ return {
317
+ payee: manifest.payee,
318
+ amountStroops: usdcToStroops(pricing.amount),
319
+ assetContract: manifest.asset_contract,
320
+ ledgerSequence
321
+ };
322
+ }
323
+ function createPolicyState(input) {
324
+ const dailyCapStroops = usdcToStroops(input.dailyCapUsdc);
325
+ const witnessSecret = input.witnessSecret;
326
+ return {
327
+ dailyCapStroops,
328
+ allowlistCommitment: commitAllowlist(input.allowedPayees, witnessSecret),
329
+ allowedPayees: input.allowedPayees,
330
+ dailySpendStroops: 0n,
331
+ dayBucket: dayBucket(input.ledgerSequence ?? 0),
332
+ witnessSecret,
333
+ ...input.expiryLedger !== void 0 ? { expiryLedger: input.expiryLedger } : {}
334
+ };
335
+ }
336
+ // Annotate the CommonJS export names for ESM import in node:
337
+ 0 && (module.exports = {
338
+ DEFAULT_PROVER,
339
+ NulthClient,
340
+ NulthPolicyError,
341
+ USDC_DECIMALS,
342
+ assertAuthEntryMatchesContext,
343
+ authDigestFromEntry,
344
+ commitAllowlist,
345
+ commitDailyCap,
346
+ createNulthSigner,
347
+ createPolicyState,
348
+ dayBucket,
349
+ decodeAuthSignature,
350
+ encodeAuthSignature,
351
+ hashPayee,
352
+ insecureMockProof,
353
+ paymentContextFromManifest,
354
+ setNulthPaymentContext,
355
+ sha256Hex,
356
+ usdcToStroops
357
+ });
@@ -0,0 +1,194 @@
1
+ import { SignAuthEntry } from '@stellar/stellar-sdk/contract';
2
+
3
+ /**
4
+ * Off-chain policy state for a Nulth ZK account.
5
+ * Allowlist and daily cap live here — never published on-chain.
6
+ */
7
+ interface NulthPolicyState {
8
+ /** Maximum daily spend in stroops (7-decimal USDC) */
9
+ dailyCapStroops: bigint;
10
+ /** SHA-256 commitment to sorted allowlisted payee addresses (hex) */
11
+ allowlistCommitment: string;
12
+ /** Allowed payee contract/account addresses — kept off-chain */
13
+ allowedPayees: readonly string[];
14
+ /** Cumulative spend for the current day bucket */
15
+ dailySpendStroops: bigint;
16
+ /** Ledger day bucket: ledger_sequence / 17280 */
17
+ dayBucket: number;
18
+ /** Agent witness secret — binds proofs without exposing policy */
19
+ witnessSecret: string;
20
+ /** Optional session expiry ledger */
21
+ expiryLedger?: number;
22
+ }
23
+ /** Inputs for building a payment authorization proof */
24
+ interface PaymentAuthContext {
25
+ /** Base64 Soroban auth entry preimage from x402 / RouteDock */
26
+ authEntry: string;
27
+ /** Destination payee address (G... or C...) */
28
+ payee: string;
29
+ /** Transfer amount in stroops */
30
+ amountStroops: bigint;
31
+ /** SAC asset contract address */
32
+ assetContract: string;
33
+ /** Current ledger sequence (for day bucket + expiry checks) */
34
+ ledgerSequence: number;
35
+ }
36
+ /** ZK auth proof attached to Soroban __check_auth */
37
+ interface NulthProof {
38
+ version: 1;
39
+ /** Groth16-style proof bytes (base64) — verified on-chain, policy stays private */
40
+ proof: string;
41
+ /** Public inputs bound into the proof (no raw allowlist or cap values) */
42
+ publicInputs: {
43
+ authDigest: string;
44
+ allowlistCommitment: string;
45
+ capCommitment: string;
46
+ payeeHash: string;
47
+ amountStroops: string;
48
+ dayBucket: string;
49
+ };
50
+ }
51
+ /** Encoded signature payload for SorobanAddressCredentials.signature */
52
+ interface NulthAuthSignature {
53
+ nulth: 'zk-v1';
54
+ proof: NulthProof;
55
+ }
56
+ interface NulthClientConfig {
57
+ /** Nulth smart account contract address (C...) — the payer */
58
+ nulthAccount: string;
59
+ /**
60
+ * Network the client will authorize payments on.
61
+ */
62
+ network: 'testnet' | 'mainnet';
63
+ /** Test-only SHA-512 proof stub. */
64
+ prover?: 'mock';
65
+ /** Off-chain policy state */
66
+ policy: NulthPolicyState;
67
+ /** Optional verifier contract deployed alongside the Nulth account */
68
+ verifierContract?: string;
69
+ }
70
+ declare class NulthPolicyError extends Error {
71
+ readonly code: 'payee_not_allowed' | 'daily_cap_exceeded' | 'session_expired' | 'auth_entry_mismatch';
72
+ constructor(code: NulthPolicyError['code'], message: string);
73
+ }
74
+
75
+ /**
76
+ * USDC decimal-to-integer conversion shared by nulth-sdk and @routedock/routedock.
77
+ *
78
+ * 1 USDC = 10^7 stroops on Stellar. This is the single canonical converter;
79
+ * callers in other packages re-export it rather than reimplementing the math.
80
+ */
81
+ declare const USDC_DECIMALS = 7;
82
+ /**
83
+ * Parse a decimal USDC string (e.g. "1.00", "0.0001") into exact stroops
84
+ * (10^-7 USDC) as a bigint, with no floating-point arithmetic.
85
+ *
86
+ * Float math drifts: summing many small amounts in `number` USDC
87
+ * (e.g. 7000 × 0.0001) yields 0.7000000000000006 instead of 0.7, which silently
88
+ * overruns a spend cap on every boundary crossing. Comparing and accumulating in
89
+ * this integer domain is exact.
90
+ *
91
+ * Throws RangeError on malformed input, negatives, precision finer than
92
+ * {@link USDC_DECIMALS} decimals, or values too large to represent exactly.
93
+ */
94
+ declare function usdcToStroops(amount: string): bigint;
95
+
96
+ /** SHA-256 hex digest */
97
+ declare function sha256Hex(data: Buffer | string): string;
98
+ /** Commitment to the daily cap — cap value never appears on-chain */
99
+ declare function commitDailyCap(dailyCapStroops: bigint, witnessSecret: string): string;
100
+ /** Commitment to sorted allowlist — individual payees never published */
101
+ declare function commitAllowlist(payees: readonly string[], witnessSecret: string): string;
102
+ /** Hash a payee for public inputs without revealing the full allowlist */
103
+ declare function hashPayee(payee: string): string;
104
+ /** Ledger day bucket aligned with agent-vault (17280 ledgers ≈ 24h) */
105
+ declare function dayBucket(ledgerSequence: number): number;
106
+ /** Auth digest bound into the ZK proof (mirrors OZ smart account binding) */
107
+ declare function authDigestFromEntry(authEntryBase64: string): string;
108
+ /**
109
+ * Verify that the Soroban auth entry actually being signed matches the
110
+ * off-chain payment context the policy was checked against. Without this,
111
+ * the policy (allowlist + daily cap) checks `context.payee` /
112
+ * `context.amountStroops` while a completely different transfer — a
113
+ * different recipient, amount, or asset — can be the one that actually
114
+ * gets signed and submitted.
115
+ *
116
+ * Throws NulthPolicyError('auth_entry_mismatch', ...) naming the field
117
+ * that failed to match, or if the entry doesn't even decode as a
118
+ * `transfer` invocation.
119
+ */
120
+ declare function assertAuthEntryMatchesContext(authEntry: string, from: string, ctx: Omit<PaymentAuthContext, 'authEntry'>): void;
121
+ type ProverBackend = 'mock';
122
+ declare const DEFAULT_PROVER: ProverBackend;
123
+ /**
124
+ * Deterministic insecure proof bytes for testnet/dev only.
125
+ *
126
+ * @internal
127
+ */
128
+ declare function insecureMockProof(preimage: string): string;
129
+ /** Encode proof as the Soroban auth signature Val (base64 JSON) */
130
+ declare function encodeAuthSignature(proof: NulthProof): string;
131
+ /** Decode an auth signature produced by {@link encodeAuthSignature} */
132
+ declare function decodeAuthSignature(encoded: string): NulthAuthSignature;
133
+
134
+ declare class NulthClient {
135
+ private readonly config;
136
+ private policy;
137
+ private readonly prover;
138
+ constructor(config: NulthClientConfig);
139
+ get nulthAccount(): string;
140
+ get allowlistCommitment(): string;
141
+ get capCommitment(): string;
142
+ /** Current off-chain daily spend (never published) */
143
+ get dailySpendStroops(): bigint;
144
+ /** Prover backend selected for this client */
145
+ get proverBackend(): ProverBackend;
146
+ /**
147
+ * Build a ZK proof that the payment satisfies off-chain policy.
148
+ * Called by RouteDock before attaching the auth signature to the tx.
149
+ */
150
+ buildPaymentAuthProof(context: PaymentAuthContext): NulthProof;
151
+ /** Encode proof for SorobanAddressCredentials.signature */
152
+ encodeProof(proof: NulthProof): string;
153
+ private enforcePolicy;
154
+ }
155
+
156
+ /** Compatible with @x402/stellar ClientStellarSigner */
157
+ interface NulthStellarSigner {
158
+ address: string;
159
+ signAuthEntry: SignAuthEntry;
160
+ }
161
+ interface NulthSignerConfig extends NulthClientConfig {
162
+ /** Pending payment context set before x402 signs auth entries */
163
+ paymentContext?: Omit<PaymentAuthContext, 'authEntry'>;
164
+ }
165
+ /**
166
+ * Create an x402-compatible signer that pays from a Nulth ZK account.
167
+ * Proofs are built off-chain; allowlist and cap never leave the agent.
168
+ */
169
+ declare function createNulthSigner(config: NulthSignerConfig): NulthStellarSigner;
170
+ /** Bind the next payment's public context before x402 calls signAuthEntry */
171
+ declare function setNulthPaymentContext(config: NulthSignerConfig, context: Omit<PaymentAuthContext, 'authEntry'>): void;
172
+ /** Helper to prepare payment context from manifest pricing */
173
+ declare function paymentContextFromManifest(manifest: {
174
+ payee: string;
175
+ asset_contract: string;
176
+ pricing: {
177
+ x402?: {
178
+ amount: string;
179
+ };
180
+ 'mpp-charge'?: {
181
+ amount: string;
182
+ };
183
+ };
184
+ }, mode: 'x402' | 'mpp-charge', ledgerSequence: number): Omit<PaymentAuthContext, 'authEntry'>;
185
+ /** Initialize off-chain policy from explicit allowlist + cap (hashed before use) */
186
+ declare function createPolicyState(input: {
187
+ dailyCapUsdc: string;
188
+ allowedPayees: readonly string[];
189
+ witnessSecret: string;
190
+ expiryLedger?: number;
191
+ ledgerSequence?: number;
192
+ }): NulthPolicyState;
193
+
194
+ export { DEFAULT_PROVER, type NulthAuthSignature, NulthClient, type NulthClientConfig, NulthPolicyError, type NulthPolicyState, type NulthProof, type NulthSignerConfig, type NulthStellarSigner, type PaymentAuthContext, type ProverBackend, USDC_DECIMALS, assertAuthEntryMatchesContext, authDigestFromEntry, commitAllowlist, commitDailyCap, createNulthSigner, createPolicyState, dayBucket, decodeAuthSignature, encodeAuthSignature, hashPayee, insecureMockProof, paymentContextFromManifest, setNulthPaymentContext, sha256Hex, usdcToStroops };
@@ -0,0 +1,194 @@
1
+ import { SignAuthEntry } from '@stellar/stellar-sdk/contract';
2
+
3
+ /**
4
+ * Off-chain policy state for a Nulth ZK account.
5
+ * Allowlist and daily cap live here — never published on-chain.
6
+ */
7
+ interface NulthPolicyState {
8
+ /** Maximum daily spend in stroops (7-decimal USDC) */
9
+ dailyCapStroops: bigint;
10
+ /** SHA-256 commitment to sorted allowlisted payee addresses (hex) */
11
+ allowlistCommitment: string;
12
+ /** Allowed payee contract/account addresses — kept off-chain */
13
+ allowedPayees: readonly string[];
14
+ /** Cumulative spend for the current day bucket */
15
+ dailySpendStroops: bigint;
16
+ /** Ledger day bucket: ledger_sequence / 17280 */
17
+ dayBucket: number;
18
+ /** Agent witness secret — binds proofs without exposing policy */
19
+ witnessSecret: string;
20
+ /** Optional session expiry ledger */
21
+ expiryLedger?: number;
22
+ }
23
+ /** Inputs for building a payment authorization proof */
24
+ interface PaymentAuthContext {
25
+ /** Base64 Soroban auth entry preimage from x402 / RouteDock */
26
+ authEntry: string;
27
+ /** Destination payee address (G... or C...) */
28
+ payee: string;
29
+ /** Transfer amount in stroops */
30
+ amountStroops: bigint;
31
+ /** SAC asset contract address */
32
+ assetContract: string;
33
+ /** Current ledger sequence (for day bucket + expiry checks) */
34
+ ledgerSequence: number;
35
+ }
36
+ /** ZK auth proof attached to Soroban __check_auth */
37
+ interface NulthProof {
38
+ version: 1;
39
+ /** Groth16-style proof bytes (base64) — verified on-chain, policy stays private */
40
+ proof: string;
41
+ /** Public inputs bound into the proof (no raw allowlist or cap values) */
42
+ publicInputs: {
43
+ authDigest: string;
44
+ allowlistCommitment: string;
45
+ capCommitment: string;
46
+ payeeHash: string;
47
+ amountStroops: string;
48
+ dayBucket: string;
49
+ };
50
+ }
51
+ /** Encoded signature payload for SorobanAddressCredentials.signature */
52
+ interface NulthAuthSignature {
53
+ nulth: 'zk-v1';
54
+ proof: NulthProof;
55
+ }
56
+ interface NulthClientConfig {
57
+ /** Nulth smart account contract address (C...) — the payer */
58
+ nulthAccount: string;
59
+ /**
60
+ * Network the client will authorize payments on.
61
+ */
62
+ network: 'testnet' | 'mainnet';
63
+ /** Test-only SHA-512 proof stub. */
64
+ prover?: 'mock';
65
+ /** Off-chain policy state */
66
+ policy: NulthPolicyState;
67
+ /** Optional verifier contract deployed alongside the Nulth account */
68
+ verifierContract?: string;
69
+ }
70
+ declare class NulthPolicyError extends Error {
71
+ readonly code: 'payee_not_allowed' | 'daily_cap_exceeded' | 'session_expired' | 'auth_entry_mismatch';
72
+ constructor(code: NulthPolicyError['code'], message: string);
73
+ }
74
+
75
+ /**
76
+ * USDC decimal-to-integer conversion shared by nulth-sdk and @routedock/routedock.
77
+ *
78
+ * 1 USDC = 10^7 stroops on Stellar. This is the single canonical converter;
79
+ * callers in other packages re-export it rather than reimplementing the math.
80
+ */
81
+ declare const USDC_DECIMALS = 7;
82
+ /**
83
+ * Parse a decimal USDC string (e.g. "1.00", "0.0001") into exact stroops
84
+ * (10^-7 USDC) as a bigint, with no floating-point arithmetic.
85
+ *
86
+ * Float math drifts: summing many small amounts in `number` USDC
87
+ * (e.g. 7000 × 0.0001) yields 0.7000000000000006 instead of 0.7, which silently
88
+ * overruns a spend cap on every boundary crossing. Comparing and accumulating in
89
+ * this integer domain is exact.
90
+ *
91
+ * Throws RangeError on malformed input, negatives, precision finer than
92
+ * {@link USDC_DECIMALS} decimals, or values too large to represent exactly.
93
+ */
94
+ declare function usdcToStroops(amount: string): bigint;
95
+
96
+ /** SHA-256 hex digest */
97
+ declare function sha256Hex(data: Buffer | string): string;
98
+ /** Commitment to the daily cap — cap value never appears on-chain */
99
+ declare function commitDailyCap(dailyCapStroops: bigint, witnessSecret: string): string;
100
+ /** Commitment to sorted allowlist — individual payees never published */
101
+ declare function commitAllowlist(payees: readonly string[], witnessSecret: string): string;
102
+ /** Hash a payee for public inputs without revealing the full allowlist */
103
+ declare function hashPayee(payee: string): string;
104
+ /** Ledger day bucket aligned with agent-vault (17280 ledgers ≈ 24h) */
105
+ declare function dayBucket(ledgerSequence: number): number;
106
+ /** Auth digest bound into the ZK proof (mirrors OZ smart account binding) */
107
+ declare function authDigestFromEntry(authEntryBase64: string): string;
108
+ /**
109
+ * Verify that the Soroban auth entry actually being signed matches the
110
+ * off-chain payment context the policy was checked against. Without this,
111
+ * the policy (allowlist + daily cap) checks `context.payee` /
112
+ * `context.amountStroops` while a completely different transfer — a
113
+ * different recipient, amount, or asset — can be the one that actually
114
+ * gets signed and submitted.
115
+ *
116
+ * Throws NulthPolicyError('auth_entry_mismatch', ...) naming the field
117
+ * that failed to match, or if the entry doesn't even decode as a
118
+ * `transfer` invocation.
119
+ */
120
+ declare function assertAuthEntryMatchesContext(authEntry: string, from: string, ctx: Omit<PaymentAuthContext, 'authEntry'>): void;
121
+ type ProverBackend = 'mock';
122
+ declare const DEFAULT_PROVER: ProverBackend;
123
+ /**
124
+ * Deterministic insecure proof bytes for testnet/dev only.
125
+ *
126
+ * @internal
127
+ */
128
+ declare function insecureMockProof(preimage: string): string;
129
+ /** Encode proof as the Soroban auth signature Val (base64 JSON) */
130
+ declare function encodeAuthSignature(proof: NulthProof): string;
131
+ /** Decode an auth signature produced by {@link encodeAuthSignature} */
132
+ declare function decodeAuthSignature(encoded: string): NulthAuthSignature;
133
+
134
+ declare class NulthClient {
135
+ private readonly config;
136
+ private policy;
137
+ private readonly prover;
138
+ constructor(config: NulthClientConfig);
139
+ get nulthAccount(): string;
140
+ get allowlistCommitment(): string;
141
+ get capCommitment(): string;
142
+ /** Current off-chain daily spend (never published) */
143
+ get dailySpendStroops(): bigint;
144
+ /** Prover backend selected for this client */
145
+ get proverBackend(): ProverBackend;
146
+ /**
147
+ * Build a ZK proof that the payment satisfies off-chain policy.
148
+ * Called by RouteDock before attaching the auth signature to the tx.
149
+ */
150
+ buildPaymentAuthProof(context: PaymentAuthContext): NulthProof;
151
+ /** Encode proof for SorobanAddressCredentials.signature */
152
+ encodeProof(proof: NulthProof): string;
153
+ private enforcePolicy;
154
+ }
155
+
156
+ /** Compatible with @x402/stellar ClientStellarSigner */
157
+ interface NulthStellarSigner {
158
+ address: string;
159
+ signAuthEntry: SignAuthEntry;
160
+ }
161
+ interface NulthSignerConfig extends NulthClientConfig {
162
+ /** Pending payment context set before x402 signs auth entries */
163
+ paymentContext?: Omit<PaymentAuthContext, 'authEntry'>;
164
+ }
165
+ /**
166
+ * Create an x402-compatible signer that pays from a Nulth ZK account.
167
+ * Proofs are built off-chain; allowlist and cap never leave the agent.
168
+ */
169
+ declare function createNulthSigner(config: NulthSignerConfig): NulthStellarSigner;
170
+ /** Bind the next payment's public context before x402 calls signAuthEntry */
171
+ declare function setNulthPaymentContext(config: NulthSignerConfig, context: Omit<PaymentAuthContext, 'authEntry'>): void;
172
+ /** Helper to prepare payment context from manifest pricing */
173
+ declare function paymentContextFromManifest(manifest: {
174
+ payee: string;
175
+ asset_contract: string;
176
+ pricing: {
177
+ x402?: {
178
+ amount: string;
179
+ };
180
+ 'mpp-charge'?: {
181
+ amount: string;
182
+ };
183
+ };
184
+ }, mode: 'x402' | 'mpp-charge', ledgerSequence: number): Omit<PaymentAuthContext, 'authEntry'>;
185
+ /** Initialize off-chain policy from explicit allowlist + cap (hashed before use) */
186
+ declare function createPolicyState(input: {
187
+ dailyCapUsdc: string;
188
+ allowedPayees: readonly string[];
189
+ witnessSecret: string;
190
+ expiryLedger?: number;
191
+ ledgerSequence?: number;
192
+ }): NulthPolicyState;
193
+
194
+ export { DEFAULT_PROVER, type NulthAuthSignature, NulthClient, type NulthClientConfig, NulthPolicyError, type NulthPolicyState, type NulthProof, type NulthSignerConfig, type NulthStellarSigner, type PaymentAuthContext, type ProverBackend, USDC_DECIMALS, assertAuthEntryMatchesContext, authDigestFromEntry, commitAllowlist, commitDailyCap, createNulthSigner, createPolicyState, dayBucket, decodeAuthSignature, encodeAuthSignature, hashPayee, insecureMockProof, paymentContextFromManifest, setNulthPaymentContext, sha256Hex, usdcToStroops };
package/dist/index.js ADDED
@@ -0,0 +1,312 @@
1
+ // src/types.ts
2
+ var NulthPolicyError = class extends Error {
3
+ code;
4
+ constructor(code, message) {
5
+ super(message);
6
+ this.name = "NulthPolicyError";
7
+ this.code = code;
8
+ }
9
+ };
10
+
11
+ // src/proof.ts
12
+ import { createHash } from "crypto";
13
+ import { Address, hash, scValToNative, xdr } from "@stellar/stellar-sdk";
14
+
15
+ // src/usdc.ts
16
+ var USDC_DECIMALS = 7;
17
+ var USDC_SCALE = 10n ** BigInt(USDC_DECIMALS);
18
+ function usdcToStroops(amount) {
19
+ const match = /^(\d+)(?:\.(\d+))?$/.exec(amount.trim());
20
+ if (!match) {
21
+ throw new RangeError(`Invalid USDC amount: "${amount}"`);
22
+ }
23
+ const whole = match[1];
24
+ const frac = match[2] ?? "";
25
+ if (frac.length > USDC_DECIMALS) {
26
+ throw new RangeError(
27
+ `USDC amount "${amount}" exceeds ${USDC_DECIMALS} decimals of precision`
28
+ );
29
+ }
30
+ const fracUnits = BigInt(frac.padEnd(USDC_DECIMALS, "0"));
31
+ const units = BigInt(whole) * USDC_SCALE + fracUnits;
32
+ if (units > BigInt(Number.MAX_SAFE_INTEGER)) {
33
+ throw new RangeError(`USDC amount "${amount}" is too large to represent exactly`);
34
+ }
35
+ return units;
36
+ }
37
+
38
+ // src/proof.ts
39
+ function sha256Hex(data) {
40
+ const buf = typeof data === "string" ? Buffer.from(data, "utf8") : data;
41
+ return createHash("sha256").update(buf).digest("hex");
42
+ }
43
+ function commitDailyCap(dailyCapStroops, witnessSecret) {
44
+ return sha256Hex(`${witnessSecret}:cap:${dailyCapStroops.toString()}`);
45
+ }
46
+ function commitAllowlist(payees, witnessSecret) {
47
+ const sorted = [...payees].sort().join(",");
48
+ return sha256Hex(`${witnessSecret}:allowlist:${sorted}`);
49
+ }
50
+ function hashPayee(payee) {
51
+ return sha256Hex(`payee:${payee}`);
52
+ }
53
+ function dayBucket(ledgerSequence) {
54
+ return Math.floor(ledgerSequence / 17280);
55
+ }
56
+ function authDigestFromEntry(authEntryBase64) {
57
+ const entryHash = hash(Buffer.from(authEntryBase64, "base64"));
58
+ return sha256Hex(entryHash);
59
+ }
60
+ function assertAuthEntryMatchesContext(authEntry, from, ctx) {
61
+ let preimage;
62
+ try {
63
+ preimage = xdr.HashIdPreimage.fromXDR(authEntry, "base64");
64
+ } catch {
65
+ throw new NulthPolicyError("auth_entry_mismatch", "authEntry does not decode as a HashIdPreimage");
66
+ }
67
+ if (preimage.switch() !== xdr.EnvelopeType.envelopeTypeSorobanAuthorization()) {
68
+ throw new NulthPolicyError(
69
+ "auth_entry_mismatch",
70
+ "authEntry is not a Soroban authorization preimage"
71
+ );
72
+ }
73
+ const inv = preimage.sorobanAuthorization().invocation();
74
+ if (inv.subInvocations().length !== 0) {
75
+ throw new NulthPolicyError("auth_entry_mismatch", "authEntry has unexpected sub-invocations");
76
+ }
77
+ if (inv.function().switch() !== xdr.SorobanAuthorizedFunctionType.sorobanAuthorizedFunctionTypeContractFn()) {
78
+ throw new NulthPolicyError("auth_entry_mismatch", "authEntry does not invoke a contract function");
79
+ }
80
+ const fn = inv.function().contractFn();
81
+ const contractAddress = Address.fromScAddress(fn.contractAddress()).toString();
82
+ if (contractAddress !== ctx.assetContract) {
83
+ throw new NulthPolicyError(
84
+ "auth_entry_mismatch",
85
+ `authEntry contract ${contractAddress} does not match assetContract ${ctx.assetContract}`
86
+ );
87
+ }
88
+ const functionName = fn.functionName().toString();
89
+ if (functionName !== "transfer") {
90
+ throw new NulthPolicyError(
91
+ "auth_entry_mismatch",
92
+ `authEntry function ${functionName} does not match expected transfer`
93
+ );
94
+ }
95
+ const args = fn.args();
96
+ if (args.length !== 3) {
97
+ throw new NulthPolicyError("auth_entry_mismatch", `authEntry transfer has ${args.length} args, expected 3`);
98
+ }
99
+ const entryFrom = scValToNative(args[0]);
100
+ if (entryFrom !== from) {
101
+ throw new NulthPolicyError(
102
+ "auth_entry_mismatch",
103
+ `authEntry from ${entryFrom} does not match nulthAccount ${from}`
104
+ );
105
+ }
106
+ const entryTo = scValToNative(args[1]);
107
+ if (entryTo !== ctx.payee) {
108
+ throw new NulthPolicyError(
109
+ "auth_entry_mismatch",
110
+ `authEntry to ${entryTo} does not match paymentContext.payee ${ctx.payee}`
111
+ );
112
+ }
113
+ const entryAmount = scValToNative(args[2]);
114
+ if (entryAmount !== ctx.amountStroops) {
115
+ throw new NulthPolicyError(
116
+ "auth_entry_mismatch",
117
+ `authEntry amount ${entryAmount} does not match paymentContext.amountStroops ${ctx.amountStroops}`
118
+ );
119
+ }
120
+ }
121
+ var DEFAULT_PROVER = "mock";
122
+ function insecureMockProof(preimage) {
123
+ return createHash("sha512").update(`nulth:${preimage}`).digest("base64");
124
+ }
125
+ function encodeAuthSignature(proof) {
126
+ const payload = {
127
+ nulth: "zk-v1",
128
+ proof
129
+ };
130
+ return Buffer.from(JSON.stringify(payload), "utf8").toString("base64");
131
+ }
132
+ function decodeAuthSignature(encoded) {
133
+ const raw = JSON.parse(Buffer.from(encoded, "base64").toString("utf8"));
134
+ if (typeof raw !== "object" || raw === null || raw.nulth !== "zk-v1") {
135
+ throw new Error("Invalid Nulth ZK auth signature");
136
+ }
137
+ return raw;
138
+ }
139
+
140
+ // src/NulthClient.ts
141
+ var NulthClient = class {
142
+ constructor(config) {
143
+ this.config = config;
144
+ this.policy = { ...config.policy };
145
+ this.prover = config.prover ?? DEFAULT_PROVER;
146
+ if (this.prover !== "mock") {
147
+ throw new Error(
148
+ `NulthClient: unknown prover backend ${String(config.prover)}; only 'mock' is implemented`
149
+ );
150
+ }
151
+ if (config.network !== "testnet") {
152
+ throw new Error(
153
+ `NulthClient cannot use the insecure mock prover on mainnet; a production prover is required (network must be 'testnet', got ${String(config.network)})`
154
+ );
155
+ }
156
+ console.warn("NulthClient is using the insecure mock prover; proofs are not cryptographically sound");
157
+ }
158
+ config;
159
+ policy;
160
+ prover;
161
+ get nulthAccount() {
162
+ return this.config.nulthAccount;
163
+ }
164
+ get allowlistCommitment() {
165
+ return this.policy.allowlistCommitment;
166
+ }
167
+ get capCommitment() {
168
+ return commitDailyCap(this.policy.dailyCapStroops, this.policy.witnessSecret);
169
+ }
170
+ /** Current off-chain daily spend (never published) */
171
+ get dailySpendStroops() {
172
+ return this.policy.dailySpendStroops;
173
+ }
174
+ /** Prover backend selected for this client */
175
+ get proverBackend() {
176
+ return this.prover;
177
+ }
178
+ /**
179
+ * Build a ZK proof that the payment satisfies off-chain policy.
180
+ * Called by RouteDock before attaching the auth signature to the tx.
181
+ */
182
+ buildPaymentAuthProof(context) {
183
+ this.enforcePolicy(context);
184
+ const bucket = dayBucket(context.ledgerSequence);
185
+ if (bucket !== this.policy.dayBucket) {
186
+ this.policy.dayBucket = bucket;
187
+ this.policy.dailySpendStroops = 0n;
188
+ }
189
+ const authDigest = authDigestFromEntry(context.authEntry);
190
+ const capCommitment = this.capCommitment;
191
+ const payeeHash = hashPayee(context.payee);
192
+ const preimage = [
193
+ authDigest,
194
+ this.policy.allowlistCommitment,
195
+ capCommitment,
196
+ payeeHash,
197
+ context.amountStroops.toString(),
198
+ bucket.toString(),
199
+ this.config.nulthAccount
200
+ ].join("|");
201
+ const proof = {
202
+ version: 1,
203
+ proof: insecureMockProof(preimage),
204
+ publicInputs: {
205
+ authDigest,
206
+ allowlistCommitment: this.policy.allowlistCommitment,
207
+ capCommitment,
208
+ payeeHash,
209
+ amountStroops: context.amountStroops.toString(),
210
+ dayBucket: bucket.toString()
211
+ }
212
+ };
213
+ this.policy.dailySpendStroops += context.amountStroops;
214
+ return proof;
215
+ }
216
+ /** Encode proof for SorobanAddressCredentials.signature */
217
+ encodeProof(proof) {
218
+ return encodeAuthSignature(proof);
219
+ }
220
+ enforcePolicy(context) {
221
+ if (this.policy.expiryLedger !== void 0 && context.ledgerSequence > this.policy.expiryLedger) {
222
+ throw new NulthPolicyError("session_expired", "Nulth session expired");
223
+ }
224
+ if (!this.policy.allowedPayees.includes(context.payee)) {
225
+ throw new NulthPolicyError(
226
+ "payee_not_allowed",
227
+ `Payee ${context.payee} not in off-chain allowlist`
228
+ );
229
+ }
230
+ const bucket = dayBucket(context.ledgerSequence);
231
+ const spend = bucket === this.policy.dayBucket ? this.policy.dailySpendStroops : 0n;
232
+ const projected = spend + context.amountStroops;
233
+ if (projected > this.policy.dailyCapStroops) {
234
+ throw new NulthPolicyError("daily_cap_exceeded", "Nulth daily cap exceeded");
235
+ }
236
+ }
237
+ };
238
+
239
+ // src/signer.ts
240
+ function createNulthSigner(config) {
241
+ const client = new NulthClient(config);
242
+ const nulthAccount = config.nulthAccount;
243
+ return {
244
+ address: nulthAccount,
245
+ signAuthEntry: async (authEntry) => {
246
+ const ctx = config.paymentContext;
247
+ if (!ctx) {
248
+ throw new Error(
249
+ "Nulth paymentContext must be set before signing \u2014 call setNulthPaymentContext()"
250
+ );
251
+ }
252
+ assertAuthEntryMatchesContext(authEntry, nulthAccount, ctx);
253
+ const proof = client.buildPaymentAuthProof({
254
+ authEntry,
255
+ ...ctx
256
+ });
257
+ return {
258
+ signedAuthEntry: encodeAuthSignature(proof),
259
+ signerAddress: nulthAccount
260
+ };
261
+ }
262
+ };
263
+ }
264
+ function setNulthPaymentContext(config, context) {
265
+ config.paymentContext = context;
266
+ }
267
+ function paymentContextFromManifest(manifest, mode, ledgerSequence) {
268
+ const pricing = manifest.pricing[mode];
269
+ if (!pricing) {
270
+ throw new Error(`manifest.pricing.${mode} missing`);
271
+ }
272
+ return {
273
+ payee: manifest.payee,
274
+ amountStroops: usdcToStroops(pricing.amount),
275
+ assetContract: manifest.asset_contract,
276
+ ledgerSequence
277
+ };
278
+ }
279
+ function createPolicyState(input) {
280
+ const dailyCapStroops = usdcToStroops(input.dailyCapUsdc);
281
+ const witnessSecret = input.witnessSecret;
282
+ return {
283
+ dailyCapStroops,
284
+ allowlistCommitment: commitAllowlist(input.allowedPayees, witnessSecret),
285
+ allowedPayees: input.allowedPayees,
286
+ dailySpendStroops: 0n,
287
+ dayBucket: dayBucket(input.ledgerSequence ?? 0),
288
+ witnessSecret,
289
+ ...input.expiryLedger !== void 0 ? { expiryLedger: input.expiryLedger } : {}
290
+ };
291
+ }
292
+ export {
293
+ DEFAULT_PROVER,
294
+ NulthClient,
295
+ NulthPolicyError,
296
+ USDC_DECIMALS,
297
+ assertAuthEntryMatchesContext,
298
+ authDigestFromEntry,
299
+ commitAllowlist,
300
+ commitDailyCap,
301
+ createNulthSigner,
302
+ createPolicyState,
303
+ dayBucket,
304
+ decodeAuthSignature,
305
+ encodeAuthSignature,
306
+ hashPayee,
307
+ insecureMockProof,
308
+ paymentContextFromManifest,
309
+ setNulthPaymentContext,
310
+ sha256Hex,
311
+ usdcToStroops
312
+ };
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@routedock/nulth-sdk",
3
+ "version": "0.2.0",
4
+ "description": "Off-chain ZK proof builder for Nulth smart accounts on Stellar",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "dist/index.cjs",
8
+ "module": "dist/index.js",
9
+ "types": "dist/index.d.ts",
10
+ "files": [
11
+ "dist",
12
+ "README.md"
13
+ ],
14
+ "exports": {
15
+ ".": {
16
+ "import": {
17
+ "types": "./dist/index.d.ts",
18
+ "default": "./dist/index.js"
19
+ },
20
+ "require": {
21
+ "types": "./dist/index.d.cts",
22
+ "default": "./dist/index.cjs"
23
+ }
24
+ }
25
+ },
26
+ "dependencies": {
27
+ "@stellar/stellar-sdk": "^14.6.1"
28
+ },
29
+ "devDependencies": {
30
+ "@types/node": "^20.0.0",
31
+ "tsup": "^8.0.0",
32
+ "tsx": "^4.22.3",
33
+ "typescript": "^5.4.5"
34
+ },
35
+ "scripts": {
36
+ "build": "tsup",
37
+ "typecheck": "tsc --noEmit",
38
+ "test": "tsx --test src/__tests__/nulth.test.ts"
39
+ }
40
+ }