payid 0.2.8 → 0.3.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.
@@ -0,0 +1,13 @@
1
+ import type { DecisionProof } from "./types";
2
+ import { ethers } from "ethers";
3
+ export declare function generateDecisionProof(params: {
4
+ payId: string;
5
+ owner: string;
6
+ decision: "ALLOW" | "REJECT";
7
+ context: any;
8
+ ruleConfig: any;
9
+ signer: ethers.Signer;
10
+ chainId: number;
11
+ verifyingContract: string;
12
+ ttlSeconds?: number;
13
+ }): Promise<DecisionProof>;
@@ -0,0 +1,20 @@
1
+ import { randomBytes } from "crypto";
2
+ import { hashContext, hashRuleSet } from "./hash";
3
+ import { signDecision } from "./sign";
4
+ export async function generateDecisionProof(params) {
5
+ const issuedAt = Math.floor(Date.now() / 1000);
6
+ const expiresAt = issuedAt + (params.ttlSeconds ?? 60);
7
+ const payload = {
8
+ version: "payid.decision.v1",
9
+ payId: params.payId,
10
+ owner: params.owner,
11
+ decision: params.decision === "ALLOW" ? 1 : 0,
12
+ contextHash: hashContext(params.context),
13
+ ruleSetHash: hashRuleSet(params.ruleConfig),
14
+ issuedAt,
15
+ expiresAt,
16
+ nonce: `0x${randomBytes(32).toString("hex")}`
17
+ };
18
+ const signature = await signDecision(params.signer, params.chainId, params.verifyingContract, payload);
19
+ return { payload, signature };
20
+ }
@@ -0,0 +1,2 @@
1
+ export declare function hashContext(context: any): string;
2
+ export declare function hashRuleSet(ruleConfig: any): string;
@@ -0,0 +1,21 @@
1
+ import { keccak256 } from "ethers";
2
+ /**
3
+ * NOTE:
4
+ * Untuk v1, JSON stringify yang sudah distabilkan
5
+ * (keys sorted). WAJIB untuk golden tests.
6
+ */
7
+ function stableStringify(obj) {
8
+ if (Array.isArray(obj)) {
9
+ return `[${obj.map(stableStringify).join(",")}]`;
10
+ }
11
+ if (obj && typeof obj === "object") {
12
+ return `{${Object.keys(obj).sort().map(k => `"${k}":${stableStringify(obj[k])}`).join(",")}}`;
13
+ }
14
+ return JSON.stringify(obj);
15
+ }
16
+ export function hashContext(context) {
17
+ return keccak256(Buffer.from(stableStringify(context)));
18
+ }
19
+ export function hashRuleSet(ruleConfig) {
20
+ return keccak256(Buffer.from(stableStringify(ruleConfig)));
21
+ }
@@ -0,0 +1,3 @@
1
+ import { ethers } from "ethers";
2
+ import type { DecisionPayload } from "./types";
3
+ export declare function signDecision(signer: ethers.Signer, chainId: number, verifyingContract: string, payload: DecisionPayload): Promise<string>;
@@ -0,0 +1,28 @@
1
+ export async function signDecision(signer, chainId, verifyingContract, payload) {
2
+ const domain = {
3
+ name: "PAY.ID Decision",
4
+ version: "1",
5
+ chainId,
6
+ verifyingContract
7
+ };
8
+ const types = {
9
+ Decision: [
10
+ { name: "version", type: "string" },
11
+ { name: "payId", type: "string" },
12
+ { name: "owner", type: "address" },
13
+ { name: "decision", type: "uint8" },
14
+ { name: "contextHash", type: "bytes32" },
15
+ { name: "ruleSetHash", type: "bytes32" },
16
+ { name: "issuedAt", type: "uint64" },
17
+ { name: "expiresAt", type: "uint64" },
18
+ { name: "nonce", type: "bytes32" }
19
+ ]
20
+ };
21
+ if (typeof signer.signTypedData === "function") {
22
+ return await signer.signTypedData(domain, types, payload);
23
+ }
24
+ if (typeof signer._signTypedData === "function") {
25
+ return await signer._signTypedData(domain, types, payload);
26
+ }
27
+ throw new Error("Signer does not support EIP-712 signing (signTypedData)");
28
+ }
@@ -0,0 +1,16 @@
1
+ export type DecisionValue = 0 | 1;
2
+ export interface DecisionPayload {
3
+ version: "payid.decision.v1";
4
+ payId: string;
5
+ owner: string;
6
+ decision: DecisionValue;
7
+ contextHash: string;
8
+ ruleSetHash: string;
9
+ issuedAt: number;
10
+ expiresAt: number;
11
+ nonce: string;
12
+ }
13
+ export interface DecisionProof {
14
+ payload: DecisionPayload;
15
+ signature: string;
16
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { DecisionProof } from "../decision-proof/types";
2
+ export declare function buildPayCallData(contractAddress: string, proof: DecisionProof): string;
@@ -0,0 +1,10 @@
1
+ import { ethers } from "ethers";
2
+ export function buildPayCallData(contractAddress, proof) {
3
+ const iface = new ethers.Interface([
4
+ "function pay(bytes payload, bytes signature)"
5
+ ]);
6
+ return iface.encodeFunctionData("pay", [
7
+ ethers.toUtf8Bytes(JSON.stringify(proof.payload)),
8
+ proof.signature
9
+ ]);
10
+ }
@@ -0,0 +1,13 @@
1
+ export interface UserOperation {
2
+ sender: string;
3
+ nonce: string;
4
+ initCode: string;
5
+ callData: string;
6
+ callGasLimit: string;
7
+ verificationGasLimit: string;
8
+ preVerificationGas: string;
9
+ maxFeePerGas: string;
10
+ maxPriorityFeePerGas: string;
11
+ paymasterAndData: string;
12
+ signature: string;
13
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,15 @@
1
+ import type { UserOperation } from "./types";
2
+ export declare function buildUserOperation(params: {
3
+ sender: string;
4
+ callData: string;
5
+ nonce: string;
6
+ gas: {
7
+ callGasLimit: string;
8
+ verificationGasLimit: string;
9
+ preVerificationGas: string;
10
+ maxFeePerGas: string;
11
+ maxPriorityFeePerGas: string;
12
+ };
13
+ initCode?: string;
14
+ paymasterAndData?: string;
15
+ }): UserOperation;
@@ -0,0 +1,15 @@
1
+ export function buildUserOperation(params) {
2
+ return {
3
+ sender: params.sender,
4
+ nonce: params.nonce,
5
+ initCode: params.initCode ?? "0x",
6
+ callData: params.callData,
7
+ callGasLimit: params.gas.callGasLimit,
8
+ verificationGasLimit: params.gas.verificationGasLimit,
9
+ preVerificationGas: params.gas.preVerificationGas,
10
+ maxFeePerGas: params.gas.maxFeePerGas,
11
+ maxPriorityFeePerGas: params.gas.maxPriorityFeePerGas,
12
+ paymasterAndData: params.paymasterAndData ?? "0x",
13
+ signature: "0x" // signed later by smart account
14
+ };
15
+ }
@@ -0,0 +1,4 @@
1
+ import type { RuleContext, RuleResult, RuleConfig } from "payid-types";
2
+ export declare function evaluate(wasmBinary: Buffer, context: RuleContext, ruleConfig: RuleConfig, options?: {
3
+ trustedIssuers?: Set<string>;
4
+ }): Promise<RuleResult>;
@@ -0,0 +1,46 @@
1
+ import { executeRule } from "payid-rule-engine";
2
+ import { normalizeContext } from "./normalize";
3
+ import { preprocessContextV2 } from "payid-rule-engine";
4
+ export async function evaluate(wasmBinary, context, ruleConfig, options) {
5
+ // ---- basic validation (v1 behavior) ----
6
+ if (!context || typeof context !== "object") {
7
+ throw new Error("evaluate(): context is required");
8
+ }
9
+ if (!context.tx) {
10
+ throw new Error("evaluate(): context.tx is required");
11
+ }
12
+ if (!ruleConfig || typeof ruleConfig !== "object") {
13
+ throw new Error("evaluate(): ruleConfig is required");
14
+ }
15
+ let result;
16
+ try {
17
+ // ---- NEW: preprocess v2 context if enabled ----
18
+ const preparedContext = options?.trustedIssuers
19
+ ? preprocessContextV2(context, ruleConfig, options.trustedIssuers)
20
+ : context;
21
+ // ---- existing normalization ----
22
+ const normalized = normalizeContext(preparedContext);
23
+ // ---- execute WASM rule engine ----
24
+ result = await executeRule(wasmBinary, normalized, ruleConfig);
25
+ }
26
+ catch (err) {
27
+ return {
28
+ decision: "REJECT",
29
+ code: "CONTEXT_OR_ENGINE_ERROR",
30
+ reason: err?.message ?? "rule evaluation failed"
31
+ };
32
+ }
33
+ // ---- output validation ----
34
+ if (result.decision !== "ALLOW" && result.decision !== "REJECT") {
35
+ return {
36
+ decision: "REJECT",
37
+ code: "INVALID_ENGINE_OUTPUT",
38
+ reason: "invalid decision value"
39
+ };
40
+ }
41
+ return {
42
+ decision: result.decision,
43
+ code: result.code || "UNKNOWN",
44
+ reason: result.reason
45
+ };
46
+ }
@@ -0,0 +1 @@
1
+ export { PayID } from "./payid";
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { PayID } from "./payid";
@@ -0,0 +1,2 @@
1
+ import type { RuleContext } from "payid-types";
2
+ export declare function normalizeContext(ctx: RuleContext): RuleContext;
@@ -0,0 +1,11 @@
1
+ export function normalizeContext(ctx) {
2
+ return {
3
+ ...ctx,
4
+ tx: {
5
+ ...ctx.tx,
6
+ sender: ctx.tx.sender?.toLowerCase(),
7
+ receiver: ctx.tx.receiver?.toLowerCase(),
8
+ asset: ctx.tx.asset.toUpperCase()
9
+ }
10
+ };
11
+ }
@@ -0,0 +1,70 @@
1
+ import type { RuleContext, RuleResult, RuleConfig } from "payid-types";
2
+ import type { RuleSource } from "./resolver/types";
3
+ import { ethers } from "ethers";
4
+ import type { UserOperation } from "./erc4337/types";
5
+ export declare class PayID {
6
+ private wasm;
7
+ constructor(wasmPath: string);
8
+ evaluate(context: RuleContext, ruleConfig: RuleConfig): Promise<RuleResult>;
9
+ evaluateAndProve(params: {
10
+ context: RuleContext;
11
+ ruleConfig: RuleConfig;
12
+ payId: string;
13
+ owner: string;
14
+ signer: ethers.Signer;
15
+ chainId: number;
16
+ verifyingContract: string;
17
+ ttlSeconds?: number;
18
+ }): Promise<{
19
+ result: RuleResult;
20
+ proof: import("./decision-proof/types").DecisionProof;
21
+ }>;
22
+ evaluateWithRuleSource(context: RuleContext, ruleSource: RuleSource): Promise<RuleResult>;
23
+ evaluateAndProveFromSource(params: {
24
+ context: RuleContext;
25
+ ruleSource: RuleSource;
26
+ payId: string;
27
+ owner: string;
28
+ signer: ethers.Signer;
29
+ chainId: number;
30
+ verifyingContract: string;
31
+ ttlSeconds?: number;
32
+ }): Promise<{
33
+ result: RuleResult;
34
+ proof: import("./decision-proof/types").DecisionProof;
35
+ } | {
36
+ result: {
37
+ decision: string;
38
+ code: string;
39
+ reason: any;
40
+ };
41
+ proof: null;
42
+ }>;
43
+ evaluateProveAndBuildUserOp(params: {
44
+ context: RuleContext;
45
+ ruleSource: {
46
+ uri: string;
47
+ hash?: string;
48
+ };
49
+ payId: string;
50
+ owner: string;
51
+ signer: ethers.Signer;
52
+ smartAccount: string;
53
+ targetContract: string;
54
+ nonce: string;
55
+ gas: {
56
+ callGasLimit: string;
57
+ verificationGasLimit: string;
58
+ preVerificationGas: string;
59
+ maxFeePerGas: string;
60
+ maxPriorityFeePerGas: string;
61
+ };
62
+ paymasterAndData?: string;
63
+ chainId: number;
64
+ verifyingContract: string;
65
+ }): Promise<{
66
+ result: RuleResult;
67
+ userOp: UserOperation | null;
68
+ proof: any;
69
+ }>;
70
+ }
package/dist/payid.js ADDED
@@ -0,0 +1,93 @@
1
+ import { evaluate as evaluatePolicy } from "./evaluate";
2
+ import { generateDecisionProof } from "./decision-proof/generate";
3
+ import { resolveRule } from "./resolver/resolver";
4
+ import * as fs from "fs";
5
+ import { buildPayCallData } from "./erc4337/build";
6
+ import { buildUserOperation } from "./erc4337/userop";
7
+ export class PayID {
8
+ constructor(wasmPath) {
9
+ this.wasm = fs.readFileSync(wasmPath);
10
+ }
11
+ async evaluate(context, ruleConfig) {
12
+ return evaluatePolicy(this.wasm, context, ruleConfig);
13
+ }
14
+ async evaluateAndProve(params) {
15
+ const result = await this.evaluate(params.context, params.ruleConfig);
16
+ const proof = await generateDecisionProof({
17
+ payId: params.payId,
18
+ owner: params.owner,
19
+ decision: result.decision,
20
+ context: params.context,
21
+ ruleConfig: params.ruleConfig,
22
+ signer: params.signer,
23
+ chainId: params.chainId,
24
+ verifyingContract: params.verifyingContract,
25
+ ttlSeconds: params.ttlSeconds
26
+ });
27
+ return { result, proof };
28
+ }
29
+ async evaluateWithRuleSource(context, ruleSource) {
30
+ try {
31
+ const { config } = await resolveRule(ruleSource);
32
+ return await this.evaluate(context, config);
33
+ }
34
+ catch (err) {
35
+ return {
36
+ decision: "REJECT",
37
+ code: "RULE_RESOLVE_ERROR",
38
+ reason: err?.message ?? "failed to resolve rule"
39
+ };
40
+ }
41
+ }
42
+ async evaluateAndProveFromSource(params) {
43
+ try {
44
+ const { config } = await resolveRule(params.ruleSource);
45
+ const result = await this.evaluate(params.context, config);
46
+ const proof = await generateDecisionProof({
47
+ payId: params.payId,
48
+ owner: params.owner,
49
+ decision: result.decision,
50
+ context: params.context,
51
+ ruleConfig: config,
52
+ signer: params.signer,
53
+ chainId: params.chainId,
54
+ verifyingContract: params.verifyingContract,
55
+ ttlSeconds: params.ttlSeconds
56
+ });
57
+ return { result, proof };
58
+ }
59
+ catch (err) {
60
+ return {
61
+ result: {
62
+ decision: "REJECT",
63
+ code: "RULE_RESOLVE_ERROR",
64
+ reason: err?.message ?? "rule resolve failed"
65
+ },
66
+ proof: null
67
+ };
68
+ }
69
+ }
70
+ async evaluateProveAndBuildUserOp(params) {
71
+ const { result, proof } = await this.evaluateAndProveFromSource({
72
+ context: params.context,
73
+ ruleSource: params.ruleSource,
74
+ payId: params.payId,
75
+ owner: params.owner,
76
+ signer: params.signer,
77
+ chainId: params.chainId,
78
+ verifyingContract: params.verifyingContract
79
+ });
80
+ if (result.decision !== "ALLOW" || !proof) {
81
+ return { result: result, userOp: null, proof };
82
+ }
83
+ const callData = buildPayCallData(params.targetContract, proof);
84
+ const userOp = buildUserOperation({
85
+ sender: params.smartAccount,
86
+ nonce: params.nonce,
87
+ callData,
88
+ gas: params.gas,
89
+ paymasterAndData: params.paymasterAndData
90
+ });
91
+ return { result, userOp, proof };
92
+ }
93
+ }
@@ -0,0 +1 @@
1
+ export declare function resolveHttpRule(uri: string, expectedHash?: string): Promise<any>;
@@ -0,0 +1,10 @@
1
+ import { verifyHash } from "./utils";
2
+ export async function resolveHttpRule(uri, expectedHash) {
3
+ const res = await fetch(uri);
4
+ if (!res.ok) {
5
+ throw new Error(`HTTP_RULE_FETCH_FAILED: ${res.status}`);
6
+ }
7
+ const text = await res.text();
8
+ verifyHash(text, expectedHash);
9
+ return JSON.parse(text);
10
+ }
@@ -0,0 +1 @@
1
+ export declare function resolveIpfsRule(uri: string, expectedHash?: string, gateway?: string): Promise<any>;
@@ -0,0 +1,13 @@
1
+ import { verifyHash } from "./utils";
2
+ const DEFAULT_GATEWAY = "https://ipfs.io/ipfs/";
3
+ export async function resolveIpfsRule(uri, expectedHash, gateway = DEFAULT_GATEWAY) {
4
+ const cid = uri.replace("ipfs://", "");
5
+ const url = `${gateway}${cid}`;
6
+ const res = await fetch(url);
7
+ if (!res.ok) {
8
+ throw new Error(`IPFS_RULE_FETCH_FAILED: ${res.status}`);
9
+ }
10
+ const text = await res.text();
11
+ verifyHash(text, expectedHash);
12
+ return JSON.parse(text);
13
+ }
@@ -0,0 +1,2 @@
1
+ import type { RuleSource, ResolvedRule } from "./types";
2
+ export declare function resolveRule(source: RuleSource): Promise<ResolvedRule>;
@@ -0,0 +1,19 @@
1
+ import { resolveHttpRule } from "./http";
2
+ import { resolveIpfsRule } from "./ipfs";
3
+ export async function resolveRule(source) {
4
+ const { uri, hash } = source;
5
+ let config;
6
+ if (uri.startsWith("ipfs://")) {
7
+ config = await resolveIpfsRule(uri, hash);
8
+ }
9
+ else if (uri.startsWith("http://") || uri.startsWith("https://")) {
10
+ config = await resolveHttpRule(uri, hash);
11
+ }
12
+ else {
13
+ throw new Error("UNSUPPORTED_RULE_URI");
14
+ }
15
+ return {
16
+ config,
17
+ source
18
+ };
19
+ }
@@ -0,0 +1,8 @@
1
+ export interface RuleSource {
2
+ uri: string;
3
+ hash?: string;
4
+ }
5
+ export interface ResolvedRule {
6
+ config: any;
7
+ source: RuleSource;
8
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export declare function verifyHash(content: string, expectedHash?: string): void;
@@ -0,0 +1,9 @@
1
+ import { keccak256 } from "ethers";
2
+ export function verifyHash(content, expectedHash) {
3
+ if (!expectedHash)
4
+ return;
5
+ const actual = keccak256(Buffer.from(content));
6
+ if (actual !== expectedHash) {
7
+ throw new Error("RULE_HASH_MISMATCH");
8
+ }
9
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payid",
3
- "version": "0.2.8",
3
+ "version": "0.3.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -10,6 +10,9 @@
10
10
  "payid-rule-engine": "^0.1.1",
11
11
  "payid-types": "^0.1.1"
12
12
  },
13
+ "files": [
14
+ "dist"
15
+ ],
13
16
  "scripts": {
14
17
  "build:js": "bun build src/index.ts --outdir dist --target node",
15
18
  "build:build": "tsc -p tsconfig.build.json",