payid 0.2.7 → 0.2.9

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.
Files changed (51) hide show
  1. package/dist/decision-proof/generate.d.ts +13 -0
  2. package/dist/decision-proof/generate.js +20 -0
  3. package/dist/decision-proof/hash.d.ts +2 -0
  4. package/dist/decision-proof/hash.js +21 -0
  5. package/dist/decision-proof/sign.d.ts +3 -0
  6. package/dist/decision-proof/sign.js +28 -0
  7. package/dist/decision-proof/types.d.ts +16 -0
  8. package/dist/decision-proof/types.js +1 -0
  9. package/dist/erc4337/build.d.ts +2 -0
  10. package/dist/erc4337/build.js +10 -0
  11. package/dist/erc4337/types.d.ts +13 -0
  12. package/dist/erc4337/types.js +1 -0
  13. package/dist/erc4337/userop.d.ts +15 -0
  14. package/dist/erc4337/userop.js +15 -0
  15. package/dist/evaluate.d.ts +4 -0
  16. package/dist/evaluate.js +46 -0
  17. package/{src/index.ts → dist/index.d.ts} +1 -1
  18. package/dist/index.js +1 -0
  19. package/dist/normalize.d.ts +2 -0
  20. package/dist/normalize.js +11 -0
  21. package/dist/payid.d.ts +70 -0
  22. package/dist/payid.js +93 -0
  23. package/dist/resolver/http.d.ts +1 -0
  24. package/dist/resolver/http.js +10 -0
  25. package/dist/resolver/ipfs.d.ts +1 -0
  26. package/dist/resolver/ipfs.js +13 -0
  27. package/dist/resolver/resolver.d.ts +2 -0
  28. package/dist/resolver/resolver.js +19 -0
  29. package/dist/resolver/types.d.ts +8 -0
  30. package/dist/resolver/types.js +1 -0
  31. package/dist/resolver/utils.d.ts +1 -0
  32. package/dist/resolver/utils.js +9 -0
  33. package/package.json +6 -2
  34. package/index.ts +0 -1
  35. package/src/decision-proof/generate.ts +0 -41
  36. package/src/decision-proof/hash.ts +0 -26
  37. package/src/decision-proof/sign.ts +0 -42
  38. package/src/decision-proof/types.ts +0 -18
  39. package/src/erc4337/build.ts +0 -16
  40. package/src/erc4337/types.ts +0 -13
  41. package/src/erc4337/userop.ts +0 -30
  42. package/src/evaluate.ts +0 -63
  43. package/src/normalize.ts +0 -13
  44. package/src/payid.ts +0 -167
  45. package/src/resolver/http.ts +0 -16
  46. package/src/resolver/ipfs.ts +0 -22
  47. package/src/resolver/resolver.ts +0 -24
  48. package/src/resolver/types.ts +0 -9
  49. package/src/resolver/utils.ts +0 -13
  50. package/src/rules/rule_generic_evaluator.wasm +0 -0
  51. package/tsconfig.json +0 -29
@@ -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
+ }
@@ -1 +1 @@
1
- export { PayID } from "./payid";
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.7",
3
+ "version": "0.2.9",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -10,9 +10,13 @@
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
- "build": "bun run build:js"
18
+ "build:build": "tsc -p tsconfig.build.json",
19
+ "build": "bun run build:js && bun run build:build"
16
20
  },
