payid-rule-engine 0.3.1 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +28 -0
- package/dist/index.js +174 -0
- package/package.json +14 -7
- package/index.ts +0 -1
- package/src/index.ts +0 -22
- package/src/preprocess.ts +0 -48
- package/src/sandbox.ts +0 -55
- package/src/tsSandbox.ts +0 -259
- package/src/validateRequires.ts +0 -15
- package/src/wasm/rule_engine.wasm +0 -0
- package/src/wasm.ts +0 -72
- package/tsconfig.json +0 -29
- package/tsup.config.ts +0 -11
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { RuleContext, RuleResult } from 'payid-types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Override URL tempat WASM di-fetch di browser.
|
|
5
|
+
* Panggil sebelum createPayID() / createPayIDServer():
|
|
6
|
+
*
|
|
7
|
+
* import { setWasmUrl } from 'payid-rule-engine';
|
|
8
|
+
* setWasmUrl('https://gateway.pinata.cloud/ipfs/YOUR_CID');
|
|
9
|
+
*/
|
|
10
|
+
declare function setWasmUrl(url: string): void;
|
|
11
|
+
declare function loadWasm(binary?: Buffer | Uint8Array): Promise<WebAssembly.Instance>;
|
|
12
|
+
|
|
13
|
+
declare function runWasmRule(context: RuleContext, config: any, wasmBinary?: Buffer | Uint8Array): Promise<RuleResult>;
|
|
14
|
+
|
|
15
|
+
declare function preprocessContextV2(context: any, ruleConfig: any, trustedIssuers: Set<string>): any;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Execute PAY.ID WASM rule engine.
|
|
19
|
+
*
|
|
20
|
+
* @param wasmBinary Compiled WASM binary
|
|
21
|
+
* @param context Rule execution context
|
|
22
|
+
* @param ruleConfig Rule configuration JSON
|
|
23
|
+
*
|
|
24
|
+
* @returns RuleResult (ALLOW / REJECT)
|
|
25
|
+
*/
|
|
26
|
+
declare function executeRule(context: RuleContext, ruleConfig: unknown, wasmBinary: Buffer): Promise<RuleResult>;
|
|
27
|
+
|
|
28
|
+
export { executeRule, loadWasm, preprocessContextV2, runWasmRule, setWasmUrl };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// src/wasm.ts
|
|
2
|
+
var _wasmUrl = "https://gateway.pinata.cloud/ipfs/bafkreigwfxsb7oot7v55x7vxslvj23csxl2fhk2w7hsnboe55o26s2mgfy";
|
|
3
|
+
var _instance = null;
|
|
4
|
+
var _loading = null;
|
|
5
|
+
function setWasmUrl(url) {
|
|
6
|
+
if (url === _wasmUrl) return;
|
|
7
|
+
_wasmUrl = url;
|
|
8
|
+
_instance = null;
|
|
9
|
+
_loading = null;
|
|
10
|
+
}
|
|
11
|
+
var wasiStub = {
|
|
12
|
+
fd_write: () => 8,
|
|
13
|
+
fd_read: () => 8,
|
|
14
|
+
fd_close: () => 0,
|
|
15
|
+
fd_seek: () => 8,
|
|
16
|
+
fd_fdstat_get: () => 8,
|
|
17
|
+
fd_prestat_get: () => 8,
|
|
18
|
+
fd_prestat_dir_name: () => 8,
|
|
19
|
+
environ_get: () => 0,
|
|
20
|
+
environ_sizes_get: () => 0,
|
|
21
|
+
args_get: () => 0,
|
|
22
|
+
args_sizes_get: () => 0,
|
|
23
|
+
clock_time_get: () => 0,
|
|
24
|
+
proc_exit: () => {
|
|
25
|
+
},
|
|
26
|
+
random_get: () => 0
|
|
27
|
+
};
|
|
28
|
+
async function compile(binary) {
|
|
29
|
+
const module = await WebAssembly.compile(binary);
|
|
30
|
+
const instance = await WebAssembly.instantiate(module, {
|
|
31
|
+
wasi_snapshot_preview1: wasiStub
|
|
32
|
+
});
|
|
33
|
+
const _init = instance.exports._initialize;
|
|
34
|
+
if (_init) _init();
|
|
35
|
+
return instance;
|
|
36
|
+
}
|
|
37
|
+
async function loadWasm(binary) {
|
|
38
|
+
if (binary && binary.length > 0) {
|
|
39
|
+
return compile(binary instanceof Uint8Array ? binary : new Uint8Array(binary));
|
|
40
|
+
}
|
|
41
|
+
if (_instance) return _instance;
|
|
42
|
+
if (_loading) return _loading;
|
|
43
|
+
_loading = (async () => {
|
|
44
|
+
let wasmBinary;
|
|
45
|
+
if (typeof process !== "undefined" && process.versions?.node) {
|
|
46
|
+
const { readFileSync } = await import(
|
|
47
|
+
/* webpackIgnore: true */
|
|
48
|
+
"fs"
|
|
49
|
+
);
|
|
50
|
+
const { join, dirname } = await import(
|
|
51
|
+
/* webpackIgnore: true */
|
|
52
|
+
"path"
|
|
53
|
+
);
|
|
54
|
+
const { fileURLToPath } = await import(
|
|
55
|
+
/* webpackIgnore: true */
|
|
56
|
+
"url"
|
|
57
|
+
);
|
|
58
|
+
const __dir = dirname(fileURLToPath(import.meta.url));
|
|
59
|
+
const buf = readFileSync(join(__dir, "wasm/rule_engine.wasm"));
|
|
60
|
+
wasmBinary = new Uint8Array(buf);
|
|
61
|
+
} else {
|
|
62
|
+
const res = await fetch(_wasmUrl);
|
|
63
|
+
if (!res.ok) throw new Error(
|
|
64
|
+
`[PAY.ID] Failed to fetch WASM from "${_wasmUrl}": HTTP ${res.status}`
|
|
65
|
+
);
|
|
66
|
+
wasmBinary = new Uint8Array(await res.arrayBuffer());
|
|
67
|
+
}
|
|
68
|
+
const instance = await compile(wasmBinary);
|
|
69
|
+
_instance = instance;
|
|
70
|
+
_loading = null;
|
|
71
|
+
return instance;
|
|
72
|
+
})();
|
|
73
|
+
return _loading;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/sandbox.ts
|
|
77
|
+
var enc = new TextEncoder();
|
|
78
|
+
var dec = new TextDecoder();
|
|
79
|
+
async function runWasmRule(context, config, wasmBinary) {
|
|
80
|
+
const instance = await loadWasm(wasmBinary);
|
|
81
|
+
const memory = instance.exports.memory;
|
|
82
|
+
const alloc = instance.exports.alloc;
|
|
83
|
+
const free_ = instance.exports.free;
|
|
84
|
+
const evaluate = instance.exports.evaluate;
|
|
85
|
+
if (!alloc || !evaluate) {
|
|
86
|
+
throw new Error(`WASM missing exports: alloc=${!!alloc} evaluate=${!!evaluate}`);
|
|
87
|
+
}
|
|
88
|
+
const ctxBuf = enc.encode(JSON.stringify(context));
|
|
89
|
+
const cfgBuf = enc.encode(JSON.stringify(config));
|
|
90
|
+
const OUT_SIZE = 4096;
|
|
91
|
+
const ctxPtr = alloc(ctxBuf.length);
|
|
92
|
+
const cfgPtr = alloc(cfgBuf.length);
|
|
93
|
+
const outPtr = alloc(OUT_SIZE);
|
|
94
|
+
new Uint8Array(memory.buffer).set(ctxBuf, ctxPtr);
|
|
95
|
+
new Uint8Array(memory.buffer).set(cfgBuf, cfgPtr);
|
|
96
|
+
let rc;
|
|
97
|
+
try {
|
|
98
|
+
rc = evaluate(ctxPtr, ctxBuf.length, cfgPtr, cfgBuf.length, outPtr, OUT_SIZE);
|
|
99
|
+
} catch (err) {
|
|
100
|
+
throw new Error(`WASM evaluate threw: ${err}`);
|
|
101
|
+
}
|
|
102
|
+
if (rc < 0) throw new Error(`WASM evaluate failed rc=${rc}`);
|
|
103
|
+
const out = new Uint8Array(memory.buffer).slice(outPtr, outPtr + rc);
|
|
104
|
+
const result = JSON.parse(dec.decode(out));
|
|
105
|
+
if (free_) {
|
|
106
|
+
free_(ctxPtr, ctxBuf.length);
|
|
107
|
+
free_(cfgPtr, cfgBuf.length);
|
|
108
|
+
free_(outPtr, OUT_SIZE);
|
|
109
|
+
}
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// src/validateRequires.ts
|
|
114
|
+
import { get } from "lodash";
|
|
115
|
+
function validateRequiredContext(context, requires) {
|
|
116
|
+
if (!requires) return;
|
|
117
|
+
for (const path of requires) {
|
|
118
|
+
const value = get(context, path);
|
|
119
|
+
if (value === void 0 || value === null) {
|
|
120
|
+
throw new Error(`Missing required context field: ${path}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// src/preprocess.ts
|
|
126
|
+
import { verifyAttestation } from "payid-attestation";
|
|
127
|
+
function preprocessContextV2(context, ruleConfig, trustedIssuers) {
|
|
128
|
+
validateRequiredContext(context, ruleConfig.requires);
|
|
129
|
+
if (context.env) {
|
|
130
|
+
verifyAttestation(
|
|
131
|
+
{ timestamp: context.env.timestamp },
|
|
132
|
+
context.env.proof,
|
|
133
|
+
trustedIssuers
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
if (context.state) {
|
|
137
|
+
verifyAttestation(
|
|
138
|
+
{
|
|
139
|
+
spentToday: context.state.spentToday,
|
|
140
|
+
period: context.state.period
|
|
141
|
+
},
|
|
142
|
+
context.state.proof,
|
|
143
|
+
trustedIssuers
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
if (context.oracle) {
|
|
147
|
+
const { proof, ...data } = context.oracle;
|
|
148
|
+
verifyAttestation(data, proof, trustedIssuers);
|
|
149
|
+
}
|
|
150
|
+
if (context.risk) {
|
|
151
|
+
verifyAttestation(
|
|
152
|
+
{
|
|
153
|
+
score: context.risk.score,
|
|
154
|
+
category: context.risk.category,
|
|
155
|
+
modelHash: context.risk.proof.modelHash
|
|
156
|
+
},
|
|
157
|
+
context.risk.proof,
|
|
158
|
+
trustedIssuers
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
return context;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// src/index.ts
|
|
165
|
+
async function executeRule(context, ruleConfig, wasmBinary) {
|
|
166
|
+
return runWasmRule(context, ruleConfig, wasmBinary);
|
|
167
|
+
}
|
|
168
|
+
export {
|
|
169
|
+
executeRule,
|
|
170
|
+
loadWasm,
|
|
171
|
+
preprocessContextV2,
|
|
172
|
+
runWasmRule,
|
|
173
|
+
setWasmUrl
|
|
174
|
+
};
|
package/package.json
CHANGED
|
@@ -1,22 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payid-rule-engine",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
6
17
|
"scripts": {
|
|
7
|
-
"buildOld": "tsup src/* --format esm --dts --clean",
|
|
8
18
|
"build": "tsup"
|
|
9
19
|
},
|
|
10
20
|
"dependencies": {
|
|
11
21
|
"@types/lodash": "^4.17.21",
|
|
12
22
|
"lodash": "^4.17.21",
|
|
13
|
-
"payid-attestation": "^0.1.
|
|
14
|
-
"payid-types": "^0.2.
|
|
23
|
+
"payid-attestation": "^0.1.6",
|
|
24
|
+
"payid-types": "^0.2.2",
|
|
15
25
|
"wasi": "^0.0.6"
|
|
16
26
|
},
|
|
17
|
-
"devDependencies": {
|
|
18
|
-
"@types/bun": "latest"
|
|
19
|
-
},
|
|
20
27
|
"peerDependencies": {
|
|
21
28
|
"typescript": "^5"
|
|
22
29
|
}
|
package/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "./src";
|
package/src/index.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import type { RuleContext, RuleResult } from "payid-types";
|
|
2
|
-
import { runWasmRule } from "./sandbox";
|
|
3
|
-
export * from "./wasm";
|
|
4
|
-
export * from "./sandbox";
|
|
5
|
-
export * from "./preprocess";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Execute PAY.ID WASM rule engine.
|
|
9
|
-
*
|
|
10
|
-
* @param wasmBinary Compiled WASM binary
|
|
11
|
-
* @param context Rule execution context
|
|
12
|
-
* @param ruleConfig Rule configuration JSON
|
|
13
|
-
*
|
|
14
|
-
* @returns RuleResult (ALLOW / REJECT)
|
|
15
|
-
*/
|
|
16
|
-
export async function executeRule(
|
|
17
|
-
context: RuleContext,
|
|
18
|
-
ruleConfig: unknown,
|
|
19
|
-
wasmBinary: Buffer,
|
|
20
|
-
): Promise<RuleResult> {
|
|
21
|
-
return runWasmRule(context, ruleConfig, wasmBinary,);
|
|
22
|
-
}
|
package/src/preprocess.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { validateRequiredContext } from "./validateRequires";
|
|
2
|
-
import { verifyAttestation } from "payid-attestation";
|
|
3
|
-
|
|
4
|
-
export function preprocessContextV2(
|
|
5
|
-
context: any,
|
|
6
|
-
ruleConfig: any,
|
|
7
|
-
trustedIssuers: Set<string>
|
|
8
|
-
) {
|
|
9
|
-
validateRequiredContext(context, ruleConfig.requires);
|
|
10
|
-
|
|
11
|
-
if (context.env) {
|
|
12
|
-
verifyAttestation(
|
|
13
|
-
{ timestamp: context.env.timestamp },
|
|
14
|
-
context.env.proof,
|
|
15
|
-
trustedIssuers
|
|
16
|
-
);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
if (context.state) {
|
|
20
|
-
verifyAttestation(
|
|
21
|
-
{
|
|
22
|
-
spentToday: context.state.spentToday,
|
|
23
|
-
period: context.state.period
|
|
24
|
-
},
|
|
25
|
-
context.state.proof,
|
|
26
|
-
trustedIssuers
|
|
27
|
-
);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (context.oracle) {
|
|
31
|
-
const { proof, ...data } = context.oracle;
|
|
32
|
-
verifyAttestation(data, proof, trustedIssuers);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
if (context.risk) {
|
|
36
|
-
verifyAttestation(
|
|
37
|
-
{
|
|
38
|
-
score: context.risk.score,
|
|
39
|
-
category: context.risk.category,
|
|
40
|
-
modelHash: context.risk.proof.modelHash
|
|
41
|
-
},
|
|
42
|
-
context.risk.proof,
|
|
43
|
-
trustedIssuers
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return context;
|
|
48
|
-
}
|
package/src/sandbox.ts
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import type { RuleContext, RuleResult } from "payid-types";
|
|
2
|
-
import { loadWasm } from "./wasm";
|
|
3
|
-
|
|
4
|
-
const enc = new TextEncoder();
|
|
5
|
-
const dec = new TextDecoder();
|
|
6
|
-
|
|
7
|
-
export async function runWasmRule(
|
|
8
|
-
context: RuleContext,
|
|
9
|
-
config: any,
|
|
10
|
-
wasmBinary?: Buffer | Uint8Array,
|
|
11
|
-
): Promise<RuleResult> {
|
|
12
|
-
const instance = await loadWasm(wasmBinary);
|
|
13
|
-
|
|
14
|
-
const memory = instance.exports.memory as WebAssembly.Memory;
|
|
15
|
-
const alloc = instance.exports.alloc as ((size: number) => number) | undefined;
|
|
16
|
-
const free_ = instance.exports.free as ((ptr: number, size: number) => void) | undefined;
|
|
17
|
-
const evaluate = instance.exports.evaluate as (
|
|
18
|
-
a: number, b: number, c: number, d: number, e: number, f: number
|
|
19
|
-
) => number;
|
|
20
|
-
|
|
21
|
-
if (!alloc || !evaluate) {
|
|
22
|
-
throw new Error(`WASM missing exports: alloc=${!!alloc} evaluate=${!!evaluate}`);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const ctxBuf = enc.encode(JSON.stringify(context));
|
|
26
|
-
const cfgBuf = enc.encode(JSON.stringify(config));
|
|
27
|
-
const OUT_SIZE = 4096;
|
|
28
|
-
|
|
29
|
-
const ctxPtr = alloc(ctxBuf.length);
|
|
30
|
-
const cfgPtr = alloc(cfgBuf.length);
|
|
31
|
-
const outPtr = alloc(OUT_SIZE);
|
|
32
|
-
|
|
33
|
-
new Uint8Array(memory.buffer).set(ctxBuf, ctxPtr);
|
|
34
|
-
new Uint8Array(memory.buffer).set(cfgBuf, cfgPtr);
|
|
35
|
-
|
|
36
|
-
let rc: number;
|
|
37
|
-
try {
|
|
38
|
-
rc = evaluate(ctxPtr, ctxBuf.length, cfgPtr, cfgBuf.length, outPtr, OUT_SIZE);
|
|
39
|
-
} catch (err) {
|
|
40
|
-
throw new Error(`WASM evaluate threw: ${err}`);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (rc < 0) throw new Error(`WASM evaluate failed rc=${rc}`);
|
|
44
|
-
|
|
45
|
-
const out = new Uint8Array(memory.buffer).slice(outPtr, outPtr + rc);
|
|
46
|
-
const result = JSON.parse(dec.decode(out));
|
|
47
|
-
|
|
48
|
-
if (free_) {
|
|
49
|
-
free_(ctxPtr, ctxBuf.length);
|
|
50
|
-
free_(cfgPtr, cfgBuf.length);
|
|
51
|
-
free_(outPtr, OUT_SIZE);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return result;
|
|
55
|
-
}
|
package/src/tsSandbox.ts
DELETED
|
@@ -1,259 +0,0 @@
|
|
|
1
|
-
// sandbox.ts — Pure TypeScript rule engine (no WASM)
|
|
2
|
-
//
|
|
3
|
-
// Tidak butuh compile Rust, tidak butuh WASI.
|
|
4
|
-
|
|
5
|
-
import type { RuleContext, RuleResult } from "payid-types";
|
|
6
|
-
|
|
7
|
-
export async function runWasmRule(
|
|
8
|
-
_wasmBinary: Buffer,
|
|
9
|
-
context: RuleContext,
|
|
10
|
-
config: any
|
|
11
|
-
): Promise<RuleResult> {
|
|
12
|
-
return evaluateRule(context, config);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// Core evaluation ─
|
|
16
|
-
|
|
17
|
-
function evaluateRule(context: any, config: any): RuleResult {
|
|
18
|
-
const rules: any[] = config?.rules;
|
|
19
|
-
if (!Array.isArray(rules) || rules.length === 0) {
|
|
20
|
-
return { decision: "ALLOW", code: "NO_RULES", reason: "no rules defined" };
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const logic: string = config?.logic ?? "AND";
|
|
24
|
-
return evalRules(context, rules, logic);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function evalRules(context: any, rules: any[], logic: string): RuleResult {
|
|
28
|
-
for (const rule of rules) {
|
|
29
|
-
const res = evalOneRule(context, rule);
|
|
30
|
-
if (res.decision === "REJECT" && logic === "AND") return res;
|
|
31
|
-
if (res.decision === "ALLOW" && logic === "OR") return res;
|
|
32
|
-
}
|
|
33
|
-
if (logic === "AND") return { decision: "ALLOW", code: "OK", reason: "all rules passed" };
|
|
34
|
-
return { decision: "REJECT", code: "NO_RULE_MATCH", reason: "no rule matched in OR group" };
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function evalOneRule(context: any, rule: any): RuleResult {
|
|
38
|
-
const ruleId = rule?.id ?? "UNKNOWN_RULE";
|
|
39
|
-
const message = rule?.message ?? "";
|
|
40
|
-
|
|
41
|
-
// Format C: nested rules
|
|
42
|
-
if (Array.isArray(rule?.rules)) {
|
|
43
|
-
const subLogic = rule?.logic ?? "AND";
|
|
44
|
-
const res = evalRules(context, rule.rules, subLogic);
|
|
45
|
-
if (res.decision === "REJECT" && message) {
|
|
46
|
-
return { decision: "REJECT", code: ruleId, reason: message };
|
|
47
|
-
}
|
|
48
|
-
return res;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Format B: multi-condition
|
|
52
|
-
if (Array.isArray(rule?.conditions)) {
|
|
53
|
-
const inner = rule?.logic ?? "AND";
|
|
54
|
-
for (const cond of rule.conditions) {
|
|
55
|
-
const passed = evalCondition(context, cond);
|
|
56
|
-
if (!passed && inner === "AND") {
|
|
57
|
-
const reason = message || cond?.field || ruleId;
|
|
58
|
-
return { decision: "REJECT", code: ruleId, reason };
|
|
59
|
-
}
|
|
60
|
-
if (passed && inner === "OR") {
|
|
61
|
-
return { decision: "ALLOW", code: ruleId };
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
if (inner === "AND") return { decision: "ALLOW", code: ruleId };
|
|
65
|
-
return { decision: "REJECT", code: ruleId, reason: message || "no condition matched in OR" };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Format A: single if
|
|
69
|
-
if (rule?.if !== undefined) {
|
|
70
|
-
const passed = evalCondition(context, rule.if);
|
|
71
|
-
if (!passed) {
|
|
72
|
-
const reason = message || rule.if?.field || ruleId;
|
|
73
|
-
return {
|
|
74
|
-
decision: "REJECT",
|
|
75
|
-
code: ruleId,
|
|
76
|
-
reason: interpolate(reason, context)
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
return { decision: "ALLOW", code: ruleId };
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return { decision: "REJECT", code: ruleId, reason: "rule has no evaluable condition" };
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Condition evaluation
|
|
86
|
-
|
|
87
|
-
function evalCondition(context: any, cond: any): boolean {
|
|
88
|
-
const fieldExpr: string = cond?.field;
|
|
89
|
-
const op: string = cond?.op;
|
|
90
|
-
if (!fieldExpr || !op) return false;
|
|
91
|
-
|
|
92
|
-
const baseField = splitTransform(fieldExpr)[0];
|
|
93
|
-
|
|
94
|
-
if (op === "exists") return resolveField(context, baseField) !== undefined;
|
|
95
|
-
if (op === "not_exists") return resolveField(context, baseField) === undefined;
|
|
96
|
-
|
|
97
|
-
const actualRaw = resolveField(context, baseField);
|
|
98
|
-
if (actualRaw === undefined) return false;
|
|
99
|
-
const actual = applyTransform(actualRaw, fieldExpr);
|
|
100
|
-
|
|
101
|
-
// Cross-field reference
|
|
102
|
-
let expected = cond.value;
|
|
103
|
-
if (typeof expected === "string" && expected.startsWith("$")) {
|
|
104
|
-
const refField = expected.slice(1);
|
|
105
|
-
const refBase = splitTransform(refField)[0];
|
|
106
|
-
const refRaw = resolveField(context, refBase);
|
|
107
|
-
if (refRaw === undefined) return false;
|
|
108
|
-
expected = applyTransform(refRaw, refField);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return applyOp(actual, op, expected);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Field resolution
|
|
115
|
-
|
|
116
|
-
function resolveField(ctx: any, path: string): any {
|
|
117
|
-
const base = splitTransform(path)[0];
|
|
118
|
-
return base.split(".").reduce((o: any, k: string) => o?.[k], ctx);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function splitTransform(expr: string): [string, string | null] {
|
|
122
|
-
const i = expr.indexOf("|");
|
|
123
|
-
if (i === -1) return [expr, null];
|
|
124
|
-
return [expr.slice(0, i), expr.slice(i + 1)];
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Field transforms
|
|
128
|
-
|
|
129
|
-
function applyTransform(val: any, expr: string): any {
|
|
130
|
-
const transform = splitTransform(expr)[1];
|
|
131
|
-
if (!transform) return val;
|
|
132
|
-
|
|
133
|
-
const colonIdx = transform.indexOf(":");
|
|
134
|
-
const name = colonIdx === -1 ? transform : transform.slice(0, colonIdx);
|
|
135
|
-
const arg = colonIdx === -1 ? null : transform.slice(colonIdx + 1);
|
|
136
|
-
|
|
137
|
-
const n = toU128(val);
|
|
138
|
-
|
|
139
|
-
switch (name) {
|
|
140
|
-
case "div": {
|
|
141
|
-
if (n === null) return val;
|
|
142
|
-
const d = arg ? BigInt(arg) : 1n;
|
|
143
|
-
if (d === 0n) return val;
|
|
144
|
-
return Number(n / d);
|
|
145
|
-
}
|
|
146
|
-
case "mod": {
|
|
147
|
-
if (n === null) return val;
|
|
148
|
-
const m = arg ? BigInt(arg) : 1n;
|
|
149
|
-
if (m === 0n) return val;
|
|
150
|
-
return Number(n % m);
|
|
151
|
-
}
|
|
152
|
-
case "abs": return n !== null ? Number(n < 0n ? -n : n) : val;
|
|
153
|
-
case "hour": return n !== null ? Number((n % 86400n) / 3600n) : val;
|
|
154
|
-
case "day": return n !== null ? Number((n / 86400n + 4n) % 7n) : val;
|
|
155
|
-
case "date": return n !== null ? dayOfMonth(Number(n / 86400n)) : val;
|
|
156
|
-
case "month": return n !== null ? monthOfYear(Number(n / 86400n)) : val;
|
|
157
|
-
case "len": return String(val).length;
|
|
158
|
-
case "lower": return String(val).toLowerCase();
|
|
159
|
-
case "upper": return String(val).toUpperCase();
|
|
160
|
-
default: return val;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function toU128(v: any): bigint | null {
|
|
165
|
-
try {
|
|
166
|
-
if (typeof v === "bigint") return v;
|
|
167
|
-
if (typeof v === "number") return BigInt(Math.trunc(v));
|
|
168
|
-
if (typeof v === "string" && v !== "") return BigInt(v);
|
|
169
|
-
return null;
|
|
170
|
-
} catch { return null; }
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Gregorian calendar
|
|
174
|
-
|
|
175
|
-
function isLeap(y: number): boolean { return (y % 4 === 0 && y % 100 !== 0) || y % 400 === 0; }
|
|
176
|
-
|
|
177
|
-
function daysToYMD(days: number): [number, number, number] {
|
|
178
|
-
let y = 1970;
|
|
179
|
-
while (true) { const dy = isLeap(y) ? 366 : 365; if (days < dy) break; days -= dy; y++; }
|
|
180
|
-
const months = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
181
|
-
if (isLeap(y)) months[2] = 29;
|
|
182
|
-
let m = 1;
|
|
183
|
-
while (true) { if (days < months[m]!) break; days -= months[m]!; m++; }
|
|
184
|
-
return [y, m, days + 1];
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function dayOfMonth(days: number): number { return daysToYMD(days)[2]; }
|
|
188
|
-
function monthOfYear(days: number): number { return daysToYMD(days)[1]; }
|
|
189
|
-
|
|
190
|
-
// Operator dispatch
|
|
191
|
-
|
|
192
|
-
function applyOp(actual: any, op: string, expected: any): boolean {
|
|
193
|
-
const a = toU128(actual);
|
|
194
|
-
const b = toU128(expected);
|
|
195
|
-
|
|
196
|
-
switch (op) {
|
|
197
|
-
case ">=": return a !== null && b !== null && a >= b;
|
|
198
|
-
case "<=": return a !== null && b !== null && a <= b;
|
|
199
|
-
case ">": return a !== null && b !== null && a > b;
|
|
200
|
-
case "<": return a !== null && b !== null && a < b;
|
|
201
|
-
|
|
202
|
-
case "==": return String(actual) === String(expected) || actual == expected;
|
|
203
|
-
case "!=": return String(actual) !== String(expected) && actual != expected;
|
|
204
|
-
|
|
205
|
-
case "in": return Array.isArray(expected) && expected.some(e => looseEq(actual, e));
|
|
206
|
-
case "not_in": return Array.isArray(expected) && !expected.some(e => looseEq(actual, e));
|
|
207
|
-
|
|
208
|
-
case "between":
|
|
209
|
-
return Array.isArray(expected) && expected.length === 2 && a !== null
|
|
210
|
-
&& toU128(expected[0]) !== null && toU128(expected[1]) !== null
|
|
211
|
-
&& a >= toU128(expected[0])! && a <= toU128(expected[1])!;
|
|
212
|
-
|
|
213
|
-
case "not_between":
|
|
214
|
-
return Array.isArray(expected) && expected.length === 2 && a !== null
|
|
215
|
-
&& (a < toU128(expected[0])! || a > toU128(expected[1])!);
|
|
216
|
-
|
|
217
|
-
case "mod_eq":
|
|
218
|
-
return Array.isArray(expected) && expected.length === 2 && a !== null
|
|
219
|
-
&& toU128(expected[0]) !== null && toU128(expected[0])! > 0n
|
|
220
|
-
&& a % toU128(expected[0])! === toU128(expected[1])!;
|
|
221
|
-
|
|
222
|
-
case "mod_ne":
|
|
223
|
-
return Array.isArray(expected) && expected.length === 2 && a !== null
|
|
224
|
-
&& toU128(expected[0]) !== null && toU128(expected[0])! > 0n
|
|
225
|
-
&& a % toU128(expected[0])! !== toU128(expected[1])!;
|
|
226
|
-
|
|
227
|
-
case "contains": return typeof actual === "string" && typeof expected === "string" && actual.includes(expected);
|
|
228
|
-
case "not_contains": return typeof actual === "string" && typeof expected === "string" && !actual.includes(expected);
|
|
229
|
-
case "starts_with": return typeof actual === "string" && typeof expected === "string" && actual.startsWith(expected);
|
|
230
|
-
case "ends_with": return typeof actual === "string" && typeof expected === "string" && actual.endsWith(expected);
|
|
231
|
-
|
|
232
|
-
case "exists": return actual !== undefined && actual !== null;
|
|
233
|
-
case "not_exists": return actual === undefined || actual === null;
|
|
234
|
-
|
|
235
|
-
case "regex": return typeof actual === "string" && typeof expected === "string" && new RegExp(expected).test(actual);
|
|
236
|
-
case "not_regex": return typeof actual === "string" && typeof expected === "string" && !new RegExp(expected).test(actual);
|
|
237
|
-
|
|
238
|
-
default: return false;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function looseEq(a: any, b: any): boolean {
|
|
243
|
-
if (a == b) return true;
|
|
244
|
-
if (String(a) === String(b)) return true;
|
|
245
|
-
const ba = toU128(a), bb = toU128(b);
|
|
246
|
-
return ba !== null && bb !== null && ba === bb;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// Message interpolation
|
|
250
|
-
|
|
251
|
-
function interpolate(template: string, context: any): string {
|
|
252
|
-
return template.replace(/\{([^}]+)\}/g, (_, key) => {
|
|
253
|
-
const base = splitTransform(key)[0];
|
|
254
|
-
const raw = resolveField(context, base);
|
|
255
|
-
if (raw === undefined) return `{${key}}`;
|
|
256
|
-
const val = applyTransform(raw, key);
|
|
257
|
-
return String(val);
|
|
258
|
-
});
|
|
259
|
-
}
|
package/src/validateRequires.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { get } from "lodash";
|
|
2
|
-
|
|
3
|
-
export function validateRequiredContext(
|
|
4
|
-
context: any,
|
|
5
|
-
requires?: string[]
|
|
6
|
-
) {
|
|
7
|
-
if (!requires) return;
|
|
8
|
-
|
|
9
|
-
for (const path of requires) {
|
|
10
|
-
const value = get(context, path);
|
|
11
|
-
if (value === undefined || value === null) {
|
|
12
|
-
throw new Error(`Missing required context field: ${path}`);
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
}
|
|
Binary file
|
package/src/wasm.ts
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
let _wasmUrl = 'https://gateway.pinata.cloud/ipfs/bafkreigwfxsb7oot7v55x7vxslvj23csxl2fhk2w7hsnboe55o26s2mgfy';
|
|
2
|
-
|
|
3
|
-
let _instance: WebAssembly.Instance | null = null;
|
|
4
|
-
let _loading: Promise<WebAssembly.Instance> | null = null;
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Override URL tempat WASM di-fetch di browser.
|
|
8
|
-
* Panggil sebelum createPayID() / createPayIDServer():
|
|
9
|
-
*
|
|
10
|
-
* import { setWasmUrl } from 'payid-rule-engine';
|
|
11
|
-
* setWasmUrl('https://gateway.pinata.cloud/ipfs/YOUR_CID');
|
|
12
|
-
*/
|
|
13
|
-
export function setWasmUrl(url: string) {
|
|
14
|
-
if (url === _wasmUrl) return;
|
|
15
|
-
_wasmUrl = url;
|
|
16
|
-
_instance = null;
|
|
17
|
-
_loading = null;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const wasiStub: Record<string, (...args: any[]) => any> = {
|
|
21
|
-
fd_write: () => 8, fd_read: () => 8, fd_close: () => 0,
|
|
22
|
-
fd_seek: () => 8, fd_fdstat_get: () => 8, fd_prestat_get: () => 8,
|
|
23
|
-
fd_prestat_dir_name: () => 8, environ_get: () => 0,
|
|
24
|
-
environ_sizes_get: () => 0, args_get: () => 0, args_sizes_get: () => 0,
|
|
25
|
-
clock_time_get: () => 0, proc_exit: () => { }, random_get: () => 0,
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
async function compile(binary: Uint8Array): Promise<WebAssembly.Instance> {
|
|
29
|
-
const module = await WebAssembly.compile(binary);
|
|
30
|
-
const instance = await WebAssembly.instantiate(module, {
|
|
31
|
-
wasi_snapshot_preview1: wasiStub,
|
|
32
|
-
});
|
|
33
|
-
const _init = instance.exports._initialize as (() => void) | undefined;
|
|
34
|
-
if (_init) _init();
|
|
35
|
-
return instance;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export async function loadWasm(binary?: Buffer | Uint8Array): Promise<WebAssembly.Instance> {
|
|
39
|
-
if (binary && binary.length > 0) {
|
|
40
|
-
return compile(binary instanceof Uint8Array ? binary : new Uint8Array(binary));
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (_instance) return _instance;
|
|
44
|
-
|
|
45
|
-
if (_loading) return _loading;
|
|
46
|
-
|
|
47
|
-
_loading = (async () => {
|
|
48
|
-
let wasmBinary: Uint8Array;
|
|
49
|
-
|
|
50
|
-
if (typeof process !== 'undefined' && process.versions?.node) {
|
|
51
|
-
const { readFileSync } = await import(/* webpackIgnore: true */ 'fs');
|
|
52
|
-
const { join, dirname } = await import(/* webpackIgnore: true */ 'path');
|
|
53
|
-
const { fileURLToPath } = await import(/* webpackIgnore: true */ 'url');
|
|
54
|
-
const __dir = dirname(fileURLToPath(import.meta.url));
|
|
55
|
-
const buf = readFileSync(join(__dir, 'wasm/rule_engine.wasm'));
|
|
56
|
-
wasmBinary = new Uint8Array(buf);
|
|
57
|
-
} else {
|
|
58
|
-
const res = await fetch(_wasmUrl);
|
|
59
|
-
if (!res.ok) throw new Error(
|
|
60
|
-
`[PAY.ID] Failed to fetch WASM from "${_wasmUrl}": HTTP ${res.status}`
|
|
61
|
-
);
|
|
62
|
-
wasmBinary = new Uint8Array(await res.arrayBuffer());
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const instance = await compile(wasmBinary);
|
|
66
|
-
_instance = instance;
|
|
67
|
-
_loading = null;
|
|
68
|
-
return instance;
|
|
69
|
-
})();
|
|
70
|
-
|
|
71
|
-
return _loading;
|
|
72
|
-
}
|
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
|
-
}
|