17
21
  "devDependencies": {
18
22
  "@types/bun": "latest"
package/index.ts DELETED
@@ -1 +0,0 @@
1
- export * from "./src";
@@ -1,41 +0,0 @@
1
- import { randomBytes } from "crypto";
2
- import type { DecisionPayload, DecisionProof } from "./types";
3
- import { hashContext, hashRuleSet } from "./hash";
4
- import { signDecision } from "./sign";
5
- import { ethers } from "ethers";
6
-
7
- export async function generateDecisionProof(params: {
8
- payId: string;
9
- owner: string;
10
- decision: "ALLOW" | "REJECT";
11
- context: any;
12
- ruleConfig: any;
13
- signer: ethers.Signer;
14
- chainId: number;
15
- verifyingContract: string;
16
- ttlSeconds?: number;
17
- }): Promise<DecisionProof> {
18
- const issuedAt = Math.floor(Date.now() / 1000);
19
- const expiresAt = issuedAt + (params.ttlSeconds ?? 60);
20
-
21
- const payload: DecisionPayload = {
22
- version: "payid.decision.v1",
23
- payId: params.payId,
24
- owner: params.owner,
25
- decision: params.decision === "ALLOW" ? 1 : 0,
26
- contextHash: hashContext(params.context),
27
- ruleSetHash: hashRuleSet(params.ruleConfig),
28
- issuedAt,
29
- expiresAt,
30
- nonce: `0x${randomBytes(32).toString("hex")}`
31
- };
32
-
33
- const signature = await signDecision(
34
- params.signer,
35
- params.chainId,
36
- params.verifyingContract,
37
- payload
38
- );
39
-
40
- return { payload, signature };
41
- }
@@ -1,26 +0,0 @@
1
- import { keccak256 } from "ethers";
2
-
3
- /**
4
- * NOTE:
5
- * Untuk v1, JSON stringify yang sudah distabilkan
6
- * (keys sorted). WAJIB untuk golden tests.
7
- */
8
- function stableStringify(obj: any): string {
9
- if (Array.isArray(obj)) {
10
- return `[${obj.map(stableStringify).join(",")}]`;
11
- }
12
- if (obj && typeof obj === "object") {
13
- return `{${Object.keys(obj).sort().map(
14
- k => `"${k}":${stableStringify(obj[k])}`
15
- ).join(",")}}`;
16
- }
17
- return JSON.stringify(obj);
18
- }
19
-
20
- export function hashContext(context: any): string {
21
- return keccak256(Buffer.from(stableStringify(context)));
22
- }
23
-
24
- export function hashRuleSet(ruleConfig: any): string {
25
- return keccak256(Buffer.from(stableStringify(ruleConfig)));
26
- }
@@ -1,42 +0,0 @@
1
- import { ethers } from "ethers";
2
- import type { DecisionPayload } from "./types";
3
-
4
- export async function signDecision(
5
- signer: ethers.Signer,
6
- chainId: number,
7
- verifyingContract: string,
8
- payload: DecisionPayload
9
- ): Promise<string> {
10
- const domain = {
11
- name: "PAY.ID Decision",
12
- version: "1",
13
- chainId,
14
- verifyingContract
15
- };
16
-
17
- const types = {
18
- Decision: [
19
- { name: "version", type: "string" },
20
- { name: "payId", type: "string" },
21
- { name: "owner", type: "address" },
22
- { name: "decision", type: "uint8" },
23
- { name: "contextHash", type: "bytes32" },
24
- { name: "ruleSetHash", type: "bytes32" },
25
- { name: "issuedAt", type: "uint64" },
26
- { name: "expiresAt", type: "uint64" },
27
- { name: "nonce", type: "bytes32" }
28
- ]
29
- };
30
-
31
- if (typeof (signer as any).signTypedData === "function") {
32
- return await (signer as any).signTypedData(domain, types, payload);
33
- }
34
-
35
- if (typeof (signer as any)._signTypedData === "function") {
36
- return await (signer as any)._signTypedData(domain, types, payload);
37
- }
38
-
39
- throw new Error(
40
- "Signer does not support EIP-712 signing (signTypedData)"
41
- );
42
- }
@@ -1,18 +0,0 @@
1
- export type DecisionValue = 0 | 1; // 1=ALLOW, 0=REJECT
2
-
3
- export interface DecisionPayload {
4
- version: "payid.decision.v1";
5
- payId: string;
6
- owner: string; // authority address
7
- decision: DecisionValue;
8
- contextHash: string; // bytes32
9
- ruleSetHash: string; // bytes32
10
- issuedAt: number; // uint64
11
- expiresAt: number; // uint64
12
- nonce: string; // bytes32
13
- }
14
-
15
- export interface DecisionProof {
16
- payload: DecisionPayload;
17
- signature: string; // EIP-712 signature
18
- }
@@ -1,16 +0,0 @@
1
- import { ethers } from "ethers";
2
- import type { DecisionProof } from "../decision-proof/types";
3
-
4
- export function buildPayCallData(
5
- contractAddress: string,
6
- proof: DecisionProof
7
- ): string {
8
- const iface = new ethers.Interface([
9
- "function pay(bytes payload, bytes signature)"
10
- ]);
11
-
12
- return iface.encodeFunctionData("pay", [
13
- ethers.toUtf8Bytes(JSON.stringify(proof.payload)),
14
- proof.signature
15
- ]);
16
- }
@@ -1,13 +0,0 @@
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
- }
@@ -1,30 +0,0 @@
1
- import type { UserOperation } from "./types";
2
-
3
- export function buildUserOperation(params: {
4
- sender: string;
5
- callData: string;
6
- nonce: string;
7
- gas: {
8
- callGasLimit: string;
9
- verificationGasLimit: string;
10
- preVerificationGas: string;
11
- maxFeePerGas: string;
12
- maxPriorityFeePerGas: string;
13
- };
14
- initCode?: string;
15
- paymasterAndData?: string;
16
- }): UserOperation {
17
- return {
18
- sender: params.sender,
19
- nonce: params.nonce,
20
- initCode: params.initCode ?? "0x",
21
- callData: params.callData,
22
- callGasLimit: params.gas.callGasLimit,
23
- verificationGasLimit: params.gas.verificationGasLimit,
24
- preVerificationGas: params.gas.preVerificationGas,
25
- maxFeePerGas: params.gas.maxFeePerGas,
26
- maxPriorityFeePerGas: params.gas.maxPriorityFeePerGas,
27
- paymasterAndData: params.paymasterAndData ?? "0x",
28
- signature: "0x" // signed later by smart account
29
- };
30
- }
package/src/evaluate.ts DELETED
@@ -1,63 +0,0 @@
1
- import type { RuleContext, RuleResult, RuleConfig } from "payid-types";
2
- import { executeRule } from "payid-rule-engine";
3
- import { normalizeContext } from "./normalize";
4
- import { preprocessContextV2 } from "payid-rule-engine";
5
-
6
- export async function evaluate(
7
- wasmBinary: Buffer,
8
- context: RuleContext,
9
- ruleConfig: RuleConfig,
10
- options?: {
11
- trustedIssuers?: Set<string>;
12
- }
13
- ): Promise<RuleResult> {
14
- // ---- basic validation (v1 behavior) ----
15
- if (!context || typeof context !== "object") {
16
- throw new Error("evaluate(): context is required");
17
- }
18
-
19
- if (!context.tx) {
20
- throw new Error("evaluate(): context.tx is required");
21
- }
22
-
23
- if (!ruleConfig || typeof ruleConfig !== "object") {
24
- throw new Error("evaluate(): ruleConfig is required");
25
- }
26
-
27
- let result: RuleResult;
28
-
29
- try {
30
- // ---- NEW: preprocess v2 context if enabled ----
31
- const preparedContext =
32
- options?.trustedIssuers
33
- ? preprocessContextV2(context, ruleConfig, options.trustedIssuers)
34
- : context;
35
-
36
- // ---- existing normalization ----
37
- const normalized = normalizeContext(preparedContext);
38
-
39
- // ---- execute WASM rule engine ----
40
- result = await executeRule(wasmBinary, normalized, ruleConfig);
41
- } catch (err: any) {
42
- return {
43
- decision: "REJECT",
44
- code: "CONTEXT_OR_ENGINE_ERROR",
45
- reason: err?.message ?? "rule evaluation failed"
46
- };
47
- }
48
-
49
- // ---- output validation ----
50
- if (result.decision !== "ALLOW" && result.decision !== "REJECT") {
51
- return {
52
- decision: "REJECT",
53
- code: "INVALID_ENGINE_OUTPUT",
54
- reason: "invalid decision value"
55
- };
56
- }
57
-
58
- return {
59
- decision: result.decision,
60
- code: result.code || "UNKNOWN",
61
- reason: result.reason
62
- };
63
- }
package/src/normalize.ts DELETED
@@ -1,13 +0,0 @@
1
- import type { RuleContext } from "payid-types";
2
-
3
- export function normalizeContext(ctx: RuleContext): RuleContext {
4
- return {
5
- ...ctx,
6
- tx: {
7
- ...ctx.tx,
8
- sender: ctx.tx.sender?.toLowerCase(),
9
- receiver: ctx.tx.receiver?.toLowerCase(),
10
- asset: ctx.tx.asset.toUpperCase()
11
- }
12
- };
13
- }
package/src/payid.ts DELETED
@@ -1,167 +0,0 @@
1
- import type { RuleContext, RuleResult, RuleConfig } from "payid-types";
2
- import { evaluate as evaluatePolicy } from "./evaluate";
3
- import { generateDecisionProof } from "./decision-proof/generate";
4
- import { resolveRule } from "./resolver/resolver";
5
- import type { RuleSource } from "./resolver/types";
6
- import { ethers } from "ethers";
7
- import fs from "fs";
8
- import type { UserOperation } from "./erc4337/types";
9
- import { buildPayCallData } from "./erc4337/build";
10
- import { buildUserOperation } from "./erc4337/userop";
11
-
12
- export class PayID {
13
- private wasm: Buffer;
14
-
15
- constructor(wasmPath: string) {
16
- this.wasm = fs.readFileSync(wasmPath);
17
- }
18
-
19
- async evaluate(
20
- context: RuleContext,
21
- ruleConfig: RuleConfig
22
- ): Promise<RuleResult> {
23
- return evaluatePolicy(this.wasm, context, ruleConfig);
24
- }
25
-
26
-
27
- async evaluateAndProve(params: {
28
- context: RuleContext;
29
- ruleConfig: RuleConfig;
30
- payId: string;
31
- owner: string;
32
- signer: ethers.Signer;
33
- chainId: number;
34
- verifyingContract: string;
35
- ttlSeconds?: number;
36
- }) {
37
- const result = await this.evaluate(params.context, params.ruleConfig);
38
-
39
- const proof = await generateDecisionProof({
40
- payId: params.payId,
41
- owner: params.owner,
42
- decision: result.decision,
43
- context: params.context,
44
- ruleConfig: params.ruleConfig,
45
- signer: params.signer,
46
- chainId: params.chainId,
47
- verifyingContract: params.verifyingContract,
48
- ttlSeconds: params.ttlSeconds
49
- });
50
-
51
- return { result, proof };
52
- }
53
-
54
- async evaluateWithRuleSource(
55
- context: RuleContext,
56
- ruleSource: RuleSource
57
- ): Promise<RuleResult> {
58
- try {
59
- const { config } = await resolveRule(ruleSource);
60
- return await this.evaluate(context, config);
61
- } catch (err: any) {
62
- return {
63
- decision: "REJECT",
64
- code: "RULE_RESOLVE_ERROR",
65
- reason: err?.message ?? "failed to resolve rule"
66
- };
67
- }
68
- }
69
-
70
- async evaluateAndProveFromSource(params: {
71
- context: RuleContext;
72
- ruleSource: RuleSource;
73
- payId: string;
74
- owner: string;
75
- signer: ethers.Signer;
76
- chainId: number;
77
- verifyingContract: string;
78
- ttlSeconds?: number;
79
- }) {
80
- try {
81
- const { config } = await resolveRule(params.ruleSource);
82
-
83
- const result = await this.evaluate(params.context, config);
84
-
85
- const proof = await generateDecisionProof({
86
- payId: params.payId,
87
- owner: params.owner,
88
- decision: result.decision,
89
- context: params.context,
90
- ruleConfig: config,
91
- signer: params.signer,
92
- chainId: params.chainId,
93
- verifyingContract: params.verifyingContract,
94
- ttlSeconds: params.ttlSeconds
95
- });
96
-
97
- return { result, proof };
98
- } catch (err: any) {
99
- return {
100
- result: {
101
- decision: "REJECT",
102
- code: "RULE_RESOLVE_ERROR",
103
- reason: err?.message ?? "rule resolve failed"
104
- },
105
- proof: null
106
- };
107
- }
108
- }
109
-
110
- async evaluateProveAndBuildUserOp(params: {
111
- context: RuleContext;
112
- ruleSource: { uri: string; hash?: string; };
113
- payId: string;
114
- owner: string;
115
- signer: ethers.Signer;
116
-
117
- // ERC-4337 specific
118
- smartAccount: string;
119
- targetContract: string;
120
- nonce: string;
121
- gas: {
122
- callGasLimit: string;
123
- verificationGasLimit: string;
124
- preVerificationGas: string;
125
- maxFeePerGas: string;
126
- maxPriorityFeePerGas: string;
127
- };
128
- paymasterAndData?: string;
129
-
130
- chainId: number;
131
- verifyingContract: string;
132
- }): Promise<{
133
- result: RuleResult;
134
- userOp: UserOperation | null;
135
- proof: any;
136
- }> {
137
- const { result, proof } =
138
- await this.evaluateAndProveFromSource({
139
- context: params.context,
140
- ruleSource: params.ruleSource,
141
- payId: params.payId,
142
- owner: params.owner,
143
- signer: params.signer,
144
- chainId: params.chainId,
145
- verifyingContract: params.verifyingContract
146
- });
147
-
148
- if (result.decision !== "ALLOW" || !proof) {
149
- return { result: (result as any), userOp: null, proof };
150
- }
151
-
152
- const callData = buildPayCallData(
153
- params.targetContract,
154
- proof
155
- );
156
-
157
- const userOp = buildUserOperation({
158
- sender: params.smartAccount,
159
- nonce: params.nonce,
160
- callData,
161
- gas: params.gas,
162
- paymasterAndData: params.paymasterAndData
163
- });
164
-
165
- return { result, userOp, proof };
166
- }
167
- }
@@ -1,16 +0,0 @@
1
- import { verifyHash } from "./utils";
2
-
3
- export async function resolveHttpRule(
4
- uri: string,
5
- expectedHash?: string
6
- ): Promise<any> {
7
- const res = await fetch(uri);
8
- if (!res.ok) {
9
- throw new Error(`HTTP_RULE_FETCH_FAILED: ${res.status}`);
10
- }
11
-
12
- const text = await res.text();
13
- verifyHash(text, expectedHash);
14
-
15
- return JSON.parse(text);
16
- }
@@ -1,22 +0,0 @@
1
- import { verifyHash } from "./utils";
2
-
3
- const DEFAULT_GATEWAY = "https://ipfs.io/ipfs/";
4
-
5
- export async function resolveIpfsRule(
6
- uri: string,
7
- expectedHash?: string,
8
- gateway = DEFAULT_GATEWAY
9
- ): Promise<any> {
10
- const cid = uri.replace("ipfs://", "");
11
- const url = `${gateway}${cid}`;
12
-
13
- const res = await fetch(url);
14
- if (!res.ok) {
15
- throw new Error(`IPFS_RULE_FETCH_FAILED: ${res.status}`);
16
- }
17
-
18
- const text = await res.text();
19
- verifyHash(text, expectedHash);
20
-
21
- return JSON.parse(text);
22
- }
@@ -1,24 +0,0 @@
1
- import type { RuleSource, ResolvedRule } from "./types";
2
- import { resolveHttpRule } from "./http";
3
- import { resolveIpfsRule } from "./ipfs";
4
-
5
- export async function resolveRule(
6
- source: RuleSource
7
- ): Promise<ResolvedRule> {
8
- const { uri, hash } = source;
9
-
10
- let config: any;
11
-
12
- if (uri.startsWith("ipfs://")) {
13
- config = await resolveIpfsRule(uri, hash);
14
- } else if (uri.startsWith("http://") || uri.startsWith("https://")) {
15
- config = await resolveHttpRule(uri, hash);
16
- } else {
17
- throw new Error("UNSUPPORTED_RULE_URI");
18
- }
19
-
20
- return {
21
- config,
22
- source
23
- };
24
- }
@@ -1,9 +0,0 @@
1
- export interface RuleSource {
2
- uri: string; // ipfs://... | https://...
3
- hash?: string; // optional keccak256 hash
4
- }
5
-
6
- export interface ResolvedRule {
7
- config: any;
8
- source: RuleSource;
9
- }
@@ -1,13 +0,0 @@
1
- import { keccak256 } from "ethers";
2
-
3
- export function verifyHash(
4
- content: string,
5
- expectedHash?: string
6
- ) {
7
- if (!expectedHash) return;
8
-
9
- const actual = keccak256(Buffer.from(content));
10
- if (actual !== expectedHash) {
11
- throw new Error("RULE_HASH_MISMATCH");
12
- }
13
- }
package/tsconfig.json DELETED
@@ -1,29 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- // Environment setup & latest features
4
- "lib": ["ESNext"],
5
- "target": "ESNext",
6
- "module": "Preserve",
7
- "moduleDetection": "force",
8
- "jsx": "react-jsx",
9
- "allowJs": true,
10
-
11
- // Bundler mode
12
- "moduleResolution": "bundler",
13
- "allowImportingTsExtensions": true,
14
- "verbatimModuleSyntax": true,
15
- "noEmit": true,
16
-
17
- // Best practices
18
- "strict": true,
19
- "skipLibCheck": true,
20
- "noFallthroughCasesInSwitch": true,
21
- "noUncheckedIndexedAccess": true,
22
- "noImplicitOverride": true,
23
-
24
- // Some stricter flags (disabled by default)
25
- "noUnusedLocals": false,
26
- "noUnusedParameters": false,
27
- "noPropertyAccessFromIndexSignature": false
28
- }
29
- }