@oneaddress/setup 1.1.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/README.md +56 -0
- package/dist/index.js +3515 -0
- package/package.json +32 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3515 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
10
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
|
|
29
|
+
// ../../node_modules/sisteransi/src/index.js
|
|
30
|
+
var require_src = __commonJS({
|
|
31
|
+
"../../node_modules/sisteransi/src/index.js"(exports2, module2) {
|
|
32
|
+
"use strict";
|
|
33
|
+
var ESC = "\x1B";
|
|
34
|
+
var CSI = `${ESC}[`;
|
|
35
|
+
var beep = "\x07";
|
|
36
|
+
var cursor = {
|
|
37
|
+
to(x2, y3) {
|
|
38
|
+
if (!y3) return `${CSI}${x2 + 1}G`;
|
|
39
|
+
return `${CSI}${y3 + 1};${x2 + 1}H`;
|
|
40
|
+
},
|
|
41
|
+
move(x2, y3) {
|
|
42
|
+
let ret = "";
|
|
43
|
+
if (x2 < 0) ret += `${CSI}${-x2}D`;
|
|
44
|
+
else if (x2 > 0) ret += `${CSI}${x2}C`;
|
|
45
|
+
if (y3 < 0) ret += `${CSI}${-y3}A`;
|
|
46
|
+
else if (y3 > 0) ret += `${CSI}${y3}B`;
|
|
47
|
+
return ret;
|
|
48
|
+
},
|
|
49
|
+
up: (count = 1) => `${CSI}${count}A`,
|
|
50
|
+
down: (count = 1) => `${CSI}${count}B`,
|
|
51
|
+
forward: (count = 1) => `${CSI}${count}C`,
|
|
52
|
+
backward: (count = 1) => `${CSI}${count}D`,
|
|
53
|
+
nextLine: (count = 1) => `${CSI}E`.repeat(count),
|
|
54
|
+
prevLine: (count = 1) => `${CSI}F`.repeat(count),
|
|
55
|
+
left: `${CSI}G`,
|
|
56
|
+
hide: `${CSI}?25l`,
|
|
57
|
+
show: `${CSI}?25h`,
|
|
58
|
+
save: `${ESC}7`,
|
|
59
|
+
restore: `${ESC}8`
|
|
60
|
+
};
|
|
61
|
+
var scroll = {
|
|
62
|
+
up: (count = 1) => `${CSI}S`.repeat(count),
|
|
63
|
+
down: (count = 1) => `${CSI}T`.repeat(count)
|
|
64
|
+
};
|
|
65
|
+
var erase = {
|
|
66
|
+
screen: `${CSI}2J`,
|
|
67
|
+
up: (count = 1) => `${CSI}1J`.repeat(count),
|
|
68
|
+
down: (count = 1) => `${CSI}J`.repeat(count),
|
|
69
|
+
line: `${CSI}2K`,
|
|
70
|
+
lineEnd: `${CSI}K`,
|
|
71
|
+
lineStart: `${CSI}1K`,
|
|
72
|
+
lines(count) {
|
|
73
|
+
let clear = "";
|
|
74
|
+
for (let i = 0; i < count; i++)
|
|
75
|
+
clear += this.line + (i < count - 1 ? cursor.up() : "");
|
|
76
|
+
if (count)
|
|
77
|
+
clear += cursor.left;
|
|
78
|
+
return clear;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
module2.exports = { cursor, scroll, erase, beep };
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// ../../node_modules/picocolors/picocolors.js
|
|
86
|
+
var require_picocolors = __commonJS({
|
|
87
|
+
"../../node_modules/picocolors/picocolors.js"(exports2, module2) {
|
|
88
|
+
"use strict";
|
|
89
|
+
var p2 = process || {};
|
|
90
|
+
var argv = p2.argv || [];
|
|
91
|
+
var env = p2.env || {};
|
|
92
|
+
var isColorSupported = !(!!env.NO_COLOR || argv.includes("--no-color")) && (!!env.FORCE_COLOR || argv.includes("--color") || p2.platform === "win32" || (p2.stdout || {}).isTTY && env.TERM !== "dumb" || !!env.CI);
|
|
93
|
+
var formatter = (open, close, replace = open) => (input) => {
|
|
94
|
+
let string = "" + input, index = string.indexOf(close, open.length);
|
|
95
|
+
return ~index ? open + replaceClose(string, close, replace, index) + close : open + string + close;
|
|
96
|
+
};
|
|
97
|
+
var replaceClose = (string, close, replace, index) => {
|
|
98
|
+
let result = "", cursor = 0;
|
|
99
|
+
do {
|
|
100
|
+
result += string.substring(cursor, index) + replace;
|
|
101
|
+
cursor = index + close.length;
|
|
102
|
+
index = string.indexOf(close, cursor);
|
|
103
|
+
} while (~index);
|
|
104
|
+
return result + string.substring(cursor);
|
|
105
|
+
};
|
|
106
|
+
var createColors = (enabled = isColorSupported) => {
|
|
107
|
+
let f2 = enabled ? formatter : () => String;
|
|
108
|
+
return {
|
|
109
|
+
isColorSupported: enabled,
|
|
110
|
+
reset: f2("\x1B[0m", "\x1B[0m"),
|
|
111
|
+
bold: f2("\x1B[1m", "\x1B[22m", "\x1B[22m\x1B[1m"),
|
|
112
|
+
dim: f2("\x1B[2m", "\x1B[22m", "\x1B[22m\x1B[2m"),
|
|
113
|
+
italic: f2("\x1B[3m", "\x1B[23m"),
|
|
114
|
+
underline: f2("\x1B[4m", "\x1B[24m"),
|
|
115
|
+
inverse: f2("\x1B[7m", "\x1B[27m"),
|
|
116
|
+
hidden: f2("\x1B[8m", "\x1B[28m"),
|
|
117
|
+
strikethrough: f2("\x1B[9m", "\x1B[29m"),
|
|
118
|
+
black: f2("\x1B[30m", "\x1B[39m"),
|
|
119
|
+
red: f2("\x1B[31m", "\x1B[39m"),
|
|
120
|
+
green: f2("\x1B[32m", "\x1B[39m"),
|
|
121
|
+
yellow: f2("\x1B[33m", "\x1B[39m"),
|
|
122
|
+
blue: f2("\x1B[34m", "\x1B[39m"),
|
|
123
|
+
magenta: f2("\x1B[35m", "\x1B[39m"),
|
|
124
|
+
cyan: f2("\x1B[36m", "\x1B[39m"),
|
|
125
|
+
white: f2("\x1B[37m", "\x1B[39m"),
|
|
126
|
+
gray: f2("\x1B[90m", "\x1B[39m"),
|
|
127
|
+
bgBlack: f2("\x1B[40m", "\x1B[49m"),
|
|
128
|
+
bgRed: f2("\x1B[41m", "\x1B[49m"),
|
|
129
|
+
bgGreen: f2("\x1B[42m", "\x1B[49m"),
|
|
130
|
+
bgYellow: f2("\x1B[43m", "\x1B[49m"),
|
|
131
|
+
bgBlue: f2("\x1B[44m", "\x1B[49m"),
|
|
132
|
+
bgMagenta: f2("\x1B[45m", "\x1B[49m"),
|
|
133
|
+
bgCyan: f2("\x1B[46m", "\x1B[49m"),
|
|
134
|
+
bgWhite: f2("\x1B[47m", "\x1B[49m"),
|
|
135
|
+
blackBright: f2("\x1B[90m", "\x1B[39m"),
|
|
136
|
+
redBright: f2("\x1B[91m", "\x1B[39m"),
|
|
137
|
+
greenBright: f2("\x1B[92m", "\x1B[39m"),
|
|
138
|
+
yellowBright: f2("\x1B[93m", "\x1B[39m"),
|
|
139
|
+
blueBright: f2("\x1B[94m", "\x1B[39m"),
|
|
140
|
+
magentaBright: f2("\x1B[95m", "\x1B[39m"),
|
|
141
|
+
cyanBright: f2("\x1B[96m", "\x1B[39m"),
|
|
142
|
+
whiteBright: f2("\x1B[97m", "\x1B[39m"),
|
|
143
|
+
bgBlackBright: f2("\x1B[100m", "\x1B[49m"),
|
|
144
|
+
bgRedBright: f2("\x1B[101m", "\x1B[49m"),
|
|
145
|
+
bgGreenBright: f2("\x1B[102m", "\x1B[49m"),
|
|
146
|
+
bgYellowBright: f2("\x1B[103m", "\x1B[49m"),
|
|
147
|
+
bgBlueBright: f2("\x1B[104m", "\x1B[49m"),
|
|
148
|
+
bgMagentaBright: f2("\x1B[105m", "\x1B[49m"),
|
|
149
|
+
bgCyanBright: f2("\x1B[106m", "\x1B[49m"),
|
|
150
|
+
bgWhiteBright: f2("\x1B[107m", "\x1B[49m")
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
module2.exports = createColors();
|
|
154
|
+
module2.exports.createColors = createColors;
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// ../../node_modules/@clack/prompts/dist/index.mjs
|
|
159
|
+
var import_node_util = require("util");
|
|
160
|
+
|
|
161
|
+
// ../../node_modules/@clack/core/dist/index.mjs
|
|
162
|
+
var import_sisteransi = __toESM(require_src(), 1);
|
|
163
|
+
var import_node_process = require("process");
|
|
164
|
+
var f = __toESM(require("readline"), 1);
|
|
165
|
+
var import_node_readline = __toESM(require("readline"), 1);
|
|
166
|
+
var import_node_tty = require("tty");
|
|
167
|
+
var import_picocolors = __toESM(require_picocolors(), 1);
|
|
168
|
+
function J({ onlyFirst: t = false } = {}) {
|
|
169
|
+
const F = ["[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?(?:\\u0007|\\u001B\\u005C|\\u009C))", "(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))"].join("|");
|
|
170
|
+
return new RegExp(F, t ? void 0 : "g");
|
|
171
|
+
}
|
|
172
|
+
var Q = J();
|
|
173
|
+
function T(t) {
|
|
174
|
+
if (typeof t != "string") throw new TypeError(`Expected a \`string\`, got \`${typeof t}\``);
|
|
175
|
+
return t.replace(Q, "");
|
|
176
|
+
}
|
|
177
|
+
function O(t) {
|
|
178
|
+
return t && t.__esModule && Object.prototype.hasOwnProperty.call(t, "default") ? t.default : t;
|
|
179
|
+
}
|
|
180
|
+
var P = { exports: {} };
|
|
181
|
+
(function(t) {
|
|
182
|
+
var u2 = {};
|
|
183
|
+
t.exports = u2, u2.eastAsianWidth = function(e2) {
|
|
184
|
+
var s = e2.charCodeAt(0), i = e2.length == 2 ? e2.charCodeAt(1) : 0, D = s;
|
|
185
|
+
return 55296 <= s && s <= 56319 && 56320 <= i && i <= 57343 && (s &= 1023, i &= 1023, D = s << 10 | i, D += 65536), D == 12288 || 65281 <= D && D <= 65376 || 65504 <= D && D <= 65510 ? "F" : D == 8361 || 65377 <= D && D <= 65470 || 65474 <= D && D <= 65479 || 65482 <= D && D <= 65487 || 65490 <= D && D <= 65495 || 65498 <= D && D <= 65500 || 65512 <= D && D <= 65518 ? "H" : 4352 <= D && D <= 4447 || 4515 <= D && D <= 4519 || 4602 <= D && D <= 4607 || 9001 <= D && D <= 9002 || 11904 <= D && D <= 11929 || 11931 <= D && D <= 12019 || 12032 <= D && D <= 12245 || 12272 <= D && D <= 12283 || 12289 <= D && D <= 12350 || 12353 <= D && D <= 12438 || 12441 <= D && D <= 12543 || 12549 <= D && D <= 12589 || 12593 <= D && D <= 12686 || 12688 <= D && D <= 12730 || 12736 <= D && D <= 12771 || 12784 <= D && D <= 12830 || 12832 <= D && D <= 12871 || 12880 <= D && D <= 13054 || 13056 <= D && D <= 19903 || 19968 <= D && D <= 42124 || 42128 <= D && D <= 42182 || 43360 <= D && D <= 43388 || 44032 <= D && D <= 55203 || 55216 <= D && D <= 55238 || 55243 <= D && D <= 55291 || 63744 <= D && D <= 64255 || 65040 <= D && D <= 65049 || 65072 <= D && D <= 65106 || 65108 <= D && D <= 65126 || 65128 <= D && D <= 65131 || 110592 <= D && D <= 110593 || 127488 <= D && D <= 127490 || 127504 <= D && D <= 127546 || 127552 <= D && D <= 127560 || 127568 <= D && D <= 127569 || 131072 <= D && D <= 194367 || 177984 <= D && D <= 196605 || 196608 <= D && D <= 262141 ? "W" : 32 <= D && D <= 126 || 162 <= D && D <= 163 || 165 <= D && D <= 166 || D == 172 || D == 175 || 10214 <= D && D <= 10221 || 10629 <= D && D <= 10630 ? "Na" : D == 161 || D == 164 || 167 <= D && D <= 168 || D == 170 || 173 <= D && D <= 174 || 176 <= D && D <= 180 || 182 <= D && D <= 186 || 188 <= D && D <= 191 || D == 198 || D == 208 || 215 <= D && D <= 216 || 222 <= D && D <= 225 || D == 230 || 232 <= D && D <= 234 || 236 <= D && D <= 237 || D == 240 || 242 <= D && D <= 243 || 247 <= D && D <= 250 || D == 252 || D == 254 || D == 257 || D == 273 || D == 275 || D == 283 || 294 <= D && D <= 295 || D == 299 || 305 <= D && D <= 307 || D == 312 || 319 <= D && D <= 322 || D == 324 || 328 <= D && D <= 331 || D == 333 || 338 <= D && D <= 339 || 358 <= D && D <= 359 || D == 363 || D == 462 || D == 464 || D == 466 || D == 468 || D == 470 || D == 472 || D == 474 || D == 476 || D == 593 || D == 609 || D == 708 || D == 711 || 713 <= D && D <= 715 || D == 717 || D == 720 || 728 <= D && D <= 731 || D == 733 || D == 735 || 768 <= D && D <= 879 || 913 <= D && D <= 929 || 931 <= D && D <= 937 || 945 <= D && D <= 961 || 963 <= D && D <= 969 || D == 1025 || 1040 <= D && D <= 1103 || D == 1105 || D == 8208 || 8211 <= D && D <= 8214 || 8216 <= D && D <= 8217 || 8220 <= D && D <= 8221 || 8224 <= D && D <= 8226 || 8228 <= D && D <= 8231 || D == 8240 || 8242 <= D && D <= 8243 || D == 8245 || D == 8251 || D == 8254 || D == 8308 || D == 8319 || 8321 <= D && D <= 8324 || D == 8364 || D == 8451 || D == 8453 || D == 8457 || D == 8467 || D == 8470 || 8481 <= D && D <= 8482 || D == 8486 || D == 8491 || 8531 <= D && D <= 8532 || 8539 <= D && D <= 8542 || 8544 <= D && D <= 8555 || 8560 <= D && D <= 8569 || D == 8585 || 8592 <= D && D <= 8601 || 8632 <= D && D <= 8633 || D == 8658 || D == 8660 || D == 8679 || D == 8704 || 8706 <= D && D <= 8707 || 8711 <= D && D <= 8712 || D == 8715 || D == 8719 || D == 8721 || D == 8725 || D == 8730 || 8733 <= D && D <= 8736 || D == 8739 || D == 8741 || 8743 <= D && D <= 8748 || D == 8750 || 8756 <= D && D <= 8759 || 8764 <= D && D <= 8765 || D == 8776 || D == 8780 || D == 8786 || 8800 <= D && D <= 8801 || 8804 <= D && D <= 8807 || 8810 <= D && D <= 8811 || 8814 <= D && D <= 8815 || 8834 <= D && D <= 8835 || 8838 <= D && D <= 8839 || D == 8853 || D == 8857 || D == 8869 || D == 8895 || D == 8978 || 9312 <= D && D <= 9449 || 9451 <= D && D <= 9547 || 9552 <= D && D <= 9587 || 9600 <= D && D <= 9615 || 9618 <= D && D <= 9621 || 9632 <= D && D <= 9633 || 9635 <= D && D <= 9641 || 9650 <= D && D <= 9651 || 9654 <= D && D <= 9655 || 9660 <= D && D <= 9661 || 9664 <= D && D <= 9665 || 9670 <= D && D <= 9672 || D == 9675 || 9678 <= D && D <= 9681 || 9698 <= D && D <= 9701 || D == 9711 || 9733 <= D && D <= 9734 || D == 9737 || 9742 <= D && D <= 9743 || 9748 <= D && D <= 9749 || D == 9756 || D == 9758 || D == 9792 || D == 9794 || 9824 <= D && D <= 9825 || 9827 <= D && D <= 9829 || 9831 <= D && D <= 9834 || 9836 <= D && D <= 9837 || D == 9839 || 9886 <= D && D <= 9887 || 9918 <= D && D <= 9919 || 9924 <= D && D <= 9933 || 9935 <= D && D <= 9953 || D == 9955 || 9960 <= D && D <= 9983 || D == 10045 || D == 10071 || 10102 <= D && D <= 10111 || 11093 <= D && D <= 11097 || 12872 <= D && D <= 12879 || 57344 <= D && D <= 63743 || 65024 <= D && D <= 65039 || D == 65533 || 127232 <= D && D <= 127242 || 127248 <= D && D <= 127277 || 127280 <= D && D <= 127337 || 127344 <= D && D <= 127386 || 917760 <= D && D <= 917999 || 983040 <= D && D <= 1048573 || 1048576 <= D && D <= 1114109 ? "A" : "N";
|
|
186
|
+
}, u2.characterLength = function(e2) {
|
|
187
|
+
var s = this.eastAsianWidth(e2);
|
|
188
|
+
return s == "F" || s == "W" || s == "A" ? 2 : 1;
|
|
189
|
+
};
|
|
190
|
+
function F(e2) {
|
|
191
|
+
return e2.match(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[^\uD800-\uDFFF]/g) || [];
|
|
192
|
+
}
|
|
193
|
+
u2.length = function(e2) {
|
|
194
|
+
for (var s = F(e2), i = 0, D = 0; D < s.length; D++) i = i + this.characterLength(s[D]);
|
|
195
|
+
return i;
|
|
196
|
+
}, u2.slice = function(e2, s, i) {
|
|
197
|
+
textLen = u2.length(e2), s = s || 0, i = i || 1, s < 0 && (s = textLen + s), i < 0 && (i = textLen + i);
|
|
198
|
+
for (var D = "", C = 0, o = F(e2), E2 = 0; E2 < o.length; E2++) {
|
|
199
|
+
var a2 = o[E2], n = u2.length(a2);
|
|
200
|
+
if (C >= s - (n == 2 ? 1 : 0)) if (C + n <= i) D += a2;
|
|
201
|
+
else break;
|
|
202
|
+
C += n;
|
|
203
|
+
}
|
|
204
|
+
return D;
|
|
205
|
+
};
|
|
206
|
+
})(P);
|
|
207
|
+
var X = P.exports;
|
|
208
|
+
var DD = O(X);
|
|
209
|
+
var uD = function() {
|
|
210
|
+
return /\uD83C\uDFF4\uDB40\uDC67\uDB40\uDC62(?:\uDB40\uDC77\uDB40\uDC6C\uDB40\uDC73|\uDB40\uDC73\uDB40\uDC63\uDB40\uDC74|\uDB40\uDC65\uDB40\uDC6E\uDB40\uDC67)\uDB40\uDC7F|(?:\uD83E\uDDD1\uD83C\uDFFF\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFF\u200D\uD83E\uDD1D\u200D(?:\uD83D[\uDC68\uDC69]))(?:\uD83C[\uDFFB-\uDFFE])|(?:\uD83E\uDDD1\uD83C\uDFFE\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFE\u200D\uD83E\uDD1D\u200D(?:\uD83D[\uDC68\uDC69]))(?:\uD83C[\uDFFB-\uDFFD\uDFFF])|(?:\uD83E\uDDD1\uD83C\uDFFD\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFD\u200D\uD83E\uDD1D\u200D(?:\uD83D[\uDC68\uDC69]))(?:\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])|(?:\uD83E\uDDD1\uD83C\uDFFC\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFC\u200D\uD83E\uDD1D\u200D(?:\uD83D[\uDC68\uDC69]))(?:\uD83C[\uDFFB\uDFFD-\uDFFF])|(?:\uD83E\uDDD1\uD83C\uDFFB\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFB\u200D\uD83E\uDD1D\u200D(?:\uD83D[\uDC68\uDC69]))(?:\uD83C[\uDFFC-\uDFFF])|\uD83D\uDC68(?:\uD83C\uDFFB(?:\u200D(?:\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFF])|\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFF]))|\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFC-\uDFFF])|[\u2695\u2696\u2708]\uFE0F|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD]))?|(?:\uD83C[\uDFFC-\uDFFF])\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFF])|\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFF]))|\u200D(?:\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D)?\uD83D\uDC68|(?:\uD83D[\uDC68\uDC69])\u200D(?:\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67]))|\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67])|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFF\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFE])|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFE\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFD\uDFFF])|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFD\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFC\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB\uDFFD-\uDFFF])|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|(?:\uD83C\uDFFF\u200D[\u2695\u2696\u2708]|\uD83C\uDFFE\u200D[\u2695\u2696\u2708]|\uD83C\uDFFD\u200D[\u2695\u2696\u2708]|\uD83C\uDFFC\u200D[\u2695\u2696\u2708]|\u200D[\u2695\u2696\u2708])\uFE0F|\u200D(?:(?:\uD83D[\uDC68\uDC69])\u200D(?:\uD83D[\uDC66\uDC67])|\uD83D[\uDC66\uDC67])|\uD83C\uDFFF|\uD83C\uDFFE|\uD83C\uDFFD|\uD83C\uDFFC)?|(?:\uD83D\uDC69(?:\uD83C\uDFFB\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D(?:\uD83D[\uDC68\uDC69])|\uD83D[\uDC68\uDC69])|(?:\uD83C[\uDFFC-\uDFFF])\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D(?:\uD83D[\uDC68\uDC69])|\uD83D[\uDC68\uDC69]))|\uD83E\uDDD1(?:\uD83C[\uDFFB-\uDFFF])\u200D\uD83E\uDD1D\u200D\uD83E\uDDD1)(?:\uD83C[\uDFFB-\uDFFF])|\uD83D\uDC69\u200D\uD83D\uDC69\u200D(?:\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67]))|\uD83D\uDC69(?:\u200D(?:\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D(?:\uD83D[\uDC68\uDC69])|\uD83D[\uDC68\uDC69])|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFF\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFE\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFD\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFC\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFB\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD]))|\uD83E\uDDD1(?:\u200D(?:\uD83E\uDD1D\u200D\uD83E\uDDD1|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFF\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFE\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFD\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFC\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFB\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD]))|\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC69\u200D\uD83D\uDC69\u200D(?:\uD83D[\uDC66\uDC67])|\uD83D\uDC69\u200D\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67])|(?:\uD83D\uDC41\uFE0F\u200D\uD83D\uDDE8|\uD83E\uDDD1(?:\uD83C\uDFFF\u200D[\u2695\u2696\u2708]|\uD83C\uDFFE\u200D[\u2695\u2696\u2708]|\uD83C\uDFFD\u200D[\u2695\u2696\u2708]|\uD83C\uDFFC\u200D[\u2695\u2696\u2708]|\uD83C\uDFFB\u200D[\u2695\u2696\u2708]|\u200D[\u2695\u2696\u2708])|\uD83D\uDC69(?:\uD83C\uDFFF\u200D[\u2695\u2696\u2708]|\uD83C\uDFFE\u200D[\u2695\u2696\u2708]|\uD83C\uDFFD\u200D[\u2695\u2696\u2708]|\uD83C\uDFFC\u200D[\u2695\u2696\u2708]|\uD83C\uDFFB\u200D[\u2695\u2696\u2708]|\u200D[\u2695\u2696\u2708])|\uD83D\uDE36\u200D\uD83C\uDF2B|\uD83C\uDFF3\uFE0F\u200D\u26A7|\uD83D\uDC3B\u200D\u2744|(?:(?:\uD83C[\uDFC3\uDFC4\uDFCA]|\uD83D[\uDC6E\uDC70\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4-\uDEB6]|\uD83E[\uDD26\uDD35\uDD37-\uDD39\uDD3D\uDD3E\uDDB8\uDDB9\uDDCD-\uDDCF\uDDD4\uDDD6-\uDDDD])(?:\uD83C[\uDFFB-\uDFFF])|\uD83D\uDC6F|\uD83E[\uDD3C\uDDDE\uDDDF])\u200D[\u2640\u2642]|(?:\u26F9|\uD83C[\uDFCB\uDFCC]|\uD83D\uDD75)(?:\uFE0F|\uD83C[\uDFFB-\uDFFF])\u200D[\u2640\u2642]|\uD83C\uDFF4\u200D\u2620|(?:\uD83C[\uDFC3\uDFC4\uDFCA]|\uD83D[\uDC6E\uDC70\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4-\uDEB6]|\uD83E[\uDD26\uDD35\uDD37-\uDD39\uDD3D\uDD3E\uDDB8\uDDB9\uDDCD-\uDDCF\uDDD4\uDDD6-\uDDDD])\u200D[\u2640\u2642]|[\xA9\xAE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u2328\u23CF\u23ED-\u23EF\u23F1\u23F2\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB\u25FC\u2600-\u2604\u260E\u2611\u2618\u2620\u2622\u2623\u2626\u262A\u262E\u262F\u2638-\u263A\u2640\u2642\u265F\u2660\u2663\u2665\u2666\u2668\u267B\u267E\u2692\u2694-\u2697\u2699\u269B\u269C\u26A0\u26A7\u26B0\u26B1\u26C8\u26CF\u26D1\u26D3\u26E9\u26F0\u26F1\u26F4\u26F7\u26F8\u2702\u2708\u2709\u270F\u2712\u2714\u2716\u271D\u2721\u2733\u2734\u2744\u2747\u2763\u27A1\u2934\u2935\u2B05-\u2B07\u3030\u303D\u3297\u3299]|\uD83C[\uDD70\uDD71\uDD7E\uDD7F\uDE02\uDE37\uDF21\uDF24-\uDF2C\uDF36\uDF7D\uDF96\uDF97\uDF99-\uDF9B\uDF9E\uDF9F\uDFCD\uDFCE\uDFD4-\uDFDF\uDFF5\uDFF7]|\uD83D[\uDC3F\uDCFD\uDD49\uDD4A\uDD6F\uDD70\uDD73\uDD76-\uDD79\uDD87\uDD8A-\uDD8D\uDDA5\uDDA8\uDDB1\uDDB2\uDDBC\uDDC2-\uDDC4\uDDD1-\uDDD3\uDDDC-\uDDDE\uDDE1\uDDE3\uDDE8\uDDEF\uDDF3\uDDFA\uDECB\uDECD-\uDECF\uDEE0-\uDEE5\uDEE9\uDEF0\uDEF3])\uFE0F|\uD83C\uDFF3\uFE0F\u200D\uD83C\uDF08|\uD83D\uDC69\u200D\uD83D\uDC67|\uD83D\uDC69\u200D\uD83D\uDC66|\uD83D\uDE35\u200D\uD83D\uDCAB|\uD83D\uDE2E\u200D\uD83D\uDCA8|\uD83D\uDC15\u200D\uD83E\uDDBA|\uD83E\uDDD1(?:\uD83C\uDFFF|\uD83C\uDFFE|\uD83C\uDFFD|\uD83C\uDFFC|\uD83C\uDFFB)?|\uD83D\uDC69(?:\uD83C\uDFFF|\uD83C\uDFFE|\uD83C\uDFFD|\uD83C\uDFFC|\uD83C\uDFFB)?|\uD83C\uDDFD\uD83C\uDDF0|\uD83C\uDDF6\uD83C\uDDE6|\uD83C\uDDF4\uD83C\uDDF2|\uD83D\uDC08\u200D\u2B1B|\u2764\uFE0F\u200D(?:\uD83D\uDD25|\uD83E\uDE79)|\uD83D\uDC41\uFE0F|\uD83C\uDFF3\uFE0F|\uD83C\uDDFF(?:\uD83C[\uDDE6\uDDF2\uDDFC])|\uD83C\uDDFE(?:\uD83C[\uDDEA\uDDF9])|\uD83C\uDDFC(?:\uD83C[\uDDEB\uDDF8])|\uD83C\uDDFB(?:\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDEE\uDDF3\uDDFA])|\uD83C\uDDFA(?:\uD83C[\uDDE6\uDDEC\uDDF2\uDDF3\uDDF8\uDDFE\uDDFF])|\uD83C\uDDF9(?:\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDED\uDDEF-\uDDF4\uDDF7\uDDF9\uDDFB\uDDFC\uDDFF])|\uD83C\uDDF8(?:\uD83C[\uDDE6-\uDDEA\uDDEC-\uDDF4\uDDF7-\uDDF9\uDDFB\uDDFD-\uDDFF])|\uD83C\uDDF7(?:\uD83C[\uDDEA\uDDF4\uDDF8\uDDFA\uDDFC])|\uD83C\uDDF5(?:\uD83C[\uDDE6\uDDEA-\uDDED\uDDF0-\uDDF3\uDDF7-\uDDF9\uDDFC\uDDFE])|\uD83C\uDDF3(?:\uD83C[\uDDE6\uDDE8\uDDEA-\uDDEC\uDDEE\uDDF1\uDDF4\uDDF5\uDDF7\uDDFA\uDDFF])|\uD83C\uDDF2(?:\uD83C[\uDDE6\uDDE8-\uDDED\uDDF0-\uDDFF])|\uD83C\uDDF1(?:\uD83C[\uDDE6-\uDDE8\uDDEE\uDDF0\uDDF7-\uDDFB\uDDFE])|\uD83C\uDDF0(?:\uD83C[\uDDEA\uDDEC-\uDDEE\uDDF2\uDDF3\uDDF5\uDDF7\uDDFC\uDDFE\uDDFF])|\uD83C\uDDEF(?:\uD83C[\uDDEA\uDDF2\uDDF4\uDDF5])|\uD83C\uDDEE(?:\uD83C[\uDDE8-\uDDEA\uDDF1-\uDDF4\uDDF6-\uDDF9])|\uD83C\uDDED(?:\uD83C[\uDDF0\uDDF2\uDDF3\uDDF7\uDDF9\uDDFA])|\uD83C\uDDEC(?:\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEE\uDDF1-\uDDF3\uDDF5-\uDDFA\uDDFC\uDDFE])|\uD83C\uDDEB(?:\uD83C[\uDDEE-\uDDF0\uDDF2\uDDF4\uDDF7])|\uD83C\uDDEA(?:\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDED\uDDF7-\uDDFA])|\uD83C\uDDE9(?:\uD83C[\uDDEA\uDDEC\uDDEF\uDDF0\uDDF2\uDDF4\uDDFF])|\uD83C\uDDE8(?:\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDEE\uDDF0-\uDDF5\uDDF7\uDDFA-\uDDFF])|\uD83C\uDDE7(?:\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEF\uDDF1-\uDDF4\uDDF6-\uDDF9\uDDFB\uDDFC\uDDFE\uDDFF])|\uD83C\uDDE6(?:\uD83C[\uDDE8-\uDDEC\uDDEE\uDDF1\uDDF2\uDDF4\uDDF6-\uDDFA\uDDFC\uDDFD\uDDFF])|[#\*0-9]\uFE0F\u20E3|\u2764\uFE0F|(?:\uD83C[\uDFC3\uDFC4\uDFCA]|\uD83D[\uDC6E\uDC70\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4-\uDEB6]|\uD83E[\uDD26\uDD35\uDD37-\uDD39\uDD3D\uDD3E\uDDB8\uDDB9\uDDCD-\uDDCF\uDDD4\uDDD6-\uDDDD])(?:\uD83C[\uDFFB-\uDFFF])|(?:\u26F9|\uD83C[\uDFCB\uDFCC]|\uD83D\uDD75)(?:\uFE0F|\uD83C[\uDFFB-\uDFFF])|\uD83C\uDFF4|(?:[\u270A\u270B]|\uD83C[\uDF85\uDFC2\uDFC7]|\uD83D[\uDC42\uDC43\uDC46-\uDC50\uDC66\uDC67\uDC6B-\uDC6D\uDC72\uDC74-\uDC76\uDC78\uDC7C\uDC83\uDC85\uDC8F\uDC91\uDCAA\uDD7A\uDD95\uDD96\uDE4C\uDE4F\uDEC0\uDECC]|\uD83E[\uDD0C\uDD0F\uDD18-\uDD1C\uDD1E\uDD1F\uDD30-\uDD34\uDD36\uDD77\uDDB5\uDDB6\uDDBB\uDDD2\uDDD3\uDDD5])(?:\uD83C[\uDFFB-\uDFFF])|(?:[\u261D\u270C\u270D]|\uD83D[\uDD74\uDD90])(?:\uFE0F|\uD83C[\uDFFB-\uDFFF])|[\u270A\u270B]|\uD83C[\uDF85\uDFC2\uDFC7]|\uD83D[\uDC08\uDC15\uDC3B\uDC42\uDC43\uDC46-\uDC50\uDC66\uDC67\uDC6B-\uDC6D\uDC72\uDC74-\uDC76\uDC78\uDC7C\uDC83\uDC85\uDC8F\uDC91\uDCAA\uDD7A\uDD95\uDD96\uDE2E\uDE35\uDE36\uDE4C\uDE4F\uDEC0\uDECC]|\uD83E[\uDD0C\uDD0F\uDD18-\uDD1C\uDD1E\uDD1F\uDD30-\uDD34\uDD36\uDD77\uDDB5\uDDB6\uDDBB\uDDD2\uDDD3\uDDD5]|\uD83C[\uDFC3\uDFC4\uDFCA]|\uD83D[\uDC6E\uDC70\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4-\uDEB6]|\uD83E[\uDD26\uDD35\uDD37-\uDD39\uDD3D\uDD3E\uDDB8\uDDB9\uDDCD-\uDDCF\uDDD4\uDDD6-\uDDDD]|\uD83D\uDC6F|\uD83E[\uDD3C\uDDDE\uDDDF]|[\u231A\u231B\u23E9-\u23EC\u23F0\u23F3\u25FD\u25FE\u2614\u2615\u2648-\u2653\u267F\u2693\u26A1\u26AA\u26AB\u26BD\u26BE\u26C4\u26C5\u26CE\u26D4\u26EA\u26F2\u26F3\u26F5\u26FA\u26FD\u2705\u2728\u274C\u274E\u2753-\u2755\u2757\u2795-\u2797\u27B0\u27BF\u2B1B\u2B1C\u2B50\u2B55]|\uD83C[\uDC04\uDCCF\uDD8E\uDD91-\uDD9A\uDE01\uDE1A\uDE2F\uDE32-\uDE36\uDE38-\uDE3A\uDE50\uDE51\uDF00-\uDF20\uDF2D-\uDF35\uDF37-\uDF7C\uDF7E-\uDF84\uDF86-\uDF93\uDFA0-\uDFC1\uDFC5\uDFC6\uDFC8\uDFC9\uDFCF-\uDFD3\uDFE0-\uDFF0\uDFF8-\uDFFF]|\uD83D[\uDC00-\uDC07\uDC09-\uDC14\uDC16-\uDC3A\uDC3C-\uDC3E\uDC40\uDC44\uDC45\uDC51-\uDC65\uDC6A\uDC79-\uDC7B\uDC7D-\uDC80\uDC84\uDC88-\uDC8E\uDC90\uDC92-\uDCA9\uDCAB-\uDCFC\uDCFF-\uDD3D\uDD4B-\uDD4E\uDD50-\uDD67\uDDA4\uDDFB-\uDE2D\uDE2F-\uDE34\uDE37-\uDE44\uDE48-\uDE4A\uDE80-\uDEA2\uDEA4-\uDEB3\uDEB7-\uDEBF\uDEC1-\uDEC5\uDED0-\uDED2\uDED5-\uDED7\uDEEB\uDEEC\uDEF4-\uDEFC\uDFE0-\uDFEB]|\uD83E[\uDD0D\uDD0E\uDD10-\uDD17\uDD1D\uDD20-\uDD25\uDD27-\uDD2F\uDD3A\uDD3F-\uDD45\uDD47-\uDD76\uDD78\uDD7A-\uDDB4\uDDB7\uDDBA\uDDBC-\uDDCB\uDDD0\uDDE0-\uDDFF\uDE70-\uDE74\uDE78-\uDE7A\uDE80-\uDE86\uDE90-\uDEA8\uDEB0-\uDEB6\uDEC0-\uDEC2\uDED0-\uDED6]|(?:[\u231A\u231B\u23E9-\u23EC\u23F0\u23F3\u25FD\u25FE\u2614\u2615\u2648-\u2653\u267F\u2693\u26A1\u26AA\u26AB\u26BD\u26BE\u26C4\u26C5\u26CE\u26D4\u26EA\u26F2\u26F3\u26F5\u26FA\u26FD\u2705\u270A\u270B\u2728\u274C\u274E\u2753-\u2755\u2757\u2795-\u2797\u27B0\u27BF\u2B1B\u2B1C\u2B50\u2B55]|\uD83C[\uDC04\uDCCF\uDD8E\uDD91-\uDD9A\uDDE6-\uDDFF\uDE01\uDE1A\uDE2F\uDE32-\uDE36\uDE38-\uDE3A\uDE50\uDE51\uDF00-\uDF20\uDF2D-\uDF35\uDF37-\uDF7C\uDF7E-\uDF93\uDFA0-\uDFCA\uDFCF-\uDFD3\uDFE0-\uDFF0\uDFF4\uDFF8-\uDFFF]|\uD83D[\uDC00-\uDC3E\uDC40\uDC42-\uDCFC\uDCFF-\uDD3D\uDD4B-\uDD4E\uDD50-\uDD67\uDD7A\uDD95\uDD96\uDDA4\uDDFB-\uDE4F\uDE80-\uDEC5\uDECC\uDED0-\uDED2\uDED5-\uDED7\uDEEB\uDEEC\uDEF4-\uDEFC\uDFE0-\uDFEB]|\uD83E[\uDD0C-\uDD3A\uDD3C-\uDD45\uDD47-\uDD78\uDD7A-\uDDCB\uDDCD-\uDDFF\uDE70-\uDE74\uDE78-\uDE7A\uDE80-\uDE86\uDE90-\uDEA8\uDEB0-\uDEB6\uDEC0-\uDEC2\uDED0-\uDED6])|(?:[#\*0-9\xA9\xAE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u231A\u231B\u2328\u23CF\u23E9-\u23F3\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB-\u25FE\u2600-\u2604\u260E\u2611\u2614\u2615\u2618\u261D\u2620\u2622\u2623\u2626\u262A\u262E\u262F\u2638-\u263A\u2640\u2642\u2648-\u2653\u265F\u2660\u2663\u2665\u2666\u2668\u267B\u267E\u267F\u2692-\u2697\u2699\u269B\u269C\u26A0\u26A1\u26A7\u26AA\u26AB\u26B0\u26B1\u26BD\u26BE\u26C4\u26C5\u26C8\u26CE\u26CF\u26D1\u26D3\u26D4\u26E9\u26EA\u26F0-\u26F5\u26F7-\u26FA\u26FD\u2702\u2705\u2708-\u270D\u270F\u2712\u2714\u2716\u271D\u2721\u2728\u2733\u2734\u2744\u2747\u274C\u274E\u2753-\u2755\u2757\u2763\u2764\u2795-\u2797\u27A1\u27B0\u27BF\u2934\u2935\u2B05-\u2B07\u2B1B\u2B1C\u2B50\u2B55\u3030\u303D\u3297\u3299]|\uD83C[\uDC04\uDCCF\uDD70\uDD71\uDD7E\uDD7F\uDD8E\uDD91-\uDD9A\uDDE6-\uDDFF\uDE01\uDE02\uDE1A\uDE2F\uDE32-\uDE3A\uDE50\uDE51\uDF00-\uDF21\uDF24-\uDF93\uDF96\uDF97\uDF99-\uDF9B\uDF9E-\uDFF0\uDFF3-\uDFF5\uDFF7-\uDFFF]|\uD83D[\uDC00-\uDCFD\uDCFF-\uDD3D\uDD49-\uDD4E\uDD50-\uDD67\uDD6F\uDD70\uDD73-\uDD7A\uDD87\uDD8A-\uDD8D\uDD90\uDD95\uDD96\uDDA4\uDDA5\uDDA8\uDDB1\uDDB2\uDDBC\uDDC2-\uDDC4\uDDD1-\uDDD3\uDDDC-\uDDDE\uDDE1\uDDE3\uDDE8\uDDEF\uDDF3\uDDFA-\uDE4F\uDE80-\uDEC5\uDECB-\uDED2\uDED5-\uDED7\uDEE0-\uDEE5\uDEE9\uDEEB\uDEEC\uDEF0\uDEF3-\uDEFC\uDFE0-\uDFEB]|\uD83E[\uDD0C-\uDD3A\uDD3C-\uDD45\uDD47-\uDD78\uDD7A-\uDDCB\uDDCD-\uDDFF\uDE70-\uDE74\uDE78-\uDE7A\uDE80-\uDE86\uDE90-\uDEA8\uDEB0-\uDEB6\uDEC0-\uDEC2\uDED0-\uDED6])\uFE0F|(?:[\u261D\u26F9\u270A-\u270D]|\uD83C[\uDF85\uDFC2-\uDFC4\uDFC7\uDFCA-\uDFCC]|\uD83D[\uDC42\uDC43\uDC46-\uDC50\uDC66-\uDC78\uDC7C\uDC81-\uDC83\uDC85-\uDC87\uDC8F\uDC91\uDCAA\uDD74\uDD75\uDD7A\uDD90\uDD95\uDD96\uDE45-\uDE47\uDE4B-\uDE4F\uDEA3\uDEB4-\uDEB6\uDEC0\uDECC]|\uD83E[\uDD0C\uDD0F\uDD18-\uDD1F\uDD26\uDD30-\uDD39\uDD3C-\uDD3E\uDD77\uDDB5\uDDB6\uDDB8\uDDB9\uDDBB\uDDCD-\uDDCF\uDDD1-\uDDDD])/g;
|
|
211
|
+
};
|
|
212
|
+
var FD = O(uD);
|
|
213
|
+
function A(t, u2 = {}) {
|
|
214
|
+
if (typeof t != "string" || t.length === 0 || (u2 = { ambiguousIsNarrow: true, ...u2 }, t = T(t), t.length === 0)) return 0;
|
|
215
|
+
t = t.replace(FD(), " ");
|
|
216
|
+
const F = u2.ambiguousIsNarrow ? 1 : 2;
|
|
217
|
+
let e2 = 0;
|
|
218
|
+
for (const s of t) {
|
|
219
|
+
const i = s.codePointAt(0);
|
|
220
|
+
if (i <= 31 || i >= 127 && i <= 159 || i >= 768 && i <= 879) continue;
|
|
221
|
+
switch (DD.eastAsianWidth(s)) {
|
|
222
|
+
case "F":
|
|
223
|
+
case "W":
|
|
224
|
+
e2 += 2;
|
|
225
|
+
break;
|
|
226
|
+
case "A":
|
|
227
|
+
e2 += F;
|
|
228
|
+
break;
|
|
229
|
+
default:
|
|
230
|
+
e2 += 1;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return e2;
|
|
234
|
+
}
|
|
235
|
+
var m = 10;
|
|
236
|
+
var L = (t = 0) => (u2) => `\x1B[${u2 + t}m`;
|
|
237
|
+
var N = (t = 0) => (u2) => `\x1B[${38 + t};5;${u2}m`;
|
|
238
|
+
var I = (t = 0) => (u2, F, e2) => `\x1B[${38 + t};2;${u2};${F};${e2}m`;
|
|
239
|
+
var r = { modifier: { reset: [0, 0], bold: [1, 22], dim: [2, 22], italic: [3, 23], underline: [4, 24], overline: [53, 55], inverse: [7, 27], hidden: [8, 28], strikethrough: [9, 29] }, color: { black: [30, 39], red: [31, 39], green: [32, 39], yellow: [33, 39], blue: [34, 39], magenta: [35, 39], cyan: [36, 39], white: [37, 39], blackBright: [90, 39], gray: [90, 39], grey: [90, 39], redBright: [91, 39], greenBright: [92, 39], yellowBright: [93, 39], blueBright: [94, 39], magentaBright: [95, 39], cyanBright: [96, 39], whiteBright: [97, 39] }, bgColor: { bgBlack: [40, 49], bgRed: [41, 49], bgGreen: [42, 49], bgYellow: [43, 49], bgBlue: [44, 49], bgMagenta: [45, 49], bgCyan: [46, 49], bgWhite: [47, 49], bgBlackBright: [100, 49], bgGray: [100, 49], bgGrey: [100, 49], bgRedBright: [101, 49], bgGreenBright: [102, 49], bgYellowBright: [103, 49], bgBlueBright: [104, 49], bgMagentaBright: [105, 49], bgCyanBright: [106, 49], bgWhiteBright: [107, 49] } };
|
|
240
|
+
Object.keys(r.modifier);
|
|
241
|
+
var tD = Object.keys(r.color);
|
|
242
|
+
var eD = Object.keys(r.bgColor);
|
|
243
|
+
[...tD, ...eD];
|
|
244
|
+
function sD() {
|
|
245
|
+
const t = /* @__PURE__ */ new Map();
|
|
246
|
+
for (const [u2, F] of Object.entries(r)) {
|
|
247
|
+
for (const [e2, s] of Object.entries(F)) r[e2] = { open: `\x1B[${s[0]}m`, close: `\x1B[${s[1]}m` }, F[e2] = r[e2], t.set(s[0], s[1]);
|
|
248
|
+
Object.defineProperty(r, u2, { value: F, enumerable: false });
|
|
249
|
+
}
|
|
250
|
+
return Object.defineProperty(r, "codes", { value: t, enumerable: false }), r.color.close = "\x1B[39m", r.bgColor.close = "\x1B[49m", r.color.ansi = L(), r.color.ansi256 = N(), r.color.ansi16m = I(), r.bgColor.ansi = L(m), r.bgColor.ansi256 = N(m), r.bgColor.ansi16m = I(m), Object.defineProperties(r, { rgbToAnsi256: { value: (u2, F, e2) => u2 === F && F === e2 ? u2 < 8 ? 16 : u2 > 248 ? 231 : Math.round((u2 - 8) / 247 * 24) + 232 : 16 + 36 * Math.round(u2 / 255 * 5) + 6 * Math.round(F / 255 * 5) + Math.round(e2 / 255 * 5), enumerable: false }, hexToRgb: { value: (u2) => {
|
|
251
|
+
const F = /[a-f\d]{6}|[a-f\d]{3}/i.exec(u2.toString(16));
|
|
252
|
+
if (!F) return [0, 0, 0];
|
|
253
|
+
let [e2] = F;
|
|
254
|
+
e2.length === 3 && (e2 = [...e2].map((i) => i + i).join(""));
|
|
255
|
+
const s = Number.parseInt(e2, 16);
|
|
256
|
+
return [s >> 16 & 255, s >> 8 & 255, s & 255];
|
|
257
|
+
}, enumerable: false }, hexToAnsi256: { value: (u2) => r.rgbToAnsi256(...r.hexToRgb(u2)), enumerable: false }, ansi256ToAnsi: { value: (u2) => {
|
|
258
|
+
if (u2 < 8) return 30 + u2;
|
|
259
|
+
if (u2 < 16) return 90 + (u2 - 8);
|
|
260
|
+
let F, e2, s;
|
|
261
|
+
if (u2 >= 232) F = ((u2 - 232) * 10 + 8) / 255, e2 = F, s = F;
|
|
262
|
+
else {
|
|
263
|
+
u2 -= 16;
|
|
264
|
+
const C = u2 % 36;
|
|
265
|
+
F = Math.floor(u2 / 36) / 5, e2 = Math.floor(C / 6) / 5, s = C % 6 / 5;
|
|
266
|
+
}
|
|
267
|
+
const i = Math.max(F, e2, s) * 2;
|
|
268
|
+
if (i === 0) return 30;
|
|
269
|
+
let D = 30 + (Math.round(s) << 2 | Math.round(e2) << 1 | Math.round(F));
|
|
270
|
+
return i === 2 && (D += 60), D;
|
|
271
|
+
}, enumerable: false }, rgbToAnsi: { value: (u2, F, e2) => r.ansi256ToAnsi(r.rgbToAnsi256(u2, F, e2)), enumerable: false }, hexToAnsi: { value: (u2) => r.ansi256ToAnsi(r.hexToAnsi256(u2)), enumerable: false } }), r;
|
|
272
|
+
}
|
|
273
|
+
var iD = sD();
|
|
274
|
+
var v = /* @__PURE__ */ new Set(["\x1B", "\x9B"]);
|
|
275
|
+
var CD = 39;
|
|
276
|
+
var w = "\x07";
|
|
277
|
+
var W = "[";
|
|
278
|
+
var rD = "]";
|
|
279
|
+
var R = "m";
|
|
280
|
+
var y = `${rD}8;;`;
|
|
281
|
+
var V = (t) => `${v.values().next().value}${W}${t}${R}`;
|
|
282
|
+
var z = (t) => `${v.values().next().value}${y}${t}${w}`;
|
|
283
|
+
var ED = (t) => t.split(" ").map((u2) => A(u2));
|
|
284
|
+
var _ = (t, u2, F) => {
|
|
285
|
+
const e2 = [...u2];
|
|
286
|
+
let s = false, i = false, D = A(T(t[t.length - 1]));
|
|
287
|
+
for (const [C, o] of e2.entries()) {
|
|
288
|
+
const E2 = A(o);
|
|
289
|
+
if (D + E2 <= F ? t[t.length - 1] += o : (t.push(o), D = 0), v.has(o) && (s = true, i = e2.slice(C + 1).join("").startsWith(y)), s) {
|
|
290
|
+
i ? o === w && (s = false, i = false) : o === R && (s = false);
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
D += E2, D === F && C < e2.length - 1 && (t.push(""), D = 0);
|
|
294
|
+
}
|
|
295
|
+
!D && t[t.length - 1].length > 0 && t.length > 1 && (t[t.length - 2] += t.pop());
|
|
296
|
+
};
|
|
297
|
+
var nD = (t) => {
|
|
298
|
+
const u2 = t.split(" ");
|
|
299
|
+
let F = u2.length;
|
|
300
|
+
for (; F > 0 && !(A(u2[F - 1]) > 0); ) F--;
|
|
301
|
+
return F === u2.length ? t : u2.slice(0, F).join(" ") + u2.slice(F).join("");
|
|
302
|
+
};
|
|
303
|
+
var oD = (t, u2, F = {}) => {
|
|
304
|
+
if (F.trim !== false && t.trim() === "") return "";
|
|
305
|
+
let e2 = "", s, i;
|
|
306
|
+
const D = ED(t);
|
|
307
|
+
let C = [""];
|
|
308
|
+
for (const [E2, a2] of t.split(" ").entries()) {
|
|
309
|
+
F.trim !== false && (C[C.length - 1] = C[C.length - 1].trimStart());
|
|
310
|
+
let n = A(C[C.length - 1]);
|
|
311
|
+
if (E2 !== 0 && (n >= u2 && (F.wordWrap === false || F.trim === false) && (C.push(""), n = 0), (n > 0 || F.trim === false) && (C[C.length - 1] += " ", n++)), F.hard && D[E2] > u2) {
|
|
312
|
+
const B3 = u2 - n, p2 = 1 + Math.floor((D[E2] - B3 - 1) / u2);
|
|
313
|
+
Math.floor((D[E2] - 1) / u2) < p2 && C.push(""), _(C, a2, u2);
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
if (n + D[E2] > u2 && n > 0 && D[E2] > 0) {
|
|
317
|
+
if (F.wordWrap === false && n < u2) {
|
|
318
|
+
_(C, a2, u2);
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
C.push("");
|
|
322
|
+
}
|
|
323
|
+
if (n + D[E2] > u2 && F.wordWrap === false) {
|
|
324
|
+
_(C, a2, u2);
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
C[C.length - 1] += a2;
|
|
328
|
+
}
|
|
329
|
+
F.trim !== false && (C = C.map((E2) => nD(E2)));
|
|
330
|
+
const o = [...C.join(`
|
|
331
|
+
`)];
|
|
332
|
+
for (const [E2, a2] of o.entries()) {
|
|
333
|
+
if (e2 += a2, v.has(a2)) {
|
|
334
|
+
const { groups: B3 } = new RegExp(`(?:\\${W}(?<code>\\d+)m|\\${y}(?<uri>.*)${w})`).exec(o.slice(E2).join("")) || { groups: {} };
|
|
335
|
+
if (B3.code !== void 0) {
|
|
336
|
+
const p2 = Number.parseFloat(B3.code);
|
|
337
|
+
s = p2 === CD ? void 0 : p2;
|
|
338
|
+
} else B3.uri !== void 0 && (i = B3.uri.length === 0 ? void 0 : B3.uri);
|
|
339
|
+
}
|
|
340
|
+
const n = iD.codes.get(Number(s));
|
|
341
|
+
o[E2 + 1] === `
|
|
342
|
+
` ? (i && (e2 += z("")), s && n && (e2 += V(n))) : a2 === `
|
|
343
|
+
` && (s && n && (e2 += V(s)), i && (e2 += z(i)));
|
|
344
|
+
}
|
|
345
|
+
return e2;
|
|
346
|
+
};
|
|
347
|
+
function G(t, u2, F) {
|
|
348
|
+
return String(t).normalize().replace(/\r\n/g, `
|
|
349
|
+
`).split(`
|
|
350
|
+
`).map((e2) => oD(e2, u2, F)).join(`
|
|
351
|
+
`);
|
|
352
|
+
}
|
|
353
|
+
var aD = ["up", "down", "left", "right", "space", "enter", "cancel"];
|
|
354
|
+
var c = { actions: new Set(aD), aliases: /* @__PURE__ */ new Map([["k", "up"], ["j", "down"], ["h", "left"], ["l", "right"], ["", "cancel"], ["escape", "cancel"]]) };
|
|
355
|
+
function k(t, u2) {
|
|
356
|
+
if (typeof t == "string") return c.aliases.get(t) === u2;
|
|
357
|
+
for (const F of t) if (F !== void 0 && k(F, u2)) return true;
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
function lD(t, u2) {
|
|
361
|
+
if (t === u2) return;
|
|
362
|
+
const F = t.split(`
|
|
363
|
+
`), e2 = u2.split(`
|
|
364
|
+
`), s = [];
|
|
365
|
+
for (let i = 0; i < Math.max(F.length, e2.length); i++) F[i] !== e2[i] && s.push(i);
|
|
366
|
+
return s;
|
|
367
|
+
}
|
|
368
|
+
var xD = globalThis.process.platform.startsWith("win");
|
|
369
|
+
var S = /* @__PURE__ */ Symbol("clack:cancel");
|
|
370
|
+
function BD(t) {
|
|
371
|
+
return t === S;
|
|
372
|
+
}
|
|
373
|
+
function d(t, u2) {
|
|
374
|
+
const F = t;
|
|
375
|
+
F.isTTY && F.setRawMode(u2);
|
|
376
|
+
}
|
|
377
|
+
function cD({ input: t = import_node_process.stdin, output: u2 = import_node_process.stdout, overwrite: F = true, hideCursor: e2 = true } = {}) {
|
|
378
|
+
const s = f.createInterface({ input: t, output: u2, prompt: "", tabSize: 1 });
|
|
379
|
+
f.emitKeypressEvents(t, s), t.isTTY && t.setRawMode(true);
|
|
380
|
+
const i = (D, { name: C, sequence: o }) => {
|
|
381
|
+
const E2 = String(D);
|
|
382
|
+
if (k([E2, C, o], "cancel")) {
|
|
383
|
+
e2 && u2.write(import_sisteransi.cursor.show), process.exit(0);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
if (!F) return;
|
|
387
|
+
const a2 = C === "return" ? 0 : -1, n = C === "return" ? -1 : 0;
|
|
388
|
+
f.moveCursor(u2, a2, n, () => {
|
|
389
|
+
f.clearLine(u2, 1, () => {
|
|
390
|
+
t.once("keypress", i);
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
};
|
|
394
|
+
return e2 && u2.write(import_sisteransi.cursor.hide), t.once("keypress", i), () => {
|
|
395
|
+
t.off("keypress", i), e2 && u2.write(import_sisteransi.cursor.show), t.isTTY && !xD && t.setRawMode(false), s.terminal = false, s.close();
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
var AD = Object.defineProperty;
|
|
399
|
+
var pD = (t, u2, F) => u2 in t ? AD(t, u2, { enumerable: true, configurable: true, writable: true, value: F }) : t[u2] = F;
|
|
400
|
+
var h = (t, u2, F) => (pD(t, typeof u2 != "symbol" ? u2 + "" : u2, F), F);
|
|
401
|
+
var x = class {
|
|
402
|
+
constructor(u2, F = true) {
|
|
403
|
+
h(this, "input"), h(this, "output"), h(this, "_abortSignal"), h(this, "rl"), h(this, "opts"), h(this, "_render"), h(this, "_track", false), h(this, "_prevFrame", ""), h(this, "_subscribers", /* @__PURE__ */ new Map()), h(this, "_cursor", 0), h(this, "state", "initial"), h(this, "error", ""), h(this, "value");
|
|
404
|
+
const { input: e2 = import_node_process.stdin, output: s = import_node_process.stdout, render: i, signal: D, ...C } = u2;
|
|
405
|
+
this.opts = C, this.onKeypress = this.onKeypress.bind(this), this.close = this.close.bind(this), this.render = this.render.bind(this), this._render = i.bind(this), this._track = F, this._abortSignal = D, this.input = e2, this.output = s;
|
|
406
|
+
}
|
|
407
|
+
unsubscribe() {
|
|
408
|
+
this._subscribers.clear();
|
|
409
|
+
}
|
|
410
|
+
setSubscriber(u2, F) {
|
|
411
|
+
const e2 = this._subscribers.get(u2) ?? [];
|
|
412
|
+
e2.push(F), this._subscribers.set(u2, e2);
|
|
413
|
+
}
|
|
414
|
+
on(u2, F) {
|
|
415
|
+
this.setSubscriber(u2, { cb: F });
|
|
416
|
+
}
|
|
417
|
+
once(u2, F) {
|
|
418
|
+
this.setSubscriber(u2, { cb: F, once: true });
|
|
419
|
+
}
|
|
420
|
+
emit(u2, ...F) {
|
|
421
|
+
const e2 = this._subscribers.get(u2) ?? [], s = [];
|
|
422
|
+
for (const i of e2) i.cb(...F), i.once && s.push(() => e2.splice(e2.indexOf(i), 1));
|
|
423
|
+
for (const i of s) i();
|
|
424
|
+
}
|
|
425
|
+
prompt() {
|
|
426
|
+
return new Promise((u2, F) => {
|
|
427
|
+
if (this._abortSignal) {
|
|
428
|
+
if (this._abortSignal.aborted) return this.state = "cancel", this.close(), u2(S);
|
|
429
|
+
this._abortSignal.addEventListener("abort", () => {
|
|
430
|
+
this.state = "cancel", this.close();
|
|
431
|
+
}, { once: true });
|
|
432
|
+
}
|
|
433
|
+
const e2 = new import_node_tty.WriteStream(0);
|
|
434
|
+
e2._write = (s, i, D) => {
|
|
435
|
+
this._track && (this.value = this.rl?.line.replace(/\t/g, ""), this._cursor = this.rl?.cursor ?? 0, this.emit("value", this.value)), D();
|
|
436
|
+
}, this.input.pipe(e2), this.rl = import_node_readline.default.createInterface({ input: this.input, output: e2, tabSize: 2, prompt: "", escapeCodeTimeout: 50 }), import_node_readline.default.emitKeypressEvents(this.input, this.rl), this.rl.prompt(), this.opts.initialValue !== void 0 && this._track && this.rl.write(this.opts.initialValue), this.input.on("keypress", this.onKeypress), d(this.input, true), this.output.on("resize", this.render), this.render(), this.once("submit", () => {
|
|
437
|
+
this.output.write(import_sisteransi.cursor.show), this.output.off("resize", this.render), d(this.input, false), u2(this.value);
|
|
438
|
+
}), this.once("cancel", () => {
|
|
439
|
+
this.output.write(import_sisteransi.cursor.show), this.output.off("resize", this.render), d(this.input, false), u2(S);
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
onKeypress(u2, F) {
|
|
444
|
+
if (this.state === "error" && (this.state = "active"), F?.name && (!this._track && c.aliases.has(F.name) && this.emit("cursor", c.aliases.get(F.name)), c.actions.has(F.name) && this.emit("cursor", F.name)), u2 && (u2.toLowerCase() === "y" || u2.toLowerCase() === "n") && this.emit("confirm", u2.toLowerCase() === "y"), u2 === " " && this.opts.placeholder && (this.value || (this.rl?.write(this.opts.placeholder), this.emit("value", this.opts.placeholder))), u2 && this.emit("key", u2.toLowerCase()), F?.name === "return") {
|
|
445
|
+
if (this.opts.validate) {
|
|
446
|
+
const e2 = this.opts.validate(this.value);
|
|
447
|
+
e2 && (this.error = e2 instanceof Error ? e2.message : e2, this.state = "error", this.rl?.write(this.value));
|
|
448
|
+
}
|
|
449
|
+
this.state !== "error" && (this.state = "submit");
|
|
450
|
+
}
|
|
451
|
+
k([u2, F?.name, F?.sequence], "cancel") && (this.state = "cancel"), (this.state === "submit" || this.state === "cancel") && this.emit("finalize"), this.render(), (this.state === "submit" || this.state === "cancel") && this.close();
|
|
452
|
+
}
|
|
453
|
+
close() {
|
|
454
|
+
this.input.unpipe(), this.input.removeListener("keypress", this.onKeypress), this.output.write(`
|
|
455
|
+
`), d(this.input, false), this.rl?.close(), this.rl = void 0, this.emit(`${this.state}`, this.value), this.unsubscribe();
|
|
456
|
+
}
|
|
457
|
+
restoreCursor() {
|
|
458
|
+
const u2 = G(this._prevFrame, process.stdout.columns, { hard: true }).split(`
|
|
459
|
+
`).length - 1;
|
|
460
|
+
this.output.write(import_sisteransi.cursor.move(-999, u2 * -1));
|
|
461
|
+
}
|
|
462
|
+
render() {
|
|
463
|
+
const u2 = G(this._render(this) ?? "", process.stdout.columns, { hard: true });
|
|
464
|
+
if (u2 !== this._prevFrame) {
|
|
465
|
+
if (this.state === "initial") this.output.write(import_sisteransi.cursor.hide);
|
|
466
|
+
else {
|
|
467
|
+
const F = lD(this._prevFrame, u2);
|
|
468
|
+
if (this.restoreCursor(), F && F?.length === 1) {
|
|
469
|
+
const e2 = F[0];
|
|
470
|
+
this.output.write(import_sisteransi.cursor.move(0, e2)), this.output.write(import_sisteransi.erase.lines(1));
|
|
471
|
+
const s = u2.split(`
|
|
472
|
+
`);
|
|
473
|
+
this.output.write(s[e2]), this._prevFrame = u2, this.output.write(import_sisteransi.cursor.move(0, s.length - e2 - 1));
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (F && F?.length > 1) {
|
|
477
|
+
const e2 = F[0];
|
|
478
|
+
this.output.write(import_sisteransi.cursor.move(0, e2)), this.output.write(import_sisteransi.erase.down());
|
|
479
|
+
const s = u2.split(`
|
|
480
|
+
`).slice(e2);
|
|
481
|
+
this.output.write(s.join(`
|
|
482
|
+
`)), this._prevFrame = u2;
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
this.output.write(import_sisteransi.erase.down());
|
|
486
|
+
}
|
|
487
|
+
this.output.write(u2), this.state === "initial" && (this.state = "active"), this._prevFrame = u2;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
var fD = class extends x {
|
|
492
|
+
get cursor() {
|
|
493
|
+
return this.value ? 0 : 1;
|
|
494
|
+
}
|
|
495
|
+
get _value() {
|
|
496
|
+
return this.cursor === 0;
|
|
497
|
+
}
|
|
498
|
+
constructor(u2) {
|
|
499
|
+
super(u2, false), this.value = !!u2.initialValue, this.on("value", () => {
|
|
500
|
+
this.value = this._value;
|
|
501
|
+
}), this.on("confirm", (F) => {
|
|
502
|
+
this.output.write(import_sisteransi.cursor.move(0, -1)), this.value = F, this.state = "submit", this.close();
|
|
503
|
+
}), this.on("cursor", () => {
|
|
504
|
+
this.value = !this.value;
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
var yD = Object.defineProperty;
|
|
509
|
+
var _D = (t, u2, F) => u2 in t ? yD(t, u2, { enumerable: true, configurable: true, writable: true, value: F }) : t[u2] = F;
|
|
510
|
+
var Z = (t, u2, F) => (_D(t, typeof u2 != "symbol" ? u2 + "" : u2, F), F);
|
|
511
|
+
var kD = class extends x {
|
|
512
|
+
constructor({ mask: u2, ...F }) {
|
|
513
|
+
super(F), Z(this, "valueWithCursor", ""), Z(this, "_mask", "\u2022"), this._mask = u2 ?? "\u2022", this.on("finalize", () => {
|
|
514
|
+
this.valueWithCursor = this.masked;
|
|
515
|
+
}), this.on("value", () => {
|
|
516
|
+
if (this.cursor >= this.value.length) this.valueWithCursor = `${this.masked}${import_picocolors.default.inverse(import_picocolors.default.hidden("_"))}`;
|
|
517
|
+
else {
|
|
518
|
+
const e2 = this.masked.slice(0, this.cursor), s = this.masked.slice(this.cursor);
|
|
519
|
+
this.valueWithCursor = `${e2}${import_picocolors.default.inverse(s[0])}${s.slice(1)}`;
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
get cursor() {
|
|
524
|
+
return this._cursor;
|
|
525
|
+
}
|
|
526
|
+
get masked() {
|
|
527
|
+
return this.value.replaceAll(/./g, this._mask);
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
var SD = Object.defineProperty;
|
|
531
|
+
var $D = (t, u2, F) => u2 in t ? SD(t, u2, { enumerable: true, configurable: true, writable: true, value: F }) : t[u2] = F;
|
|
532
|
+
var q = (t, u2, F) => ($D(t, typeof u2 != "symbol" ? u2 + "" : u2, F), F);
|
|
533
|
+
var jD = class extends x {
|
|
534
|
+
constructor(u2) {
|
|
535
|
+
super(u2, false), q(this, "options"), q(this, "cursor", 0), this.options = u2.options, this.cursor = this.options.findIndex(({ value: F }) => F === u2.initialValue), this.cursor === -1 && (this.cursor = 0), this.changeValue(), this.on("cursor", (F) => {
|
|
536
|
+
switch (F) {
|
|
537
|
+
case "left":
|
|
538
|
+
case "up":
|
|
539
|
+
this.cursor = this.cursor === 0 ? this.options.length - 1 : this.cursor - 1;
|
|
540
|
+
break;
|
|
541
|
+
case "down":
|
|
542
|
+
case "right":
|
|
543
|
+
this.cursor = this.cursor === this.options.length - 1 ? 0 : this.cursor + 1;
|
|
544
|
+
break;
|
|
545
|
+
}
|
|
546
|
+
this.changeValue();
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
get _value() {
|
|
550
|
+
return this.options[this.cursor];
|
|
551
|
+
}
|
|
552
|
+
changeValue() {
|
|
553
|
+
this.value = this._value.value;
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
var PD = class extends x {
|
|
557
|
+
get valueWithCursor() {
|
|
558
|
+
if (this.state === "submit") return this.value;
|
|
559
|
+
if (this.cursor >= this.value.length) return `${this.value}\u2588`;
|
|
560
|
+
const u2 = this.value.slice(0, this.cursor), [F, ...e2] = this.value.slice(this.cursor);
|
|
561
|
+
return `${u2}${import_picocolors.default.inverse(F)}${e2.join("")}`;
|
|
562
|
+
}
|
|
563
|
+
get cursor() {
|
|
564
|
+
return this._cursor;
|
|
565
|
+
}
|
|
566
|
+
constructor(u2) {
|
|
567
|
+
super(u2), this.on("finalize", () => {
|
|
568
|
+
this.value || (this.value = u2.defaultValue);
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
// ../../node_modules/@clack/prompts/dist/index.mjs
|
|
574
|
+
var import_node_process2 = __toESM(require("process"), 1);
|
|
575
|
+
var import_picocolors2 = __toESM(require_picocolors(), 1);
|
|
576
|
+
var import_sisteransi2 = __toESM(require_src(), 1);
|
|
577
|
+
function X2() {
|
|
578
|
+
return import_node_process2.default.platform !== "win32" ? import_node_process2.default.env.TERM !== "linux" : !!import_node_process2.default.env.CI || !!import_node_process2.default.env.WT_SESSION || !!import_node_process2.default.env.TERMINUS_SUBLIME || import_node_process2.default.env.ConEmuTask === "{cmd::Cmder}" || import_node_process2.default.env.TERM_PROGRAM === "Terminus-Sublime" || import_node_process2.default.env.TERM_PROGRAM === "vscode" || import_node_process2.default.env.TERM === "xterm-256color" || import_node_process2.default.env.TERM === "alacritty" || import_node_process2.default.env.TERMINAL_EMULATOR === "JetBrains-JediTerm";
|
|
579
|
+
}
|
|
580
|
+
var E = X2();
|
|
581
|
+
var u = (s, n) => E ? s : n;
|
|
582
|
+
var ee = u("\u25C6", "*");
|
|
583
|
+
var A2 = u("\u25A0", "x");
|
|
584
|
+
var B = u("\u25B2", "x");
|
|
585
|
+
var S2 = u("\u25C7", "o");
|
|
586
|
+
var te = u("\u250C", "T");
|
|
587
|
+
var a = u("\u2502", "|");
|
|
588
|
+
var m2 = u("\u2514", "\u2014");
|
|
589
|
+
var j2 = u("\u25CF", ">");
|
|
590
|
+
var R2 = u("\u25CB", " ");
|
|
591
|
+
var V2 = u("\u25FB", "[\u2022]");
|
|
592
|
+
var M2 = u("\u25FC", "[+]");
|
|
593
|
+
var G2 = u("\u25FB", "[ ]");
|
|
594
|
+
var se = u("\u25AA", "\u2022");
|
|
595
|
+
var N2 = u("\u2500", "-");
|
|
596
|
+
var re = u("\u256E", "+");
|
|
597
|
+
var ie = u("\u251C", "+");
|
|
598
|
+
var ne = u("\u256F", "+");
|
|
599
|
+
var ae = u("\u25CF", "\u2022");
|
|
600
|
+
var oe = u("\u25C6", "*");
|
|
601
|
+
var ce = u("\u25B2", "!");
|
|
602
|
+
var le = u("\u25A0", "x");
|
|
603
|
+
var y2 = (s) => {
|
|
604
|
+
switch (s) {
|
|
605
|
+
case "initial":
|
|
606
|
+
case "active":
|
|
607
|
+
return import_picocolors2.default.cyan(ee);
|
|
608
|
+
case "cancel":
|
|
609
|
+
return import_picocolors2.default.red(A2);
|
|
610
|
+
case "error":
|
|
611
|
+
return import_picocolors2.default.yellow(B);
|
|
612
|
+
case "submit":
|
|
613
|
+
return import_picocolors2.default.green(S2);
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
var k2 = (s) => {
|
|
617
|
+
const { cursor: n, options: t, style: i } = s, r2 = s.maxItems ?? Number.POSITIVE_INFINITY, c2 = Math.max(process.stdout.rows - 4, 0), o = Math.min(c2, Math.max(r2, 5));
|
|
618
|
+
let l2 = 0;
|
|
619
|
+
n >= l2 + o - 3 ? l2 = Math.max(Math.min(n - o + 3, t.length - o), 0) : n < l2 + 2 && (l2 = Math.max(n - 2, 0));
|
|
620
|
+
const $2 = o < t.length && l2 > 0, d2 = o < t.length && l2 + o < t.length;
|
|
621
|
+
return t.slice(l2, l2 + o).map((w2, b2, C) => {
|
|
622
|
+
const I2 = b2 === 0 && $2, x2 = b2 === C.length - 1 && d2;
|
|
623
|
+
return I2 || x2 ? import_picocolors2.default.dim("...") : i(w2, b2 + l2 === n);
|
|
624
|
+
});
|
|
625
|
+
};
|
|
626
|
+
var ue = (s) => new PD({ validate: s.validate, placeholder: s.placeholder, defaultValue: s.defaultValue, initialValue: s.initialValue, render() {
|
|
627
|
+
const n = `${import_picocolors2.default.gray(a)}
|
|
628
|
+
${y2(this.state)} ${s.message}
|
|
629
|
+
`, t = s.placeholder ? import_picocolors2.default.inverse(s.placeholder[0]) + import_picocolors2.default.dim(s.placeholder.slice(1)) : import_picocolors2.default.inverse(import_picocolors2.default.hidden("_")), i = this.value ? this.valueWithCursor : t;
|
|
630
|
+
switch (this.state) {
|
|
631
|
+
case "error":
|
|
632
|
+
return `${n.trim()}
|
|
633
|
+
${import_picocolors2.default.yellow(a)} ${i}
|
|
634
|
+
${import_picocolors2.default.yellow(m2)} ${import_picocolors2.default.yellow(this.error)}
|
|
635
|
+
`;
|
|
636
|
+
case "submit":
|
|
637
|
+
return `${n}${import_picocolors2.default.gray(a)} ${import_picocolors2.default.dim(this.value || s.placeholder)}`;
|
|
638
|
+
case "cancel":
|
|
639
|
+
return `${n}${import_picocolors2.default.gray(a)} ${import_picocolors2.default.strikethrough(import_picocolors2.default.dim(this.value ?? ""))}${this.value?.trim() ? `
|
|
640
|
+
${import_picocolors2.default.gray(a)}` : ""}`;
|
|
641
|
+
default:
|
|
642
|
+
return `${n}${import_picocolors2.default.cyan(a)} ${i}
|
|
643
|
+
${import_picocolors2.default.cyan(m2)}
|
|
644
|
+
`;
|
|
645
|
+
}
|
|
646
|
+
} }).prompt();
|
|
647
|
+
var $e = (s) => new kD({ validate: s.validate, mask: s.mask ?? se, render() {
|
|
648
|
+
const n = `${import_picocolors2.default.gray(a)}
|
|
649
|
+
${y2(this.state)} ${s.message}
|
|
650
|
+
`, t = this.valueWithCursor, i = this.masked;
|
|
651
|
+
switch (this.state) {
|
|
652
|
+
case "error":
|
|
653
|
+
return `${n.trim()}
|
|
654
|
+
${import_picocolors2.default.yellow(a)} ${i}
|
|
655
|
+
${import_picocolors2.default.yellow(m2)} ${import_picocolors2.default.yellow(this.error)}
|
|
656
|
+
`;
|
|
657
|
+
case "submit":
|
|
658
|
+
return `${n}${import_picocolors2.default.gray(a)} ${import_picocolors2.default.dim(i)}`;
|
|
659
|
+
case "cancel":
|
|
660
|
+
return `${n}${import_picocolors2.default.gray(a)} ${import_picocolors2.default.strikethrough(import_picocolors2.default.dim(i ?? ""))}${i ? `
|
|
661
|
+
${import_picocolors2.default.gray(a)}` : ""}`;
|
|
662
|
+
default:
|
|
663
|
+
return `${n}${import_picocolors2.default.cyan(a)} ${t}
|
|
664
|
+
${import_picocolors2.default.cyan(m2)}
|
|
665
|
+
`;
|
|
666
|
+
}
|
|
667
|
+
} }).prompt();
|
|
668
|
+
var me = (s) => {
|
|
669
|
+
const n = s.active ?? "Yes", t = s.inactive ?? "No";
|
|
670
|
+
return new fD({ active: n, inactive: t, initialValue: s.initialValue ?? true, render() {
|
|
671
|
+
const i = `${import_picocolors2.default.gray(a)}
|
|
672
|
+
${y2(this.state)} ${s.message}
|
|
673
|
+
`, r2 = this.value ? n : t;
|
|
674
|
+
switch (this.state) {
|
|
675
|
+
case "submit":
|
|
676
|
+
return `${i}${import_picocolors2.default.gray(a)} ${import_picocolors2.default.dim(r2)}`;
|
|
677
|
+
case "cancel":
|
|
678
|
+
return `${i}${import_picocolors2.default.gray(a)} ${import_picocolors2.default.strikethrough(import_picocolors2.default.dim(r2))}
|
|
679
|
+
${import_picocolors2.default.gray(a)}`;
|
|
680
|
+
default:
|
|
681
|
+
return `${i}${import_picocolors2.default.cyan(a)} ${this.value ? `${import_picocolors2.default.green(j2)} ${n}` : `${import_picocolors2.default.dim(R2)} ${import_picocolors2.default.dim(n)}`} ${import_picocolors2.default.dim("/")} ${this.value ? `${import_picocolors2.default.dim(R2)} ${import_picocolors2.default.dim(t)}` : `${import_picocolors2.default.green(j2)} ${t}`}
|
|
682
|
+
${import_picocolors2.default.cyan(m2)}
|
|
683
|
+
`;
|
|
684
|
+
}
|
|
685
|
+
} }).prompt();
|
|
686
|
+
};
|
|
687
|
+
var de = (s) => {
|
|
688
|
+
const n = (t, i) => {
|
|
689
|
+
const r2 = t.label ?? String(t.value);
|
|
690
|
+
switch (i) {
|
|
691
|
+
case "selected":
|
|
692
|
+
return `${import_picocolors2.default.dim(r2)}`;
|
|
693
|
+
case "active":
|
|
694
|
+
return `${import_picocolors2.default.green(j2)} ${r2} ${t.hint ? import_picocolors2.default.dim(`(${t.hint})`) : ""}`;
|
|
695
|
+
case "cancelled":
|
|
696
|
+
return `${import_picocolors2.default.strikethrough(import_picocolors2.default.dim(r2))}`;
|
|
697
|
+
default:
|
|
698
|
+
return `${import_picocolors2.default.dim(R2)} ${import_picocolors2.default.dim(r2)}`;
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
return new jD({ options: s.options, initialValue: s.initialValue, render() {
|
|
702
|
+
const t = `${import_picocolors2.default.gray(a)}
|
|
703
|
+
${y2(this.state)} ${s.message}
|
|
704
|
+
`;
|
|
705
|
+
switch (this.state) {
|
|
706
|
+
case "submit":
|
|
707
|
+
return `${t}${import_picocolors2.default.gray(a)} ${n(this.options[this.cursor], "selected")}`;
|
|
708
|
+
case "cancel":
|
|
709
|
+
return `${t}${import_picocolors2.default.gray(a)} ${n(this.options[this.cursor], "cancelled")}
|
|
710
|
+
${import_picocolors2.default.gray(a)}`;
|
|
711
|
+
default:
|
|
712
|
+
return `${t}${import_picocolors2.default.cyan(a)} ${k2({ cursor: this.cursor, options: this.options, maxItems: s.maxItems, style: (i, r2) => n(i, r2 ? "active" : "inactive") }).join(`
|
|
713
|
+
${import_picocolors2.default.cyan(a)} `)}
|
|
714
|
+
${import_picocolors2.default.cyan(m2)}
|
|
715
|
+
`;
|
|
716
|
+
}
|
|
717
|
+
} }).prompt();
|
|
718
|
+
};
|
|
719
|
+
var ye = (s = "", n = "") => {
|
|
720
|
+
const t = `
|
|
721
|
+
${s}
|
|
722
|
+
`.split(`
|
|
723
|
+
`), i = (0, import_node_util.stripVTControlCharacters)(n).length, r2 = Math.max(t.reduce((o, l2) => {
|
|
724
|
+
const $2 = (0, import_node_util.stripVTControlCharacters)(l2);
|
|
725
|
+
return $2.length > o ? $2.length : o;
|
|
726
|
+
}, 0), i) + 2, c2 = t.map((o) => `${import_picocolors2.default.gray(a)} ${import_picocolors2.default.dim(o)}${" ".repeat(r2 - (0, import_node_util.stripVTControlCharacters)(o).length)}${import_picocolors2.default.gray(a)}`).join(`
|
|
727
|
+
`);
|
|
728
|
+
process.stdout.write(`${import_picocolors2.default.gray(a)}
|
|
729
|
+
${import_picocolors2.default.green(S2)} ${import_picocolors2.default.reset(n)} ${import_picocolors2.default.gray(N2.repeat(Math.max(r2 - i - 1, 1)) + re)}
|
|
730
|
+
${c2}
|
|
731
|
+
${import_picocolors2.default.gray(ie + N2.repeat(r2 + 2) + ne)}
|
|
732
|
+
`);
|
|
733
|
+
};
|
|
734
|
+
var ve = (s = "") => {
|
|
735
|
+
process.stdout.write(`${import_picocolors2.default.gray(m2)} ${import_picocolors2.default.red(s)}
|
|
736
|
+
|
|
737
|
+
`);
|
|
738
|
+
};
|
|
739
|
+
var we = (s = "") => {
|
|
740
|
+
process.stdout.write(`${import_picocolors2.default.gray(te)} ${s}
|
|
741
|
+
`);
|
|
742
|
+
};
|
|
743
|
+
var fe = (s = "") => {
|
|
744
|
+
process.stdout.write(`${import_picocolors2.default.gray(a)}
|
|
745
|
+
${import_picocolors2.default.gray(m2)} ${s}
|
|
746
|
+
|
|
747
|
+
`);
|
|
748
|
+
};
|
|
749
|
+
var v2 = { message: (s = "", { symbol: n = import_picocolors2.default.gray(a) } = {}) => {
|
|
750
|
+
const t = [`${import_picocolors2.default.gray(a)}`];
|
|
751
|
+
if (s) {
|
|
752
|
+
const [i, ...r2] = s.split(`
|
|
753
|
+
`);
|
|
754
|
+
t.push(`${n} ${i}`, ...r2.map((c2) => `${import_picocolors2.default.gray(a)} ${c2}`));
|
|
755
|
+
}
|
|
756
|
+
process.stdout.write(`${t.join(`
|
|
757
|
+
`)}
|
|
758
|
+
`);
|
|
759
|
+
}, info: (s) => {
|
|
760
|
+
v2.message(s, { symbol: import_picocolors2.default.blue(ae) });
|
|
761
|
+
}, success: (s) => {
|
|
762
|
+
v2.message(s, { symbol: import_picocolors2.default.green(oe) });
|
|
763
|
+
}, step: (s) => {
|
|
764
|
+
v2.message(s, { symbol: import_picocolors2.default.green(S2) });
|
|
765
|
+
}, warn: (s) => {
|
|
766
|
+
v2.message(s, { symbol: import_picocolors2.default.yellow(ce) });
|
|
767
|
+
}, warning: (s) => {
|
|
768
|
+
v2.warn(s);
|
|
769
|
+
}, error: (s) => {
|
|
770
|
+
v2.message(s, { symbol: import_picocolors2.default.red(le) });
|
|
771
|
+
} };
|
|
772
|
+
var L2 = () => {
|
|
773
|
+
const s = E ? ["\u25D2", "\u25D0", "\u25D3", "\u25D1"] : ["\u2022", "o", "O", "0"], n = E ? 80 : 120, t = process.env.CI === "true";
|
|
774
|
+
let i, r2, c2 = false, o = "", l2;
|
|
775
|
+
const $2 = (h2) => {
|
|
776
|
+
const g2 = h2 > 1 ? "Something went wrong" : "Canceled";
|
|
777
|
+
c2 && P2(g2, h2);
|
|
778
|
+
}, d2 = () => $2(2), w2 = () => $2(1), b2 = () => {
|
|
779
|
+
process.on("uncaughtExceptionMonitor", d2), process.on("unhandledRejection", d2), process.on("SIGINT", w2), process.on("SIGTERM", w2), process.on("exit", $2);
|
|
780
|
+
}, C = () => {
|
|
781
|
+
process.removeListener("uncaughtExceptionMonitor", d2), process.removeListener("unhandledRejection", d2), process.removeListener("SIGINT", w2), process.removeListener("SIGTERM", w2), process.removeListener("exit", $2);
|
|
782
|
+
}, I2 = () => {
|
|
783
|
+
if (l2 === void 0) return;
|
|
784
|
+
t && process.stdout.write(`
|
|
785
|
+
`);
|
|
786
|
+
const h2 = l2.split(`
|
|
787
|
+
`);
|
|
788
|
+
process.stdout.write(import_sisteransi2.cursor.move(-999, h2.length - 1)), process.stdout.write(import_sisteransi2.erase.down(h2.length));
|
|
789
|
+
}, x2 = (h2) => h2.replace(/\.+$/, ""), O2 = (h2 = "") => {
|
|
790
|
+
c2 = true, i = cD(), o = x2(h2), process.stdout.write(`${import_picocolors2.default.gray(a)}
|
|
791
|
+
`);
|
|
792
|
+
let g2 = 0, f2 = 0;
|
|
793
|
+
b2(), r2 = setInterval(() => {
|
|
794
|
+
if (t && o === l2) return;
|
|
795
|
+
I2(), l2 = o;
|
|
796
|
+
const W2 = import_picocolors2.default.magenta(s[g2]), _2 = t ? "..." : ".".repeat(Math.floor(f2)).slice(0, 3);
|
|
797
|
+
process.stdout.write(`${W2} ${o}${_2}`), g2 = g2 + 1 < s.length ? g2 + 1 : 0, f2 = f2 < s.length ? f2 + 0.125 : 0;
|
|
798
|
+
}, n);
|
|
799
|
+
}, P2 = (h2 = "", g2 = 0) => {
|
|
800
|
+
c2 = false, clearInterval(r2), I2();
|
|
801
|
+
const f2 = g2 === 0 ? import_picocolors2.default.green(S2) : g2 === 1 ? import_picocolors2.default.red(A2) : import_picocolors2.default.red(B);
|
|
802
|
+
o = x2(h2 ?? o), process.stdout.write(`${f2} ${o}
|
|
803
|
+
`), C(), i();
|
|
804
|
+
};
|
|
805
|
+
return { start: O2, stop: P2, message: (h2 = "") => {
|
|
806
|
+
o = x2(h2 ?? o);
|
|
807
|
+
} };
|
|
808
|
+
};
|
|
809
|
+
|
|
810
|
+
// src/prompts.ts
|
|
811
|
+
var import_node_crypto3 = require("crypto");
|
|
812
|
+
var import_node_fs4 = require("fs");
|
|
813
|
+
var import_node_os2 = require("os");
|
|
814
|
+
var import_node_path5 = require("path");
|
|
815
|
+
|
|
816
|
+
// src/header.ts
|
|
817
|
+
var R3 = "\x1B[0m";
|
|
818
|
+
var B2 = "\x1B[1m";
|
|
819
|
+
var AMB = "\x1B[38;2;224;162;72m";
|
|
820
|
+
var CRM = "\x1B[38;2;245;238;216m";
|
|
821
|
+
var FNT = "\x1B[38;2;130;100;45m";
|
|
822
|
+
var DIM = "\x1B[38;2;70;58;35m";
|
|
823
|
+
var MID = "\x1B[38;2;155;135;100m";
|
|
824
|
+
var GRN = "\x1B[38;2;80;200;120m";
|
|
825
|
+
var ab = (s) => `${B2}${AMB}${s}${R3}`;
|
|
826
|
+
var wb = (s) => `${B2}${CRM}${s}${R3}`;
|
|
827
|
+
var fn = (s) => `${FNT}${s}${R3}`;
|
|
828
|
+
var dm = (s) => `${DIM}${s}${R3}`;
|
|
829
|
+
var mi = (s) => `${MID}${s}${R3}`;
|
|
830
|
+
var gn = (s) => `${GRN}${s}${R3}`;
|
|
831
|
+
var _O = [" \u2588\u2588\u2588\u2588\u2588 ", "\u2588\u2588 \u2588\u2588", "\u2588\u2588 \u2588\u2588", "\u2588\u2588 \u2588\u2588", "\u2588\u2588 \u2588\u2588", "\u2588\u2588 \u2588\u2588", " \u2588\u2588\u2588\u2588\u2588 "];
|
|
832
|
+
var _N = ["\u2588\u2588 \u2588\u2588", "\u2588\u2588\u2588 \u2588\u2588", "\u2588\u2588\u2588\u2588 \u2588\u2588", "\u2588\u2588\u2588\u2588\u2588\u2588\u2588", "\u2588\u2588 \u2588\u2588\u2588\u2588", "\u2588\u2588 \u2588\u2588\u2588", "\u2588\u2588 \u2588\u2588"];
|
|
833
|
+
var _E = ["\u2588\u2588\u2588\u2588\u2588\u2588\u2588", "\u2588\u2588 ", "\u2588\u2588 ", "\u2588\u2588\u2588\u2588\u2588\u2588 ", "\u2588\u2588 ", "\u2588\u2588 ", "\u2588\u2588\u2588\u2588\u2588\u2588\u2588"];
|
|
834
|
+
var _A = [" \u2588\u2588\u2588 ", " \u2588\u2588 \u2588\u2588 ", "\u2588\u2588 \u2588\u2588", "\u2588\u2588\u2588\u2588\u2588\u2588\u2588", "\u2588\u2588 \u2588\u2588", "\u2588\u2588 \u2588\u2588", "\u2588\u2588 \u2588\u2588"];
|
|
835
|
+
var _D2 = ["\u2588\u2588\u2588\u2588\u2588\u2588 ", "\u2588\u2588 \u2588\u2588", "\u2588\u2588 \u2588\u2588", "\u2588\u2588 \u2588\u2588", "\u2588\u2588 \u2588\u2588", "\u2588\u2588 \u2588\u2588", "\u2588\u2588\u2588\u2588\u2588\u2588 "];
|
|
836
|
+
var _R = ["\u2588\u2588\u2588\u2588\u2588\u2588 ", "\u2588\u2588 \u2588\u2588", "\u2588\u2588 \u2588\u2588", "\u2588\u2588\u2588\u2588\u2588\u2588 ", "\u2588\u2588\u2588\u2588 ", "\u2588\u2588 \u2588\u2588 ", "\u2588\u2588 \u2588\u2588 "];
|
|
837
|
+
var _S = [" \u2588\u2588\u2588\u2588\u2588\u2588", "\u2588\u2588 ", "\u2588\u2588 ", " \u2588\u2588\u2588\u2588\u2588 ", " \u2588\u2588", " \u2588\u2588", "\u2588\u2588\u2588\u2588\u2588\u2588 "];
|
|
838
|
+
var ONE_ROWS = Array.from({ length: 7 }, (_2, i) => [_O[i], _N[i], _E[i]].join(" "));
|
|
839
|
+
var ADDR_ROWS = Array.from({ length: 7 }, (_2, i) => [_A[i], _D2[i], _D2[i], _R[i], _E[i], _S[i], _S[i]].join(" "));
|
|
840
|
+
function printHeader() {
|
|
841
|
+
const BAR_LEN = 86;
|
|
842
|
+
const TOP_BAR = fn("\u250C") + dm("\u2500".repeat(BAR_LEN)) + fn("\u2510");
|
|
843
|
+
const BOT_BAR = fn("\u2514") + dm("\u2500".repeat(BAR_LEN)) + fn("\u2518");
|
|
844
|
+
const SIDE = dm("\u2502");
|
|
845
|
+
console.log("");
|
|
846
|
+
console.log(" " + TOP_BAR);
|
|
847
|
+
console.log(` ${SIDE}${" ".repeat(BAR_LEN)}${SIDE}`);
|
|
848
|
+
for (let i = 0; i < 7; i++) {
|
|
849
|
+
const line = wb(ONE_ROWS[i]) + " " + ab(ADDR_ROWS[i]);
|
|
850
|
+
console.log(` ${SIDE} ${line} ${SIDE}`);
|
|
851
|
+
}
|
|
852
|
+
console.log(` ${SIDE} ${fn("\u2591".repeat(81))} ${SIDE}`);
|
|
853
|
+
console.log(` ${SIDE}${" ".repeat(BAR_LEN)}${SIDE}`);
|
|
854
|
+
const wizard = mi("Partner Setup Wizard");
|
|
855
|
+
const version = fn("v2 \xB7 partners.oneaddress.io");
|
|
856
|
+
const tagLine = ` ${wizard} ${version} ${gn("\xB7io")}`;
|
|
857
|
+
const padLen = BAR_LEN - 4 - stripAnsi(tagLine).length;
|
|
858
|
+
console.log(` ${SIDE} ${tagLine}${" ".repeat(Math.max(0, padLen))} ${SIDE}`);
|
|
859
|
+
console.log(` ${SIDE}${" ".repeat(BAR_LEN)}${SIDE}`);
|
|
860
|
+
console.log(" " + BOT_BAR);
|
|
861
|
+
console.log("");
|
|
862
|
+
}
|
|
863
|
+
function stripAnsi(s) {
|
|
864
|
+
return s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// src/scaffold.ts
|
|
868
|
+
var import_promises = require("fs/promises");
|
|
869
|
+
var import_node_fs = require("fs");
|
|
870
|
+
var import_node_path = require("path");
|
|
871
|
+
|
|
872
|
+
// src/templates.ts
|
|
873
|
+
var TEMPLATES = {
|
|
874
|
+
// ── TypeScript / Node.js (Express) ─────────────────────────────────────────
|
|
875
|
+
"ts-node": [
|
|
876
|
+
{
|
|
877
|
+
name: ".env",
|
|
878
|
+
content: `# OneAddress webhook credentials \u2014 written by npx @oneaddress/setup
|
|
879
|
+
PARTNER_ID=%%PARTNER_ID%%
|
|
880
|
+
WEBHOOK_SECRET=%%WEBHOOK_SECRET%%
|
|
881
|
+
PARTNER_PRIVATE_KEY_PEM=%%PRIVATE_KEY%%
|
|
882
|
+
PORT=3001
|
|
883
|
+
`
|
|
884
|
+
},
|
|
885
|
+
{
|
|
886
|
+
name: ".env.example",
|
|
887
|
+
content: `# OneAddress webhook credentials
|
|
888
|
+
# Copy this file to .env and fill in real values. Never commit .env to source control.
|
|
889
|
+
|
|
890
|
+
# Your partner UUID from the OneAddress Partner Portal
|
|
891
|
+
PARTNER_ID=your-partner-uuid-here
|
|
892
|
+
|
|
893
|
+
# HMAC-SHA256 webhook signing secret from the Partner Portal
|
|
894
|
+
WEBHOOK_SECRET=your-webhook-secret-here
|
|
895
|
+
|
|
896
|
+
# PKCS#8 PEM private key for decrypting address payloads.
|
|
897
|
+
# Newlines encoded as literal \\n in a single-line value:
|
|
898
|
+
# -----BEGIN PRIVATE KEY-----\\nMIGHAgEA...\\n-----END PRIVATE KEY-----
|
|
899
|
+
PARTNER_PRIVATE_KEY_PEM=
|
|
900
|
+
|
|
901
|
+
# HTTP port for the local webhook server
|
|
902
|
+
PORT=3001
|
|
903
|
+
`
|
|
904
|
+
},
|
|
905
|
+
{
|
|
906
|
+
name: ".gitignore",
|
|
907
|
+
content: `.env
|
|
908
|
+
node_modules/
|
|
909
|
+
dist/
|
|
910
|
+
`
|
|
911
|
+
},
|
|
912
|
+
{
|
|
913
|
+
name: "package.json",
|
|
914
|
+
content: `{
|
|
915
|
+
"name": "oneaddress-webhook-handler",
|
|
916
|
+
"version": "1.0.0",
|
|
917
|
+
"private": true,
|
|
918
|
+
"scripts": {
|
|
919
|
+
"dev": "tsx watch src/server.ts",
|
|
920
|
+
"start": "node dist/server.js",
|
|
921
|
+
"build": "tsup src/server.ts --format cjs --no-dts --outDir dist",
|
|
922
|
+
"test": "tsx scripts/test.ts"
|
|
923
|
+
},
|
|
924
|
+
"dependencies": {
|
|
925
|
+
"@oneaddress/partner-sdk": "latest",
|
|
926
|
+
"dotenv": "^16.0.0",
|
|
927
|
+
"express": "^4.18.0"
|
|
928
|
+
},
|
|
929
|
+
"devDependencies": {
|
|
930
|
+
"@types/express": "^4.17.0",
|
|
931
|
+
"@types/node": "^20.0.0",
|
|
932
|
+
"tsup": "^8.0.0",
|
|
933
|
+
"tsx": "^4.0.0",
|
|
934
|
+
"typescript": "^5.0.0"
|
|
935
|
+
},
|
|
936
|
+
"engines": { "node": ">=18" }
|
|
937
|
+
}
|
|
938
|
+
`
|
|
939
|
+
},
|
|
940
|
+
{
|
|
941
|
+
name: "src/store.ts",
|
|
942
|
+
content: `/**
|
|
943
|
+
* \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
|
|
944
|
+
* \u2551 src/store.ts \u2014 YOUR DATABASE INTEGRATION LIVES HERE \u2551
|
|
945
|
+
* \u2551 \u2551
|
|
946
|
+
* \u2551 Implement the two functions below using whatever database, \u2551
|
|
947
|
+
* \u2551 ORM, or API your organisation already uses. \u2551
|
|
948
|
+
* \u2551 server.ts calls them after verifying and decrypting each event \u2551
|
|
949
|
+
* \u2551 \u2014 the protocol layer is handled for you, never touch it. \u2551
|
|
950
|
+
* \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
|
|
951
|
+
*/
|
|
952
|
+
|
|
953
|
+
export type Address = Record<string, unknown>;
|
|
954
|
+
|
|
955
|
+
export type Customer = {
|
|
956
|
+
email: string;
|
|
957
|
+
name: string;
|
|
958
|
+
};
|
|
959
|
+
|
|
960
|
+
export type VerifyResult = 'match' | 'mismatch' | 'not_found';
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* Called when a consumer updates their address (address.updated event).
|
|
964
|
+
*
|
|
965
|
+
* The \`address\` object is already decrypted \u2014 fields vary by country but
|
|
966
|
+
* typically include: street, suburb, state, postcode, country.
|
|
967
|
+
*
|
|
968
|
+
* Persist this to your database (update the customer's address record,
|
|
969
|
+
* publish to a queue, call an internal API \u2014 whatever your system needs).
|
|
970
|
+
*/
|
|
971
|
+
export async function saveAddress(customer: Customer, address: Address): Promise<void> {
|
|
972
|
+
// \u2500\u2500\u2500 TODO: implement \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
973
|
+
//
|
|
974
|
+
// Examples:
|
|
975
|
+
//
|
|
976
|
+
// Prisma:
|
|
977
|
+
// await prisma.customer.update({
|
|
978
|
+
// where: { email: customer.email },
|
|
979
|
+
// data: { address: JSON.stringify(address) },
|
|
980
|
+
// });
|
|
981
|
+
//
|
|
982
|
+
// Postgres.js:
|
|
983
|
+
// await sql\`
|
|
984
|
+
// UPDATE customers SET address = \${sql.json(address)}
|
|
985
|
+
// WHERE email = \${customer.email}
|
|
986
|
+
// \`;
|
|
987
|
+
//
|
|
988
|
+
// HTTP / CRM:
|
|
989
|
+
// await fetch('https://your-crm.internal/addresses', {
|
|
990
|
+
// method: 'PUT',
|
|
991
|
+
// body: JSON.stringify({ email: customer.email, address }),
|
|
992
|
+
// });
|
|
993
|
+
//
|
|
994
|
+
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
995
|
+
|
|
996
|
+
console.log('[store] saveAddress \u2014 implement this function to persist the address');
|
|
997
|
+
console.log('[store] customer:', customer.email);
|
|
998
|
+
console.log('[store] address: ', JSON.stringify(address));
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/**
|
|
1002
|
+
* Called during an address verification check (address.verify event).
|
|
1003
|
+
*
|
|
1004
|
+
* The \`address\` object is already decrypted. Compare it against whatever
|
|
1005
|
+
* you have on file for this customer and return the appropriate result.
|
|
1006
|
+
*
|
|
1007
|
+
* Return values:
|
|
1008
|
+
* 'match' \u2014 the address matches your records exactly
|
|
1009
|
+
* 'mismatch' \u2014 you have a record but the address differs
|
|
1010
|
+
* 'not_found' \u2014 you have no record for this customer at all
|
|
1011
|
+
*/
|
|
1012
|
+
export async function verifyAddress(customer: Customer, address: Address): Promise<VerifyResult> {
|
|
1013
|
+
// \u2500\u2500\u2500 TODO: implement \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1014
|
+
//
|
|
1015
|
+
// Example:
|
|
1016
|
+
// const record = await prisma.customer.findUnique({ where: { email: customer.email } });
|
|
1017
|
+
// if (!record) return 'not_found';
|
|
1018
|
+
// const stored = JSON.parse(record.address ?? '{}') as Address;
|
|
1019
|
+
// const match = Object.entries(address).every(([k, v]) => stored[k] === v);
|
|
1020
|
+
// return match ? 'match' : 'mismatch';
|
|
1021
|
+
//
|
|
1022
|
+
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1023
|
+
|
|
1024
|
+
console.log('[store] verifyAddress \u2014 returning stub "match". Implement this function.');
|
|
1025
|
+
return 'match';
|
|
1026
|
+
}
|
|
1027
|
+
`
|
|
1028
|
+
},
|
|
1029
|
+
{
|
|
1030
|
+
name: "src/server.ts",
|
|
1031
|
+
content: `/**
|
|
1032
|
+
* \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
|
|
1033
|
+
* \u2551 src/server.ts \u2014 PROTOCOL LAYER (do not edit) \u2551
|
|
1034
|
+
* \u2551 \u2551
|
|
1035
|
+
* \u2551 Handles all OneAddress webhook mechanics: HMAC-SHA256 \u2551
|
|
1036
|
+
* \u2551 verification, timestamp replay protection, ECDH address \u2551
|
|
1037
|
+
* \u2551 decryption, dispatch-ID deduplication, and callback posting. \u2551
|
|
1038
|
+
* \u2551 \u2551
|
|
1039
|
+
* \u2551 To wire up your database: edit src/store.ts instead. \u2551
|
|
1040
|
+
* \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
|
|
1041
|
+
*/
|
|
1042
|
+
import 'dotenv/config';
|
|
1043
|
+
import express, { Request, Response } from 'express';
|
|
1044
|
+
import { verifySignature, decryptAddress, isFreshTimestamp } from '@oneaddress/partner-sdk';
|
|
1045
|
+
import { saveAddress, verifyAddress } from './store.js';
|
|
1046
|
+
|
|
1047
|
+
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET ?? '';
|
|
1048
|
+
const PARTNER_PRIVATE_KEY = process.env.PARTNER_PRIVATE_KEY_PEM ?? '';
|
|
1049
|
+
const PARTNER_ID = process.env.PARTNER_ID ?? '%%PARTNER_ID%%';
|
|
1050
|
+
const PORT = Number(process.env.PORT ?? 3001);
|
|
1051
|
+
|
|
1052
|
+
if (!WEBHOOK_SECRET || !PARTNER_PRIVATE_KEY || !PARTNER_ID) {
|
|
1053
|
+
console.error('[startup] Missing required env vars. Check your .env file.');
|
|
1054
|
+
process.exit(1);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// In-memory dedup cache \u2014 for production use a distributed store (Redis, etc.)
|
|
1058
|
+
const seenDispatches = new Set<string>();
|
|
1059
|
+
|
|
1060
|
+
const app = express();
|
|
1061
|
+
app.use('/webhook', express.text({ type: 'application/json', limit: '1mb' }));
|
|
1062
|
+
|
|
1063
|
+
app.post('/webhook', async (req: Request, res: Response) => {
|
|
1064
|
+
const rawBody = req.body as string;
|
|
1065
|
+
const signature = req.headers['x-oneaddress-signature'] as string ?? '';
|
|
1066
|
+
const timestamp = req.headers['x-oneaddress-timestamp'] as string ?? '';
|
|
1067
|
+
const dispatch = req.headers['x-oneaddress-dispatch'] as string ?? '';
|
|
1068
|
+
|
|
1069
|
+
// 1. Timestamp freshness (\xB15-minute replay window)
|
|
1070
|
+
if (!isFreshTimestamp(timestamp, 300)) {
|
|
1071
|
+
return res.status(400).json({ error: 'Request timestamp out of tolerance' });
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// 2. HMAC-SHA256 signature verification
|
|
1075
|
+
if (!verifySignature(rawBody, signature, timestamp, WEBHOOK_SECRET)) {
|
|
1076
|
+
return res.status(401).json({ error: 'Invalid signature' });
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// 3. Parse body
|
|
1080
|
+
let body: Record<string, unknown>;
|
|
1081
|
+
try { body = JSON.parse(rawBody) as Record<string, unknown>; }
|
|
1082
|
+
catch { return res.status(400).json({ error: 'Invalid JSON' }); }
|
|
1083
|
+
|
|
1084
|
+
// 4. Idempotency \u2014 ignore duplicate retries
|
|
1085
|
+
if (dispatch && seenDispatches.has(dispatch)) {
|
|
1086
|
+
return res.status(200).json({ ok: true, duplicate: true });
|
|
1087
|
+
}
|
|
1088
|
+
if (dispatch) seenDispatches.add(dispatch);
|
|
1089
|
+
|
|
1090
|
+
// 5. Decrypt address payload
|
|
1091
|
+
const encPayload = body.address_encrypted as {
|
|
1092
|
+
ephemeralPublicKey: string; iv: string; ciphertext: string; hkdfSalt?: string;
|
|
1093
|
+
} | null;
|
|
1094
|
+
|
|
1095
|
+
if (!encPayload) {
|
|
1096
|
+
return res.status(200).json({ ok: true, note: 'No encrypted payload' });
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
let address: Record<string, unknown>;
|
|
1100
|
+
try {
|
|
1101
|
+
address = await decryptAddress(encPayload, PARTNER_PRIVATE_KEY, PARTNER_ID);
|
|
1102
|
+
} catch (err) {
|
|
1103
|
+
console.error('[webhook] Decryption failed \u2014 check PARTNER_PRIVATE_KEY_PEM in .env:', err);
|
|
1104
|
+
return res.status(200).json({ ok: true, note: 'Received \u2014 decryption pending key setup' });
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
const event = body.event as string;
|
|
1108
|
+
const customer = body.customer as { email?: string; name?: string } | undefined;
|
|
1109
|
+
const ctx = { email: customer?.email ?? '', name: customer?.name ?? '' };
|
|
1110
|
+
|
|
1111
|
+
// \u2500\u2500 address.updated: consumer changed their address \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1112
|
+
if (event === 'address.updated') {
|
|
1113
|
+
console.log(\`[webhook] address.updated for \${ctx.email}\`);
|
|
1114
|
+
await saveAddress(ctx, address);
|
|
1115
|
+
return res.status(200).json({ ok: true });
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// \u2500\u2500 address.verify: consumer is running an address check \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1119
|
+
if (event === 'address.verify') {
|
|
1120
|
+
const callbackUrl = body.callback_url as string;
|
|
1121
|
+
const callbackToken = body.callback_token as string;
|
|
1122
|
+
const batchId = body.batch_id as string;
|
|
1123
|
+
|
|
1124
|
+
console.log(\`[webhook] address.verify for \${ctx.email}\`);
|
|
1125
|
+
const result = await verifyAddress(ctx, address);
|
|
1126
|
+
console.log(\`[webhook] address.verify \u2192 \${result}\`);
|
|
1127
|
+
|
|
1128
|
+
await fetch(callbackUrl, {
|
|
1129
|
+
method: 'POST',
|
|
1130
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1131
|
+
body: JSON.stringify({
|
|
1132
|
+
batch_id: batchId,
|
|
1133
|
+
partner_id: PARTNER_ID,
|
|
1134
|
+
member_name: ctx.name,
|
|
1135
|
+
result,
|
|
1136
|
+
token: callbackToken,
|
|
1137
|
+
}),
|
|
1138
|
+
});
|
|
1139
|
+
return res.status(200).json({ ok: true });
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
// Unknown event \u2014 acknowledge (forward compatibility)
|
|
1143
|
+
console.log(\`[webhook] Unknown event "\${event}" \u2014 acknowledged\`);
|
|
1144
|
+
return res.status(200).json({ ok: true, skipped: true });
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
app.get('/health', (_req, res) => res.json({ status: 'ok' }));
|
|
1148
|
+
|
|
1149
|
+
app.listen(PORT, () =>
|
|
1150
|
+
console.log(\`[server] OneAddress webhook server \u2192 http://localhost:\${PORT}/webhook\`),
|
|
1151
|
+
);
|
|
1152
|
+
`
|
|
1153
|
+
},
|
|
1154
|
+
{
|
|
1155
|
+
name: "scripts/test.ts",
|
|
1156
|
+
content: `/**
|
|
1157
|
+
* Conformance test runner.
|
|
1158
|
+
*
|
|
1159
|
+
* Usage:
|
|
1160
|
+
* npm test \u2014 localhost:\${PORT}/webhook
|
|
1161
|
+
* npm test https://my-api.example.com/webhook \u2014 live URL
|
|
1162
|
+
*/
|
|
1163
|
+
import 'dotenv/config';
|
|
1164
|
+
import { createPrivateKey, createPublicKey } from 'node:crypto';
|
|
1165
|
+
import { spawnSync } from 'node:child_process';
|
|
1166
|
+
|
|
1167
|
+
const secret = process.env.WEBHOOK_SECRET ?? '';
|
|
1168
|
+
const partnerId = process.env.PARTNER_ID ?? '%%PARTNER_ID%%';
|
|
1169
|
+
const keyPem = (process.env.PARTNER_PRIVATE_KEY_PEM ?? '').replace(/\\\\n/g, '\\n');
|
|
1170
|
+
const port = process.env.PORT ?? '3001';
|
|
1171
|
+
const targetUrl = process.argv[2] ?? \`http://localhost:\${port}/webhook\`;
|
|
1172
|
+
|
|
1173
|
+
if (!secret || !partnerId || !keyPem) {
|
|
1174
|
+
console.error('[test] Missing required env vars. Check your .env file.');
|
|
1175
|
+
process.exit(1);
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
let publicKeyB64: string;
|
|
1179
|
+
try {
|
|
1180
|
+
const priv = createPrivateKey({ key: keyPem, format: 'pem' });
|
|
1181
|
+
const pub = createPublicKey(priv);
|
|
1182
|
+
publicKeyB64 = pub.export({ type: 'spki', format: 'der' }).toString('base64');
|
|
1183
|
+
} catch (err) {
|
|
1184
|
+
console.error('[test] Failed to derive public key from PARTNER_PRIVATE_KEY_PEM:', err);
|
|
1185
|
+
process.exit(1);
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
console.log(\`[test] Running conformance against \${targetUrl}\\n\`);
|
|
1189
|
+
|
|
1190
|
+
const result = spawnSync(
|
|
1191
|
+
'npx',
|
|
1192
|
+
['@oneaddress/conformance', 'test', targetUrl, '--secret', secret, '--partner-id', partnerId, '--public-key', publicKeyB64],
|
|
1193
|
+
{ stdio: 'inherit', shell: true },
|
|
1194
|
+
);
|
|
1195
|
+
|
|
1196
|
+
process.exit(result.status ?? 1);
|
|
1197
|
+
`
|
|
1198
|
+
},
|
|
1199
|
+
{
|
|
1200
|
+
name: "README.md",
|
|
1201
|
+
content: `# OneAddress Webhook Handler \u2014 TypeScript / Node.js
|
|
1202
|
+
|
|
1203
|
+
Generated by \`npx @oneaddress/setup\` for partner **%%PARTNER_ID%%**.
|
|
1204
|
+
|
|
1205
|
+
## Quick start
|
|
1206
|
+
|
|
1207
|
+
\`\`\`bash
|
|
1208
|
+
npm install
|
|
1209
|
+
npm run dev # hot-reload via tsx
|
|
1210
|
+
\`\`\`
|
|
1211
|
+
|
|
1212
|
+
Webhook endpoint: \`POST http://localhost:3001/webhook\`
|
|
1213
|
+
Health check: \`GET http://localhost:3001/health\`
|
|
1214
|
+
|
|
1215
|
+
## Architecture
|
|
1216
|
+
|
|
1217
|
+
| File | Role |
|
|
1218
|
+
|------|------|
|
|
1219
|
+
| \`src/server.ts\` | **Protocol layer \u2014 do not edit.** HMAC verification, timestamp replay protection, ECDH decryption, deduplication. |
|
|
1220
|
+
| \`src/store.ts\` | **Your integration \u2014 implement this.** Two functions called after every event. Wire up your database, ORM, or internal API here. |
|
|
1221
|
+
|
|
1222
|
+
## Implementing src/store.ts
|
|
1223
|
+
|
|
1224
|
+
After running the wizard, open \`src/store.ts\` and implement:
|
|
1225
|
+
|
|
1226
|
+
\`\`\`typescript
|
|
1227
|
+
// Called on address.updated \u2014 persist the decrypted address
|
|
1228
|
+
async function saveAddress(customer, address): Promise<void>
|
|
1229
|
+
|
|
1230
|
+
// Called on address.verify \u2014 return 'match' | 'mismatch' | 'not_found'
|
|
1231
|
+
async function verifyAddress(customer, address): Promise<VerifyResult>
|
|
1232
|
+
\`\`\`
|
|
1233
|
+
|
|
1234
|
+
The file has commented examples for Prisma, Postgres.js, and plain HTTP calls.
|
|
1235
|
+
|
|
1236
|
+
## Events
|
|
1237
|
+
|
|
1238
|
+
| Event | What OneAddress sends | What you must do |
|
|
1239
|
+
|-------|-----------------------|------------------|
|
|
1240
|
+
| \`address.updated\` | Encrypted new address | Decrypt \u2192 \`saveAddress()\` \u2192 return 200 |
|
|
1241
|
+
| \`address.verify\` | Encrypted address to check | Decrypt \u2192 \`verifyAddress()\` \u2192 POST result to \`callback_url\` |
|
|
1242
|
+
|
|
1243
|
+
Valid verify results: \`"match"\` \xB7 \`"mismatch"\` \xB7 \`"not_found"\`
|
|
1244
|
+
|
|
1245
|
+
## Conformance check
|
|
1246
|
+
|
|
1247
|
+
\`\`\`bash
|
|
1248
|
+
npm test
|
|
1249
|
+
# or
|
|
1250
|
+
npx @oneaddress/conformance test %%WEBHOOK_URL%%
|
|
1251
|
+
\`\`\`
|
|
1252
|
+
|
|
1253
|
+
## Configuration
|
|
1254
|
+
|
|
1255
|
+
All credentials are in \`.env\` (written by the setup wizard \u2014 never commit it).
|
|
1256
|
+
Rotate keys and update your webhook URL at [partners.oneaddress.io](https://partners.oneaddress.io).
|
|
1257
|
+
`
|
|
1258
|
+
}
|
|
1259
|
+
],
|
|
1260
|
+
// ── Python (FastAPI) ───────────────────────────────────────────────────────
|
|
1261
|
+
python: [
|
|
1262
|
+
{
|
|
1263
|
+
name: ".env",
|
|
1264
|
+
content: `OA_PARTNER_ID=%%PARTNER_ID%%
|
|
1265
|
+
OA_WEBHOOK_SECRET=%%WEBHOOK_SECRET%%
|
|
1266
|
+
OA_PRIVATE_KEY_PEM=%%PRIVATE_KEY%%
|
|
1267
|
+
`
|
|
1268
|
+
},
|
|
1269
|
+
{
|
|
1270
|
+
name: "requirements.txt",
|
|
1271
|
+
content: `fastapi>=0.110.0
|
|
1272
|
+
uvicorn[standard]>=0.27.0
|
|
1273
|
+
python-dotenv>=1.0.0
|
|
1274
|
+
cryptography>=42.0.0
|
|
1275
|
+
httpx>=0.27.0
|
|
1276
|
+
`
|
|
1277
|
+
},
|
|
1278
|
+
{
|
|
1279
|
+
name: "app.py",
|
|
1280
|
+
content: `"""
|
|
1281
|
+
OneAddress Webhook Handler \u2014 FastAPI
|
|
1282
|
+
Generated by npx @oneaddress/setup for %%PARTNER_ID%%
|
|
1283
|
+
|
|
1284
|
+
All crypto inline \u2014 no OneAddress Python SDK required.
|
|
1285
|
+
Requires: cryptography>=42, httpx>=0.27, fastapi, uvicorn, python-dotenv
|
|
1286
|
+
|
|
1287
|
+
Quick start:
|
|
1288
|
+
pip install -r requirements.txt
|
|
1289
|
+
uvicorn app:app --port 3001
|
|
1290
|
+
"""
|
|
1291
|
+
|
|
1292
|
+
import base64
|
|
1293
|
+
import hashlib
|
|
1294
|
+
import hmac as hmac_lib
|
|
1295
|
+
import json
|
|
1296
|
+
import os
|
|
1297
|
+
import time
|
|
1298
|
+
from typing import Any
|
|
1299
|
+
|
|
1300
|
+
import httpx
|
|
1301
|
+
from cryptography.hazmat.backends import default_backend
|
|
1302
|
+
from cryptography.hazmat.primitives.asymmetric.ec import (
|
|
1303
|
+
ECDH,
|
|
1304
|
+
EllipticCurvePublicNumbers,
|
|
1305
|
+
SECP256R1,
|
|
1306
|
+
)
|
|
1307
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
1308
|
+
from cryptography.hazmat.primitives.hashes import SHA256
|
|
1309
|
+
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
|
1310
|
+
from cryptography.hazmat.primitives.serialization import load_pem_private_key
|
|
1311
|
+
from dotenv import load_dotenv
|
|
1312
|
+
from fastapi import FastAPI, Request, Response
|
|
1313
|
+
|
|
1314
|
+
load_dotenv()
|
|
1315
|
+
|
|
1316
|
+
PARTNER_ID = os.environ["OA_PARTNER_ID"]
|
|
1317
|
+
WEBHOOK_SECRET = os.environ["OA_WEBHOOK_SECRET"]
|
|
1318
|
+
PRIVATE_KEY = os.environ["OA_PRIVATE_KEY_PEM"].replace("\\\\n", "\\n")
|
|
1319
|
+
|
|
1320
|
+
app = FastAPI()
|
|
1321
|
+
seen_dispatches: set[str] = set()
|
|
1322
|
+
|
|
1323
|
+
# \u2500\u2500 Crypto helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1324
|
+
|
|
1325
|
+
def _b64decode(s: str) -> bytes:
|
|
1326
|
+
s = s.replace("-", "+").replace("_", "/")
|
|
1327
|
+
return base64.b64decode(s + "=" * (-len(s) % 4))
|
|
1328
|
+
|
|
1329
|
+
def _verify_hmac(raw_body: str, signature: str, timestamp: str, secret: str) -> bool:
|
|
1330
|
+
expected = hmac_lib.new(
|
|
1331
|
+
secret.encode(), f"{timestamp}.{raw_body}".encode(), hashlib.sha256
|
|
1332
|
+
).hexdigest()
|
|
1333
|
+
try:
|
|
1334
|
+
return hmac_lib.compare_digest(expected, signature.lower())
|
|
1335
|
+
except Exception:
|
|
1336
|
+
return False
|
|
1337
|
+
|
|
1338
|
+
def _is_fresh(timestamp: str, tolerance: int = 300) -> bool:
|
|
1339
|
+
try:
|
|
1340
|
+
return abs(int(time.time()) - int(timestamp)) <= tolerance
|
|
1341
|
+
except ValueError:
|
|
1342
|
+
return False
|
|
1343
|
+
|
|
1344
|
+
def _decrypt_address(enc: dict[str, Any], private_key_pem: str, partner_id: str) -> dict[str, Any]:
|
|
1345
|
+
priv_key = load_pem_private_key(private_key_pem.encode(), password=None)
|
|
1346
|
+
|
|
1347
|
+
eph_raw = _b64decode(enc["ephemeralPublicKey"])
|
|
1348
|
+
assert len(eph_raw) == 65 and eph_raw[0] == 0x04, "Expected uncompressed P-256 point"
|
|
1349
|
+
x = int.from_bytes(eph_raw[1:33], "big")
|
|
1350
|
+
y = int.from_bytes(eph_raw[33:65], "big")
|
|
1351
|
+
eph_pub = EllipticCurvePublicNumbers(x, y, SECP256R1()).public_key(default_backend())
|
|
1352
|
+
|
|
1353
|
+
shared = priv_key.exchange(ECDH(), eph_pub) # type: ignore[arg-type]
|
|
1354
|
+
|
|
1355
|
+
hkdf_salt_b64 = enc.get("hkdfSalt")
|
|
1356
|
+
salt = _b64decode(hkdf_salt_b64) if hkdf_salt_b64 else b"\\x00" * 32
|
|
1357
|
+
aes_key = HKDF(
|
|
1358
|
+
algorithm=SHA256(),
|
|
1359
|
+
length=32,
|
|
1360
|
+
salt=salt,
|
|
1361
|
+
info=f"oneaddress:{partner_id}".encode(),
|
|
1362
|
+
backend=default_backend(),
|
|
1363
|
+
).derive(shared)
|
|
1364
|
+
|
|
1365
|
+
iv = _b64decode(enc["iv"])
|
|
1366
|
+
ciphertext_full = _b64decode(enc["ciphertext"]) # ciphertext || 16-byte auth tag
|
|
1367
|
+
plaintext = AESGCM(aes_key).decrypt(iv, ciphertext_full, None)
|
|
1368
|
+
return json.loads(plaintext) # type: ignore[return-value]
|
|
1369
|
+
|
|
1370
|
+
# \u2500\u2500 Webhook handler \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1371
|
+
|
|
1372
|
+
@app.post("/webhook")
|
|
1373
|
+
async def webhook(request: Request) -> Response:
|
|
1374
|
+
raw_body = (await request.body()).decode()
|
|
1375
|
+
headers = {k.lower(): v for k, v in request.headers.items()}
|
|
1376
|
+
timestamp = headers.get("x-oneaddress-timestamp", "")
|
|
1377
|
+
signature = headers.get("x-oneaddress-signature", "")
|
|
1378
|
+
dispatch = headers.get("x-oneaddress-dispatch", "")
|
|
1379
|
+
|
|
1380
|
+
if not _is_fresh(timestamp):
|
|
1381
|
+
return Response(status_code=400, content='{"error":"Stale timestamp"}',
|
|
1382
|
+
media_type="application/json")
|
|
1383
|
+
if not _verify_hmac(raw_body, signature, timestamp, WEBHOOK_SECRET):
|
|
1384
|
+
return Response(status_code=401, content='{"error":"Invalid signature"}',
|
|
1385
|
+
media_type="application/json")
|
|
1386
|
+
|
|
1387
|
+
body = json.loads(raw_body)
|
|
1388
|
+
event = body.get("event", "")
|
|
1389
|
+
|
|
1390
|
+
if dispatch and dispatch in seen_dispatches:
|
|
1391
|
+
return Response(content='{"ok":true,"duplicate":true}', media_type="application/json")
|
|
1392
|
+
if dispatch:
|
|
1393
|
+
seen_dispatches.add(dispatch)
|
|
1394
|
+
|
|
1395
|
+
enc = body.get("address_encrypted")
|
|
1396
|
+
if not enc:
|
|
1397
|
+
return Response(content='{"ok":true,"note":"no encrypted payload"}',
|
|
1398
|
+
media_type="application/json")
|
|
1399
|
+
|
|
1400
|
+
try:
|
|
1401
|
+
address = _decrypt_address(enc, PRIVATE_KEY, PARTNER_ID)
|
|
1402
|
+
except Exception as e:
|
|
1403
|
+
print(f"[webhook] Decryption error (check OA_PRIVATE_KEY_PEM): {e}")
|
|
1404
|
+
return Response(content='{"ok":true,"note":"received - decryption pending key setup"}',
|
|
1405
|
+
media_type="application/json")
|
|
1406
|
+
|
|
1407
|
+
if event == "address.updated":
|
|
1408
|
+
customer = body.get("customer", {})
|
|
1409
|
+
print(f"[webhook] Address for {customer.get('email', '?')}: "
|
|
1410
|
+
f"{address.get('street')}, {address.get('suburb')} "
|
|
1411
|
+
f"{address.get('state')} {address.get('postcode')}")
|
|
1412
|
+
# TODO: persist address to your database
|
|
1413
|
+
|
|
1414
|
+
elif event == "address.verify":
|
|
1415
|
+
customer = body.get("customer", {})
|
|
1416
|
+
callback_url = body["callback_url"]
|
|
1417
|
+
callback_token= body["callback_token"]
|
|
1418
|
+
batch_id = body["batch_id"]
|
|
1419
|
+
|
|
1420
|
+
# TODO: Replace "match" with a real DB lookup.
|
|
1421
|
+
# Query your records for this customer and compare addresses.
|
|
1422
|
+
# Valid results: "match" | "mismatch" | "not_found"
|
|
1423
|
+
result = "match"
|
|
1424
|
+
|
|
1425
|
+
payload = {
|
|
1426
|
+
"batch_id": batch_id,
|
|
1427
|
+
"partner_id": PARTNER_ID,
|
|
1428
|
+
"member_name": customer.get("name", ""),
|
|
1429
|
+
"result": result,
|
|
1430
|
+
"token": callback_token,
|
|
1431
|
+
}
|
|
1432
|
+
try:
|
|
1433
|
+
async with httpx.AsyncClient(timeout=10) as client:
|
|
1434
|
+
await client.post(callback_url, json=payload)
|
|
1435
|
+
except Exception as e:
|
|
1436
|
+
print(f"[webhook] Callback POST failed: {e}")
|
|
1437
|
+
|
|
1438
|
+
else:
|
|
1439
|
+
print(f"[webhook] Unknown event '{event}' \u2014 acknowledged")
|
|
1440
|
+
|
|
1441
|
+
return Response(content='{"ok":true}', media_type="application/json")
|
|
1442
|
+
|
|
1443
|
+
@app.get("/health")
|
|
1444
|
+
def health() -> dict[str, str]:
|
|
1445
|
+
return {"status": "ok"}
|
|
1446
|
+
`
|
|
1447
|
+
},
|
|
1448
|
+
{
|
|
1449
|
+
name: "README.md",
|
|
1450
|
+
content: `# OneAddress Webhook Handler (Python / FastAPI)
|
|
1451
|
+
|
|
1452
|
+
Generated by \`npx @oneaddress/setup\` for **%%PARTNER_ID%%**.
|
|
1453
|
+
|
|
1454
|
+
## Quick start
|
|
1455
|
+
|
|
1456
|
+
\`\`\`bash
|
|
1457
|
+
pip install -r requirements.txt
|
|
1458
|
+
uvicorn app:app --port 3001
|
|
1459
|
+
\`\`\`
|
|
1460
|
+
|
|
1461
|
+
Your webhook endpoint: \`POST http://localhost:3001/webhook\`
|
|
1462
|
+
|
|
1463
|
+
## Events handled
|
|
1464
|
+
|
|
1465
|
+
### address.updated
|
|
1466
|
+
Receive \u2192 verify HMAC \u2192 decrypt \u2192 persist to your DB.
|
|
1467
|
+
|
|
1468
|
+
### address.verify
|
|
1469
|
+
Receive \u2192 verify HMAC \u2192 decrypt \u2192 compare to your records \u2192 POST callback_url.
|
|
1470
|
+
|
|
1471
|
+
The stub in \`app.py\` always returns \`"match"\`. Replace it with a real DB lookup:
|
|
1472
|
+
|
|
1473
|
+
\`\`\`python
|
|
1474
|
+
# In the address.verify branch of app.py, replace:
|
|
1475
|
+
result = "match"
|
|
1476
|
+
|
|
1477
|
+
# With something like:
|
|
1478
|
+
record = db.get_customer(customer.get("email"))
|
|
1479
|
+
result = "not_found" if not record else (
|
|
1480
|
+
"match" if addresses_match(record.address, address) else "mismatch"
|
|
1481
|
+
)
|
|
1482
|
+
\`\`\`
|
|
1483
|
+
|
|
1484
|
+
Valid results: \`"match"\` | \`"mismatch"\` | \`"not_found"\`
|
|
1485
|
+
|
|
1486
|
+
## Run conformance check
|
|
1487
|
+
|
|
1488
|
+
\`\`\`bash
|
|
1489
|
+
npx @oneaddress/conformance test %%WEBHOOK_URL%%
|
|
1490
|
+
\`\`\`
|
|
1491
|
+
|
|
1492
|
+
## Configuration
|
|
1493
|
+
|
|
1494
|
+
Credentials are in \`.env\` (written by the setup wizard). Never commit \`.env\` to source control.
|
|
1495
|
+
`
|
|
1496
|
+
}
|
|
1497
|
+
],
|
|
1498
|
+
// ── Java (Spring Boot) ─────────────────────────────────────────────────────
|
|
1499
|
+
"java-spring": [
|
|
1500
|
+
{
|
|
1501
|
+
name: "pom.xml",
|
|
1502
|
+
content: `<?xml version="1.0" encoding="UTF-8"?>
|
|
1503
|
+
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
|
1504
|
+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
1505
|
+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
|
1506
|
+
https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
|
1507
|
+
<modelVersion>4.0.0</modelVersion>
|
|
1508
|
+
|
|
1509
|
+
<parent>
|
|
1510
|
+
<groupId>org.springframework.boot</groupId>
|
|
1511
|
+
<artifactId>spring-boot-starter-parent</artifactId>
|
|
1512
|
+
<version>3.2.4</version>
|
|
1513
|
+
<relativePath/>
|
|
1514
|
+
</parent>
|
|
1515
|
+
|
|
1516
|
+
<groupId>com.example</groupId>
|
|
1517
|
+
<artifactId>oneaddress-webhook</artifactId>
|
|
1518
|
+
<version>0.0.1-SNAPSHOT</version>
|
|
1519
|
+
<packaging>jar</packaging>
|
|
1520
|
+
|
|
1521
|
+
<name>OneAddress Webhook Handler</name>
|
|
1522
|
+
<description>Spring Boot webhook receiver for OneAddress partner integrations</description>
|
|
1523
|
+
|
|
1524
|
+
<properties>
|
|
1525
|
+
<java.version>17</java.version>
|
|
1526
|
+
</properties>
|
|
1527
|
+
|
|
1528
|
+
<dependencies>
|
|
1529
|
+
<dependency>
|
|
1530
|
+
<groupId>org.springframework.boot</groupId>
|
|
1531
|
+
<artifactId>spring-boot-starter-web</artifactId>
|
|
1532
|
+
</dependency>
|
|
1533
|
+
|
|
1534
|
+
<dependency>
|
|
1535
|
+
<groupId>org.springframework.boot</groupId>
|
|
1536
|
+
<artifactId>spring-boot-starter-test</artifactId>
|
|
1537
|
+
<scope>test</scope>
|
|
1538
|
+
</dependency>
|
|
1539
|
+
</dependencies>
|
|
1540
|
+
|
|
1541
|
+
<build>
|
|
1542
|
+
<plugins>
|
|
1543
|
+
<plugin>
|
|
1544
|
+
<groupId>org.springframework.boot</groupId>
|
|
1545
|
+
<artifactId>spring-boot-maven-plugin</artifactId>
|
|
1546
|
+
</plugin>
|
|
1547
|
+
</plugins>
|
|
1548
|
+
</build>
|
|
1549
|
+
</project>
|
|
1550
|
+
`
|
|
1551
|
+
},
|
|
1552
|
+
{
|
|
1553
|
+
name: "src/main/java/com/example/oneaddress/Application.java",
|
|
1554
|
+
content: `package com.example.oneaddress;
|
|
1555
|
+
|
|
1556
|
+
import org.springframework.boot.SpringApplication;
|
|
1557
|
+
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
|
1558
|
+
|
|
1559
|
+
@SpringBootApplication
|
|
1560
|
+
public class Application {
|
|
1561
|
+
public static void main(String[] args) {
|
|
1562
|
+
SpringApplication.run(Application.class, args);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
`
|
|
1566
|
+
},
|
|
1567
|
+
{
|
|
1568
|
+
name: "src/main/java/com/example/oneaddress/OneAddressVerifier.java",
|
|
1569
|
+
content: `package com.example.oneaddress;
|
|
1570
|
+
|
|
1571
|
+
import javax.crypto.Mac;
|
|
1572
|
+
import javax.crypto.spec.SecretKeySpec;
|
|
1573
|
+
import java.nio.charset.StandardCharsets;
|
|
1574
|
+
import java.security.MessageDigest;
|
|
1575
|
+
|
|
1576
|
+
/**
|
|
1577
|
+
* Inline HMAC-SHA256 signature verification for OneAddress webhooks.
|
|
1578
|
+
* No external SDK dependency \u2014 uses JDK 17 standard library only.
|
|
1579
|
+
*/
|
|
1580
|
+
public final class OneAddressVerifier {
|
|
1581
|
+
|
|
1582
|
+
private OneAddressVerifier() {}
|
|
1583
|
+
|
|
1584
|
+
public static boolean verify(String rawBody, String signature, String timestamp, String secret) {
|
|
1585
|
+
try {
|
|
1586
|
+
String payload = timestamp + "." + rawBody;
|
|
1587
|
+
Mac mac = Mac.getInstance("HmacSHA256");
|
|
1588
|
+
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
|
|
1589
|
+
byte[] expected = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
|
|
1590
|
+
byte[] received = hexToBytes(signature);
|
|
1591
|
+
return MessageDigest.isEqual(expected, received);
|
|
1592
|
+
} catch (Exception e) {
|
|
1593
|
+
return false;
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
public static boolean isFresh(String timestamp, int toleranceSecs) {
|
|
1598
|
+
try {
|
|
1599
|
+
long ts = Long.parseLong(timestamp);
|
|
1600
|
+
long now = System.currentTimeMillis() / 1000L;
|
|
1601
|
+
return Math.abs(now - ts) <= toleranceSecs;
|
|
1602
|
+
} catch (NumberFormatException e) {
|
|
1603
|
+
return false;
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
private static byte[] hexToBytes(String hex) {
|
|
1608
|
+
int len = hex.length();
|
|
1609
|
+
byte[] out = new byte[len / 2];
|
|
1610
|
+
for (int i = 0; i < len; i += 2) {
|
|
1611
|
+
out[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4)
|
|
1612
|
+
+ Character.digit(hex.charAt(i + 1), 16));
|
|
1613
|
+
}
|
|
1614
|
+
return out;
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
`
|
|
1618
|
+
},
|
|
1619
|
+
{
|
|
1620
|
+
name: "src/main/java/com/example/oneaddress/OneAddressDecryptor.java",
|
|
1621
|
+
content: `package com.example.oneaddress;
|
|
1622
|
+
|
|
1623
|
+
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
1624
|
+
|
|
1625
|
+
import javax.crypto.Cipher;
|
|
1626
|
+
import javax.crypto.KeyAgreement;
|
|
1627
|
+
import javax.crypto.Mac;
|
|
1628
|
+
import javax.crypto.spec.GCMParameterSpec;
|
|
1629
|
+
import javax.crypto.spec.SecretKeySpec;
|
|
1630
|
+
import java.nio.charset.StandardCharsets;
|
|
1631
|
+
import java.security.KeyFactory;
|
|
1632
|
+
import java.security.spec.PKCS8EncodedKeySpec;
|
|
1633
|
+
import java.security.spec.X509EncodedKeySpec;
|
|
1634
|
+
import java.util.Arrays;
|
|
1635
|
+
import java.util.Base64;
|
|
1636
|
+
import java.util.Map;
|
|
1637
|
+
|
|
1638
|
+
/**
|
|
1639
|
+
* Inline ECDH decryption for OneAddress encrypted address payloads.
|
|
1640
|
+
* No external SDK dependency \u2014 uses JDK 17 standard library only.
|
|
1641
|
+
*/
|
|
1642
|
+
public final class OneAddressDecryptor {
|
|
1643
|
+
|
|
1644
|
+
// P-256 SPKI DER prefix (26 bytes) \u2014 prepend to the 65-byte uncompressed point
|
|
1645
|
+
private static final byte[] SPKI_PREFIX = {
|
|
1646
|
+
(byte)0x30, (byte)0x59, (byte)0x30, (byte)0x13, (byte)0x06, (byte)0x07,
|
|
1647
|
+
(byte)0x2A, (byte)0x86, (byte)0x48, (byte)0xCE, (byte)0x3D, (byte)0x02,
|
|
1648
|
+
(byte)0x01, (byte)0x06, (byte)0x08, (byte)0x2A, (byte)0x86, (byte)0x48,
|
|
1649
|
+
(byte)0xCE, (byte)0x3D, (byte)0x03, (byte)0x01, (byte)0x07, (byte)0x03,
|
|
1650
|
+
(byte)0x42, (byte)0x00
|
|
1651
|
+
};
|
|
1652
|
+
|
|
1653
|
+
private static final ObjectMapper MAPPER = new ObjectMapper();
|
|
1654
|
+
|
|
1655
|
+
private OneAddressDecryptor() {}
|
|
1656
|
+
|
|
1657
|
+
@SuppressWarnings("unchecked")
|
|
1658
|
+
public static Map<String, Object> decrypt(
|
|
1659
|
+
Map<String, Object> addressEncrypted,
|
|
1660
|
+
String privateKeyPem,
|
|
1661
|
+
String partnerId) throws Exception {
|
|
1662
|
+
|
|
1663
|
+
// 1. Load private key
|
|
1664
|
+
String stripped = privateKeyPem
|
|
1665
|
+
.replaceAll("-----BEGIN [^-]+-----", "")
|
|
1666
|
+
.replaceAll("-----END [^-]+-----", "")
|
|
1667
|
+
.replaceAll("\\\\s+", "");
|
|
1668
|
+
byte[] der = Base64.getDecoder().decode(stripped);
|
|
1669
|
+
var privateKey = KeyFactory.getInstance("EC")
|
|
1670
|
+
.generatePrivate(new PKCS8EncodedKeySpec(der));
|
|
1671
|
+
|
|
1672
|
+
// 2. Load ephemeral public key (65-byte uncompressed P-256 point)
|
|
1673
|
+
String ephB64 = (String) addressEncrypted.get("ephemeralPublicKey");
|
|
1674
|
+
byte[] ephRaw = decodeBase64AnyVariant(ephB64);
|
|
1675
|
+
byte[] spkiBytes = concat(SPKI_PREFIX, ephRaw);
|
|
1676
|
+
var ephemeralKey = KeyFactory.getInstance("EC")
|
|
1677
|
+
.generatePublic(new X509EncodedKeySpec(spkiBytes));
|
|
1678
|
+
|
|
1679
|
+
// 3. ECDH shared secret
|
|
1680
|
+
KeyAgreement ka = KeyAgreement.getInstance("ECDH");
|
|
1681
|
+
ka.init(privateKey);
|
|
1682
|
+
ka.doPhase(ephemeralKey, true);
|
|
1683
|
+
byte[] sharedSecret = ka.generateSecret();
|
|
1684
|
+
if (sharedSecret.length > 32) sharedSecret = Arrays.copyOf(sharedSecret, 32);
|
|
1685
|
+
|
|
1686
|
+
// 4. HKDF-SHA256 (inline)
|
|
1687
|
+
String hkdfSaltB64 = (String) addressEncrypted.get("hkdfSalt");
|
|
1688
|
+
byte[] salt = (hkdfSaltB64 != null && !hkdfSaltB64.isEmpty())
|
|
1689
|
+
? Base64.getDecoder().decode(hkdfSaltB64)
|
|
1690
|
+
: new byte[32]; // 32 zero bytes when absent (WebCrypto default)
|
|
1691
|
+
|
|
1692
|
+
byte[] info = ("oneaddress:" + partnerId).getBytes(StandardCharsets.UTF_8);
|
|
1693
|
+
|
|
1694
|
+
Mac mac = Mac.getInstance("HmacSHA256");
|
|
1695
|
+
// Extract: PRK = HMAC-SHA256(salt, sharedSecret)
|
|
1696
|
+
mac.init(new SecretKeySpec(salt, "HmacSHA256"));
|
|
1697
|
+
byte[] prk = mac.doFinal(sharedSecret);
|
|
1698
|
+
// Expand T(1): aesKey = HMAC-SHA256(PRK, info || 0x01)
|
|
1699
|
+
mac.init(new SecretKeySpec(prk, "HmacSHA256"));
|
|
1700
|
+
byte[] aesKey = mac.doFinal(concat(info, new byte[]{1}));
|
|
1701
|
+
|
|
1702
|
+
// 5. AES-256-GCM decrypt (ciphertext || 16-byte tag)
|
|
1703
|
+
byte[] iv = decodeBase64AnyVariant((String) addressEncrypted.get("iv"));
|
|
1704
|
+
byte[] ciphertextWithTag = decodeBase64AnyVariant((String) addressEncrypted.get("ciphertext"));
|
|
1705
|
+
|
|
1706
|
+
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
|
1707
|
+
cipher.init(Cipher.DECRYPT_MODE,
|
|
1708
|
+
new SecretKeySpec(aesKey, "AES"),
|
|
1709
|
+
new GCMParameterSpec(128, iv));
|
|
1710
|
+
byte[] plaintext = cipher.doFinal(ciphertextWithTag);
|
|
1711
|
+
|
|
1712
|
+
// 6. Parse JSON
|
|
1713
|
+
return MAPPER.readValue(plaintext, Map.class);
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
private static byte[] decodeBase64AnyVariant(String s) {
|
|
1717
|
+
try {
|
|
1718
|
+
return Base64.getDecoder().decode(s);
|
|
1719
|
+
} catch (IllegalArgumentException e) {
|
|
1720
|
+
return Base64.getUrlDecoder().decode(s);
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
private static byte[] concat(byte[] a, byte[] b) {
|
|
1725
|
+
byte[] result = new byte[a.length + b.length];
|
|
1726
|
+
System.arraycopy(a, 0, result, 0, a.length);
|
|
1727
|
+
System.arraycopy(b, 0, result, a.length, b.length);
|
|
1728
|
+
return result;
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
`
|
|
1732
|
+
},
|
|
1733
|
+
{
|
|
1734
|
+
name: "src/main/java/com/example/oneaddress/OneAddressWebhookController.java",
|
|
1735
|
+
content: `package com.example.oneaddress;
|
|
1736
|
+
|
|
1737
|
+
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
1738
|
+
import jakarta.annotation.PostConstruct;
|
|
1739
|
+
import org.slf4j.Logger;
|
|
1740
|
+
import org.slf4j.LoggerFactory;
|
|
1741
|
+
import org.springframework.http.ResponseEntity;
|
|
1742
|
+
import org.springframework.web.bind.annotation.*;
|
|
1743
|
+
|
|
1744
|
+
import java.net.URI;
|
|
1745
|
+
import java.net.http.HttpClient;
|
|
1746
|
+
import java.net.http.HttpRequest;
|
|
1747
|
+
import java.net.http.HttpResponse;
|
|
1748
|
+
import java.util.Map;
|
|
1749
|
+
|
|
1750
|
+
/**
|
|
1751
|
+
* Webhook receiver for OneAddress address.updated and address.verify events.
|
|
1752
|
+
* Endpoint: POST /webhooks/oneaddress
|
|
1753
|
+
*
|
|
1754
|
+
* Set environment variables:
|
|
1755
|
+
* ONEADDRESS_WEBHOOK_SECRET \u2014 webhook secret from the Partner Portal
|
|
1756
|
+
* ONEADDRESS_PRIVATE_KEY \u2014 PKCS8 PEM private key (use \\n for newlines)
|
|
1757
|
+
* ONEADDRESS_PARTNER_ID \u2014 your partner UUID
|
|
1758
|
+
*/
|
|
1759
|
+
@RestController
|
|
1760
|
+
@RequestMapping("/webhooks")
|
|
1761
|
+
public class OneAddressWebhookController {
|
|
1762
|
+
|
|
1763
|
+
private static final Logger log = LoggerFactory.getLogger(OneAddressWebhookController.class);
|
|
1764
|
+
private static final ObjectMapper MAPPER = new ObjectMapper();
|
|
1765
|
+
|
|
1766
|
+
private String webhookSecret;
|
|
1767
|
+
private String privateKeyPem;
|
|
1768
|
+
private String partnerId;
|
|
1769
|
+
|
|
1770
|
+
@PostConstruct
|
|
1771
|
+
void init() {
|
|
1772
|
+
webhookSecret = System.getenv("ONEADDRESS_WEBHOOK_SECRET");
|
|
1773
|
+
if (webhookSecret == null || webhookSecret.isBlank())
|
|
1774
|
+
throw new IllegalStateException("ONEADDRESS_WEBHOOK_SECRET is required");
|
|
1775
|
+
|
|
1776
|
+
String rawKey = System.getenv("ONEADDRESS_PRIVATE_KEY");
|
|
1777
|
+
if (rawKey == null || rawKey.isBlank())
|
|
1778
|
+
throw new IllegalStateException("ONEADDRESS_PRIVATE_KEY is required");
|
|
1779
|
+
privateKeyPem = rawKey.replace("\\\\n", "\\n");
|
|
1780
|
+
|
|
1781
|
+
partnerId = System.getenv("ONEADDRESS_PARTNER_ID");
|
|
1782
|
+
if (partnerId == null || partnerId.isBlank())
|
|
1783
|
+
throw new IllegalStateException("ONEADDRESS_PARTNER_ID is required");
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
@PostMapping("/oneaddress")
|
|
1787
|
+
public ResponseEntity<String> receive(
|
|
1788
|
+
@RequestBody String rawBody,
|
|
1789
|
+
@RequestHeader Map<String, String> headers) {
|
|
1790
|
+
|
|
1791
|
+
try {
|
|
1792
|
+
String timestamp = getHeader(headers, "x-oneaddress-timestamp");
|
|
1793
|
+
String signature = getHeader(headers, "x-oneaddress-signature");
|
|
1794
|
+
|
|
1795
|
+
if (timestamp == null || signature == null)
|
|
1796
|
+
return ResponseEntity.badRequest().body("{\\"error\\":\\"missing required headers\\"}");
|
|
1797
|
+
|
|
1798
|
+
if (!OneAddressVerifier.isFresh(timestamp, 300))
|
|
1799
|
+
return ResponseEntity.badRequest().body("{\\"error\\":\\"stale timestamp\\"}");
|
|
1800
|
+
|
|
1801
|
+
if (!OneAddressVerifier.verify(rawBody, signature, timestamp, webhookSecret))
|
|
1802
|
+
return ResponseEntity.status(401).body("{\\"error\\":\\"invalid signature\\"}");
|
|
1803
|
+
|
|
1804
|
+
@SuppressWarnings("unchecked")
|
|
1805
|
+
Map<String, Object> body = MAPPER.readValue(rawBody, Map.class);
|
|
1806
|
+
String event = (String) body.get("event");
|
|
1807
|
+
|
|
1808
|
+
@SuppressWarnings("unchecked")
|
|
1809
|
+
Map<String, Object> enc = (Map<String, Object>) body.get("address_encrypted");
|
|
1810
|
+
if (enc == null)
|
|
1811
|
+
return ResponseEntity.ok("{\\"ok\\":true,\\"note\\":\\"no encrypted payload\\"}");
|
|
1812
|
+
|
|
1813
|
+
Map<String, Object> address = OneAddressDecryptor.decrypt(enc, privateKeyPem, partnerId);
|
|
1814
|
+
String dispatchId = getHeader(headers, "x-oneaddress-dispatch");
|
|
1815
|
+
|
|
1816
|
+
if ("address.updated".equals(event)) {
|
|
1817
|
+
handleAddressUpdate(address, dispatchId);
|
|
1818
|
+
} else if ("address.verify".equals(event)) {
|
|
1819
|
+
handleAddressVerify(body, address);
|
|
1820
|
+
} else {
|
|
1821
|
+
log.info("[OneAddress] skipping unknown event: {}", event);
|
|
1822
|
+
return ResponseEntity.ok("{\\"ok\\":true,\\"skipped\\":true}");
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
return ResponseEntity.ok("{\\"ok\\":true}");
|
|
1826
|
+
|
|
1827
|
+
} catch (Exception e) {
|
|
1828
|
+
log.error("[OneAddress] webhook processing error", e);
|
|
1829
|
+
return ResponseEntity.status(500).body("{\\"error\\":\\"internal error\\"}");
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
private void handleAddressUpdate(Map<String, Object> address, String dispatchId) {
|
|
1834
|
+
log.info("[OneAddress] address.updated dispatchId={}", dispatchId);
|
|
1835
|
+
log.info("[OneAddress] street={} suburb={} state={} postcode={}",
|
|
1836
|
+
address.get("street"), address.get("suburb"),
|
|
1837
|
+
address.get("state"), address.get("postcode"));
|
|
1838
|
+
|
|
1839
|
+
// TODO: persist the new address to your database
|
|
1840
|
+
// Deduplicate on dispatchId for at-least-once delivery:
|
|
1841
|
+
// if (dispatchRepo.existsByDispatchId(dispatchId)) return;
|
|
1842
|
+
// customerRepo.updateAddress(address);
|
|
1843
|
+
// dispatchRepo.markSeen(dispatchId);
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
@SuppressWarnings("unchecked")
|
|
1847
|
+
private void handleAddressVerify(Map<String, Object> body, Map<String, Object> address) {
|
|
1848
|
+
Map<String, Object> customer = (Map<String, Object>) body.getOrDefault("customer", Map.of());
|
|
1849
|
+
String callbackUrl = (String) body.get("callback_url");
|
|
1850
|
+
String callbackToken = (String) body.get("callback_token");
|
|
1851
|
+
String batchId = (String) body.get("batch_id");
|
|
1852
|
+
String memberName = (String) customer.getOrDefault("name", "");
|
|
1853
|
+
|
|
1854
|
+
// TODO: Replace "match" with a real DB lookup.
|
|
1855
|
+
// Valid results: "match" | "mismatch" | "not_found"
|
|
1856
|
+
String result = "match";
|
|
1857
|
+
|
|
1858
|
+
try {
|
|
1859
|
+
Map<String, Object> payload = Map.of(
|
|
1860
|
+
"batch_id", batchId != null ? batchId : "",
|
|
1861
|
+
"partner_id", partnerId,
|
|
1862
|
+
"member_name", memberName,
|
|
1863
|
+
"result", result,
|
|
1864
|
+
"token", callbackToken != null ? callbackToken : ""
|
|
1865
|
+
);
|
|
1866
|
+
String payloadJson = MAPPER.writeValueAsString(payload);
|
|
1867
|
+
HttpClient.newHttpClient().send(
|
|
1868
|
+
HttpRequest.newBuilder()
|
|
1869
|
+
.uri(URI.create(callbackUrl))
|
|
1870
|
+
.header("Content-Type", "application/json")
|
|
1871
|
+
.POST(HttpRequest.BodyPublishers.ofString(payloadJson))
|
|
1872
|
+
.build(),
|
|
1873
|
+
HttpResponse.BodyHandlers.discarding()
|
|
1874
|
+
);
|
|
1875
|
+
} catch (Exception e) {
|
|
1876
|
+
log.error("[OneAddress] address.verify callback POST failed", e);
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
private static String getHeader(Map<String, String> headers, String name) {
|
|
1881
|
+
for (Map.Entry<String, String> entry : headers.entrySet())
|
|
1882
|
+
if (entry.getKey().equalsIgnoreCase(name)) return entry.getValue();
|
|
1883
|
+
return null;
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
`
|
|
1887
|
+
},
|
|
1888
|
+
{
|
|
1889
|
+
name: "src/main/java/com/example/oneaddress/HealthController.java",
|
|
1890
|
+
content: `package com.example.oneaddress;
|
|
1891
|
+
|
|
1892
|
+
import org.springframework.http.ResponseEntity;
|
|
1893
|
+
import org.springframework.web.bind.annotation.GetMapping;
|
|
1894
|
+
import org.springframework.web.bind.annotation.RestController;
|
|
1895
|
+
|
|
1896
|
+
import java.util.Map;
|
|
1897
|
+
|
|
1898
|
+
@RestController
|
|
1899
|
+
public class HealthController {
|
|
1900
|
+
@GetMapping("/health")
|
|
1901
|
+
public ResponseEntity<Map<String, String>> health() {
|
|
1902
|
+
return ResponseEntity.ok(Map.of("status", "ok"));
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
`
|
|
1906
|
+
},
|
|
1907
|
+
{
|
|
1908
|
+
name: "src/main/resources/application.properties",
|
|
1909
|
+
content: `server.port=3001
|
|
1910
|
+
`
|
|
1911
|
+
},
|
|
1912
|
+
{
|
|
1913
|
+
name: "README.md",
|
|
1914
|
+
content: `# OneAddress Webhook Handler (Java / Spring Boot)
|
|
1915
|
+
|
|
1916
|
+
Generated by \`npx @oneaddress/setup\` for **%%PARTNER_ID%%**.
|
|
1917
|
+
Inline crypto only \u2014 no external SDK dependency. Requires Java 17+.
|
|
1918
|
+
|
|
1919
|
+
## Quick start
|
|
1920
|
+
|
|
1921
|
+
\`\`\`bash
|
|
1922
|
+
export ONEADDRESS_WEBHOOK_SECRET=%%WEBHOOK_SECRET%%
|
|
1923
|
+
export ONEADDRESS_PRIVATE_KEY="%%PRIVATE_KEY%%"
|
|
1924
|
+
export ONEADDRESS_PARTNER_ID=%%PARTNER_ID%%
|
|
1925
|
+
|
|
1926
|
+
./mvnw spring-boot:run
|
|
1927
|
+
\`\`\`
|
|
1928
|
+
|
|
1929
|
+
Endpoints:
|
|
1930
|
+
- \`POST http://localhost:3001/webhooks/oneaddress\` \u2014 webhook receiver
|
|
1931
|
+
- \`GET http://localhost:3001/health\` \u2014 health check
|
|
1932
|
+
|
|
1933
|
+
## Events handled
|
|
1934
|
+
|
|
1935
|
+
### address.updated
|
|
1936
|
+
Receive \u2192 verify HMAC \u2192 decrypt \u2192 persist to your DB.
|
|
1937
|
+
|
|
1938
|
+
### address.verify
|
|
1939
|
+
Receive \u2192 verify HMAC \u2192 decrypt \u2192 compare to records \u2192 POST callback_url.
|
|
1940
|
+
|
|
1941
|
+
Find the \`TODO: Replace "match"\` comment in \`OneAddressWebhookController.java\`
|
|
1942
|
+
and add your DB lookup there.
|
|
1943
|
+
|
|
1944
|
+
| Env var | Description |
|
|
1945
|
+
|---------|-------------|
|
|
1946
|
+
| \`ONEADDRESS_WEBHOOK_SECRET\` | Webhook secret from the Partner Portal |
|
|
1947
|
+
| \`ONEADDRESS_PRIVATE_KEY\` | PKCS#8 PEM private key (use \`\\n\` between lines) |
|
|
1948
|
+
| \`ONEADDRESS_PARTNER_ID\` | Your partner UUID |
|
|
1949
|
+
|
|
1950
|
+
## Run conformance check
|
|
1951
|
+
|
|
1952
|
+
\`\`\`bash
|
|
1953
|
+
npx @oneaddress/conformance test http://localhost:3001/webhooks/oneaddress
|
|
1954
|
+
\`\`\`
|
|
1955
|
+
`
|
|
1956
|
+
}
|
|
1957
|
+
],
|
|
1958
|
+
// ── C# (ASP.NET Core) ──────────────────────────────────────────────────────
|
|
1959
|
+
"csharp-aspnet": [
|
|
1960
|
+
{
|
|
1961
|
+
name: "OneAddressWebhook.csproj",
|
|
1962
|
+
content: `<Project Sdk="Microsoft.NET.Sdk.Web">
|
|
1963
|
+
<PropertyGroup>
|
|
1964
|
+
<TargetFramework>net8.0</TargetFramework>
|
|
1965
|
+
<Nullable>enable</Nullable>
|
|
1966
|
+
<ImplicitUsings>enable</ImplicitUsings>
|
|
1967
|
+
</PropertyGroup>
|
|
1968
|
+
</Project>
|
|
1969
|
+
`
|
|
1970
|
+
},
|
|
1971
|
+
{
|
|
1972
|
+
name: "appsettings.json",
|
|
1973
|
+
content: `{
|
|
1974
|
+
"Logging": { "LogLevel": { "Default": "Information" } },
|
|
1975
|
+
"AllowedHosts": "*"
|
|
1976
|
+
}
|
|
1977
|
+
`
|
|
1978
|
+
},
|
|
1979
|
+
{
|
|
1980
|
+
name: "Program.cs",
|
|
1981
|
+
content: `// OneAddress webhook handler \u2014 ASP.NET Core minimal API
|
|
1982
|
+
// Inline crypto only \u2014 no external SDK dependency. Requires .NET 8+.
|
|
1983
|
+
|
|
1984
|
+
using System.Net.Http;
|
|
1985
|
+
using System.Security.Cryptography;
|
|
1986
|
+
using System.Text;
|
|
1987
|
+
using System.Text.Json;
|
|
1988
|
+
|
|
1989
|
+
// \u2500\u2500 Read config from env \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1990
|
+
|
|
1991
|
+
var webhookSecret = Environment.GetEnvironmentVariable("ONEADDRESS_WEBHOOK_SECRET")
|
|
1992
|
+
?? throw new InvalidOperationException("ONEADDRESS_WEBHOOK_SECRET is required");
|
|
1993
|
+
var privateKeyPem = (Environment.GetEnvironmentVariable("ONEADDRESS_PRIVATE_KEY") ?? "")
|
|
1994
|
+
.Replace("\\\\n", "\\n");
|
|
1995
|
+
var partnerId = Environment.GetEnvironmentVariable("ONEADDRESS_PARTNER_ID") ?? "";
|
|
1996
|
+
|
|
1997
|
+
var builder = WebApplication.CreateBuilder(args);
|
|
1998
|
+
var app = builder.Build();
|
|
1999
|
+
|
|
2000
|
+
// \u2500\u2500 Webhook endpoint \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2001
|
+
|
|
2002
|
+
app.MapPost("/webhooks/oneaddress", async (HttpRequest request) =>
|
|
2003
|
+
{
|
|
2004
|
+
using var reader = new StreamReader(request.Body);
|
|
2005
|
+
string rawBody = await reader.ReadToEndAsync();
|
|
2006
|
+
|
|
2007
|
+
var headers = request.Headers.ToDictionary(h => h.Key, h => h.Value.ToString(),
|
|
2008
|
+
StringComparer.OrdinalIgnoreCase);
|
|
2009
|
+
|
|
2010
|
+
if (!headers.TryGetValue("x-oneaddress-timestamp", out var timestamp) ||
|
|
2011
|
+
!headers.TryGetValue("x-oneaddress-signature", out var signature))
|
|
2012
|
+
return Results.Json(new { error = "missing required headers" }, statusCode: 400);
|
|
2013
|
+
|
|
2014
|
+
if (!IsFresh(timestamp))
|
|
2015
|
+
return Results.Json(new { error = "stale timestamp" }, statusCode: 400);
|
|
2016
|
+
|
|
2017
|
+
if (!VerifySignature(rawBody, signature, timestamp, webhookSecret))
|
|
2018
|
+
return Results.Json(new { error = "invalid signature" }, statusCode: 401);
|
|
2019
|
+
|
|
2020
|
+
JsonElement body;
|
|
2021
|
+
try { body = JsonSerializer.Deserialize<JsonElement>(rawBody); }
|
|
2022
|
+
catch { return Results.Json(new { error = "invalid json" }, statusCode: 400); }
|
|
2023
|
+
|
|
2024
|
+
var eventType = body.TryGetProperty("event", out var evtEl) ? evtEl.GetString() : null;
|
|
2025
|
+
|
|
2026
|
+
if (eventType != "address.updated" && eventType != "address.verify")
|
|
2027
|
+
{
|
|
2028
|
+
app.Logger.LogInformation("[OneAddress] skipping unknown event: {EventType}", eventType);
|
|
2029
|
+
return Results.Json(new { ok = true, skipped = true });
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
if (!body.TryGetProperty("address_encrypted", out var encEl))
|
|
2033
|
+
return Results.Json(new { ok = true, note = "no encrypted payload" }, statusCode: 200);
|
|
2034
|
+
|
|
2035
|
+
byte[] plaintext;
|
|
2036
|
+
try { plaintext = DecryptAddress(encEl, privateKeyPem, partnerId); }
|
|
2037
|
+
catch (Exception ex)
|
|
2038
|
+
{
|
|
2039
|
+
app.Logger.LogError(ex, "[OneAddress] decryption failed \u2014 check ONEADDRESS_PRIVATE_KEY");
|
|
2040
|
+
return Results.Json(new { ok = true, note = "received - decryption pending key setup" }, statusCode: 200);
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
var address = JsonSerializer.Deserialize<JsonElement>(plaintext);
|
|
2044
|
+
var dispatchId = headers.TryGetValue("x-oneaddress-dispatch", out var dVal) ? dVal : null;
|
|
2045
|
+
|
|
2046
|
+
if (eventType == "address.updated")
|
|
2047
|
+
{
|
|
2048
|
+
HandleAddressUpdate(address, dispatchId);
|
|
2049
|
+
}
|
|
2050
|
+
else // address.verify
|
|
2051
|
+
{
|
|
2052
|
+
await HandleAddressVerify(body, partnerId, app.Logger);
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
return Results.Json(new { ok = true });
|
|
2056
|
+
});
|
|
2057
|
+
|
|
2058
|
+
app.MapGet("/health", () => Results.Ok(new { status = "ok" }));
|
|
2059
|
+
|
|
2060
|
+
app.Run("http://localhost:3001");
|
|
2061
|
+
|
|
2062
|
+
// \u2500\u2500 Crypto helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2063
|
+
|
|
2064
|
+
static bool VerifySignature(string rawBody, string signature, string timestamp, string secret)
|
|
2065
|
+
{
|
|
2066
|
+
try
|
|
2067
|
+
{
|
|
2068
|
+
var payload = Encoding.UTF8.GetBytes($"{timestamp}.{rawBody}");
|
|
2069
|
+
var keyBytes = Encoding.UTF8.GetBytes(secret);
|
|
2070
|
+
var expected = Convert.ToHexString(HMACSHA256.HashData(keyBytes, payload)).ToLowerInvariant();
|
|
2071
|
+
var sigBytes = Convert.FromHexString(signature);
|
|
2072
|
+
var expBytes = Convert.FromHexString(expected);
|
|
2073
|
+
return sigBytes.Length == expBytes.Length
|
|
2074
|
+
&& CryptographicOperations.FixedTimeEquals(sigBytes, expBytes);
|
|
2075
|
+
}
|
|
2076
|
+
catch { return false; }
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
static bool IsFresh(string timestamp, int toleranceSecs = 300)
|
|
2080
|
+
{
|
|
2081
|
+
if (!long.TryParse(timestamp, out var ts)) return false;
|
|
2082
|
+
return Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - ts) <= toleranceSecs;
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
static byte[] DecryptAddress(JsonElement addressEncrypted, string privateKeyPem, string partnerId)
|
|
2086
|
+
{
|
|
2087
|
+
ReadOnlySpan<byte> spkiPrefix = new byte[]
|
|
2088
|
+
{
|
|
2089
|
+
0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01,
|
|
2090
|
+
0x06, 0x08, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00
|
|
2091
|
+
};
|
|
2092
|
+
|
|
2093
|
+
var pemLines = privateKeyPem
|
|
2094
|
+
.Split('\\n')
|
|
2095
|
+
.Where(l => !l.StartsWith("-----"))
|
|
2096
|
+
.Select(l => l.Trim());
|
|
2097
|
+
var derBytes = Convert.FromBase64String(string.Concat(pemLines));
|
|
2098
|
+
|
|
2099
|
+
using var ecdh = ECDiffieHellman.Create();
|
|
2100
|
+
ecdh.ImportPkcs8PrivateKey(derBytes, out _);
|
|
2101
|
+
|
|
2102
|
+
var ephB64 = addressEncrypted.GetProperty("ephemeralPublicKey").GetString()!;
|
|
2103
|
+
var ephRaw = Convert.FromBase64String(NormBase64(ephB64));
|
|
2104
|
+
|
|
2105
|
+
var spkiBytes = new byte[spkiPrefix.Length + ephRaw.Length];
|
|
2106
|
+
spkiPrefix.CopyTo(spkiBytes);
|
|
2107
|
+
ephRaw.CopyTo(spkiBytes, spkiPrefix.Length);
|
|
2108
|
+
|
|
2109
|
+
using var tempEcdh = ECDiffieHellman.Create();
|
|
2110
|
+
tempEcdh.ImportSubjectPublicKeyInfo(spkiBytes, out _);
|
|
2111
|
+
var ephemeralPub = tempEcdh.PublicKey;
|
|
2112
|
+
|
|
2113
|
+
var sharedSecret = ecdh.DeriveRawSecretAgreement(ephemeralPub);
|
|
2114
|
+
|
|
2115
|
+
string? hkdfSaltB64 = addressEncrypted.TryGetProperty("hkdfSalt", out var saltEl)
|
|
2116
|
+
? saltEl.GetString() : null;
|
|
2117
|
+
var salt = !string.IsNullOrEmpty(hkdfSaltB64)
|
|
2118
|
+
? Convert.FromBase64String(NormBase64(hkdfSaltB64))
|
|
2119
|
+
: new byte[32];
|
|
2120
|
+
|
|
2121
|
+
var info = Encoding.UTF8.GetBytes($"oneaddress:{partnerId}");
|
|
2122
|
+
var aesKey = HKDF.DeriveKey(HashAlgorithmName.SHA256, sharedSecret, 32, salt, info);
|
|
2123
|
+
|
|
2124
|
+
var iv = Convert.FromBase64String(NormBase64(
|
|
2125
|
+
addressEncrypted.GetProperty("iv").GetString()!));
|
|
2126
|
+
var ciphertextWithTag = Convert.FromBase64String(NormBase64(
|
|
2127
|
+
addressEncrypted.GetProperty("ciphertext").GetString()!));
|
|
2128
|
+
|
|
2129
|
+
var tag = ciphertextWithTag[^16..];
|
|
2130
|
+
var ciphertext = ciphertextWithTag[..^16];
|
|
2131
|
+
var plaintext = new byte[ciphertext.Length];
|
|
2132
|
+
|
|
2133
|
+
using var aesGcm = new AesGcm(aesKey, 16);
|
|
2134
|
+
aesGcm.Decrypt(iv, ciphertext, tag, plaintext);
|
|
2135
|
+
|
|
2136
|
+
return plaintext;
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
static string NormBase64(string s)
|
|
2140
|
+
{
|
|
2141
|
+
s = s.Replace('-', '+').Replace('_', '/');
|
|
2142
|
+
return (s.Length % 4) switch { 2 => s + "==", 3 => s + "=", _ => s };
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
void HandleAddressUpdate(JsonElement address, string? dispatchId)
|
|
2146
|
+
{
|
|
2147
|
+
app.Logger.LogInformation("[OneAddress] address.updated dispatchId={DispatchId}", dispatchId);
|
|
2148
|
+
|
|
2149
|
+
// TODO: persist the new address to your database
|
|
2150
|
+
// Deduplicate on dispatchId for at-least-once delivery:
|
|
2151
|
+
// if (db.Dispatches.Any(d => d.DispatchId == dispatchId)) return;
|
|
2152
|
+
// db.Customers.UpdateAddress(address);
|
|
2153
|
+
// db.Dispatches.Add(new SeenDispatch { DispatchId = dispatchId });
|
|
2154
|
+
// db.SaveChanges();
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
async Task HandleAddressVerify(JsonElement body, string pid, ILogger logger)
|
|
2158
|
+
{
|
|
2159
|
+
var callbackUrl = body.TryGetProperty("callback_url", out var cuEl) ? cuEl.GetString() : null;
|
|
2160
|
+
var callbackToken = body.TryGetProperty("callback_token", out var ctEl) ? ctEl.GetString() : null;
|
|
2161
|
+
var batchId = body.TryGetProperty("batch_id", out var biEl) ? biEl.GetString() : null;
|
|
2162
|
+
var memberName = body.TryGetProperty("customer", out var custEl)
|
|
2163
|
+
&& custEl.TryGetProperty("name", out var nameEl) ? nameEl.GetString() ?? "" : "";
|
|
2164
|
+
|
|
2165
|
+
// TODO: Replace "match" with a real DB lookup.
|
|
2166
|
+
// Valid results: "match" | "mismatch" | "not_found"
|
|
2167
|
+
var result = "match";
|
|
2168
|
+
|
|
2169
|
+
if (string.IsNullOrEmpty(callbackUrl)) return;
|
|
2170
|
+
try
|
|
2171
|
+
{
|
|
2172
|
+
var payload = JsonSerializer.Serialize(new {
|
|
2173
|
+
batch_id = batchId ?? "",
|
|
2174
|
+
partner_id = pid,
|
|
2175
|
+
member_name = memberName,
|
|
2176
|
+
result,
|
|
2177
|
+
token = callbackToken ?? "",
|
|
2178
|
+
});
|
|
2179
|
+
using var http = new HttpClient();
|
|
2180
|
+
using var content = new StringContent(payload, Encoding.UTF8, "application/json");
|
|
2181
|
+
await http.PostAsync(callbackUrl, content);
|
|
2182
|
+
}
|
|
2183
|
+
catch (Exception ex)
|
|
2184
|
+
{
|
|
2185
|
+
logger.LogError(ex, "[OneAddress] address.verify callback POST failed");
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
`
|
|
2189
|
+
},
|
|
2190
|
+
{
|
|
2191
|
+
name: "README.md",
|
|
2192
|
+
content: `# OneAddress Webhook Handler (C# / ASP.NET Core)
|
|
2193
|
+
|
|
2194
|
+
Generated by \`npx @oneaddress/setup\` for **%%PARTNER_ID%%**.
|
|
2195
|
+
Inline crypto only \u2014 no external SDK dependency. Requires .NET 8+.
|
|
2196
|
+
|
|
2197
|
+
## Quick start
|
|
2198
|
+
|
|
2199
|
+
\`\`\`bash
|
|
2200
|
+
export ONEADDRESS_WEBHOOK_SECRET=%%WEBHOOK_SECRET%%
|
|
2201
|
+
export ONEADDRESS_PRIVATE_KEY="%%PRIVATE_KEY%%"
|
|
2202
|
+
export ONEADDRESS_PARTNER_ID=%%PARTNER_ID%%
|
|
2203
|
+
|
|
2204
|
+
dotnet run
|
|
2205
|
+
\`\`\`
|
|
2206
|
+
|
|
2207
|
+
Endpoints:
|
|
2208
|
+
- \`POST http://localhost:3001/webhooks/oneaddress\` \u2014 webhook receiver
|
|
2209
|
+
- \`GET http://localhost:3001/health\` \u2014 health check
|
|
2210
|
+
|
|
2211
|
+
## Events handled
|
|
2212
|
+
|
|
2213
|
+
### address.updated
|
|
2214
|
+
Receive \u2192 verify HMAC \u2192 decrypt \u2192 persist to your DB.
|
|
2215
|
+
|
|
2216
|
+
### address.verify
|
|
2217
|
+
Receive \u2192 verify HMAC \u2192 decrypt \u2192 compare to records \u2192 POST callback_url.
|
|
2218
|
+
|
|
2219
|
+
Find \`TODO: Replace "match"\` in \`Program.cs\` \u2192 \`HandleAddressVerify\` and add your DB lookup.
|
|
2220
|
+
|
|
2221
|
+
| Env var | Description |
|
|
2222
|
+
|---------|-------------|
|
|
2223
|
+
| \`ONEADDRESS_WEBHOOK_SECRET\` | Webhook secret from the Partner Portal |
|
|
2224
|
+
| \`ONEADDRESS_PRIVATE_KEY\` | PKCS#8 PEM key \u2014 use \`\\n\` between PEM lines |
|
|
2225
|
+
| \`ONEADDRESS_PARTNER_ID\` | Your partner UUID |
|
|
2226
|
+
|
|
2227
|
+
## Run conformance check
|
|
2228
|
+
|
|
2229
|
+
\`\`\`bash
|
|
2230
|
+
npx @oneaddress/conformance test http://localhost:3001/webhooks/oneaddress
|
|
2231
|
+
\`\`\`
|
|
2232
|
+
`
|
|
2233
|
+
}
|
|
2234
|
+
],
|
|
2235
|
+
// ── Go (net/http) ─────────────────────────────────────────────────────────
|
|
2236
|
+
"go-http": [
|
|
2237
|
+
{
|
|
2238
|
+
name: ".env",
|
|
2239
|
+
content: `PARTNER_ID=%%PARTNER_ID%%
|
|
2240
|
+
WEBHOOK_SECRET=%%WEBHOOK_SECRET%%
|
|
2241
|
+
PARTNER_PRIVATE_KEY_PEM=%%PRIVATE_KEY%%
|
|
2242
|
+
PORT=3001
|
|
2243
|
+
`
|
|
2244
|
+
},
|
|
2245
|
+
{
|
|
2246
|
+
name: ".env.example",
|
|
2247
|
+
content: `# OneAddress webhook credentials \u2014 copy to .env and fill in values.
|
|
2248
|
+
# Never commit .env to source control.
|
|
2249
|
+
|
|
2250
|
+
# Your partner UUID from the OneAddress Partner Portal
|
|
2251
|
+
PARTNER_ID=your-partner-uuid-here
|
|
2252
|
+
|
|
2253
|
+
# HMAC-SHA256 webhook signing secret from the Partner Portal
|
|
2254
|
+
WEBHOOK_SECRET=your-webhook-secret-here
|
|
2255
|
+
|
|
2256
|
+
# PKCS#8 PEM private key for decrypting address payloads.
|
|
2257
|
+
# Use literal \\n between PEM lines in a single-line env var.
|
|
2258
|
+
# Example: -----BEGIN PRIVATE KEY-----\\nMIGH...\\n-----END PRIVATE KEY-----
|
|
2259
|
+
PARTNER_PRIVATE_KEY_PEM=
|
|
2260
|
+
|
|
2261
|
+
# HTTP port (default 3001)
|
|
2262
|
+
PORT=3001
|
|
2263
|
+
`
|
|
2264
|
+
},
|
|
2265
|
+
{
|
|
2266
|
+
name: "go.mod",
|
|
2267
|
+
content: `module oneaddress-webhook
|
|
2268
|
+
|
|
2269
|
+
go 1.21
|
|
2270
|
+
`
|
|
2271
|
+
},
|
|
2272
|
+
{
|
|
2273
|
+
name: "main.go",
|
|
2274
|
+
content: `// OneAddress Webhook Handler \u2014 Go (net/http)
|
|
2275
|
+
// Generated by npx @oneaddress/setup for %%PARTNER_ID%%
|
|
2276
|
+
//
|
|
2277
|
+
// All crypto is inline \u2014 no external SDK required. Requires Go 1.21+.
|
|
2278
|
+
|
|
2279
|
+
package main
|
|
2280
|
+
|
|
2281
|
+
import (
|
|
2282
|
+
"bytes"
|
|
2283
|
+
"crypto/aes"
|
|
2284
|
+
"crypto/cipher"
|
|
2285
|
+
"crypto/ecdh"
|
|
2286
|
+
"crypto/ecdsa"
|
|
2287
|
+
"crypto/hmac"
|
|
2288
|
+
"crypto/sha256"
|
|
2289
|
+
"crypto/subtle"
|
|
2290
|
+
"crypto/x509"
|
|
2291
|
+
"encoding/base64"
|
|
2292
|
+
"encoding/hex"
|
|
2293
|
+
"encoding/json"
|
|
2294
|
+
"encoding/pem"
|
|
2295
|
+
"io"
|
|
2296
|
+
"log"
|
|
2297
|
+
"net/http"
|
|
2298
|
+
"os"
|
|
2299
|
+
"strconv"
|
|
2300
|
+
"strings"
|
|
2301
|
+
"sync"
|
|
2302
|
+
"time"
|
|
2303
|
+
)
|
|
2304
|
+
|
|
2305
|
+
// \u2500\u2500 Config \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2306
|
+
|
|
2307
|
+
func main() {
|
|
2308
|
+
webhookSecret := os.Getenv("WEBHOOK_SECRET")
|
|
2309
|
+
privateKeyPEM := strings.ReplaceAll(os.Getenv("PARTNER_PRIVATE_KEY_PEM"), \`\\n\`, "\\n")
|
|
2310
|
+
partnerID := os.Getenv("PARTNER_ID")
|
|
2311
|
+
port := os.Getenv("PORT")
|
|
2312
|
+
if port == "" {
|
|
2313
|
+
port = "3001"
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
if webhookSecret == "" || privateKeyPEM == "" || partnerID == "" {
|
|
2317
|
+
log.Fatal("[startup] Missing required env vars: WEBHOOK_SECRET, PARTNER_PRIVATE_KEY_PEM, PARTNER_ID")
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
var mu sync.Mutex
|
|
2321
|
+
seen := make(map[string]bool)
|
|
2322
|
+
|
|
2323
|
+
http.HandleFunc("/webhook", func(w http.ResponseWriter, r *http.Request) {
|
|
2324
|
+
if r.Method != http.MethodPost {
|
|
2325
|
+
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
2326
|
+
return
|
|
2327
|
+
}
|
|
2328
|
+
body, err := io.ReadAll(r.Body)
|
|
2329
|
+
if err != nil {
|
|
2330
|
+
jsonResp(w, 400, map[string]any{"error": "failed to read body"})
|
|
2331
|
+
return
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
timestamp := r.Header.Get("X-OneAddress-Timestamp")
|
|
2335
|
+
signature := r.Header.Get("X-OneAddress-Signature")
|
|
2336
|
+
dispatch := r.Header.Get("X-OneAddress-Dispatch")
|
|
2337
|
+
|
|
2338
|
+
if !isFresh(timestamp, 300) {
|
|
2339
|
+
jsonResp(w, 400, map[string]any{"error": "stale timestamp"})
|
|
2340
|
+
return
|
|
2341
|
+
}
|
|
2342
|
+
if !verifyHMAC(string(body), signature, timestamp, webhookSecret) {
|
|
2343
|
+
jsonResp(w, 401, map[string]any{"error": "invalid signature"})
|
|
2344
|
+
return
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
if dispatch != "" {
|
|
2348
|
+
mu.Lock()
|
|
2349
|
+
if seen[dispatch] {
|
|
2350
|
+
mu.Unlock()
|
|
2351
|
+
jsonResp(w, 200, map[string]any{"ok": true, "duplicate": true})
|
|
2352
|
+
return
|
|
2353
|
+
}
|
|
2354
|
+
seen[dispatch] = true
|
|
2355
|
+
mu.Unlock()
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
var parsed map[string]any
|
|
2359
|
+
if err = json.Unmarshal(body, &parsed); err != nil {
|
|
2360
|
+
jsonResp(w, 400, map[string]any{"error": "invalid JSON"})
|
|
2361
|
+
return
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
event, _ := parsed["event"].(string)
|
|
2365
|
+
enc, hasEnc := parsed["address_encrypted"].(map[string]any)
|
|
2366
|
+
if !hasEnc {
|
|
2367
|
+
jsonResp(w, 200, map[string]any{"ok": true, "note": "no encrypted payload"})
|
|
2368
|
+
return
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
address, err := decryptAddress(enc, privateKeyPEM, partnerID)
|
|
2372
|
+
if err != nil {
|
|
2373
|
+
log.Printf("[webhook] Decryption error (check PARTNER_PRIVATE_KEY_PEM): %v", err)
|
|
2374
|
+
jsonResp(w, 200, map[string]any{"ok": true, "note": "received - decryption pending key setup"})
|
|
2375
|
+
return
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
switch event {
|
|
2379
|
+
case "address.updated":
|
|
2380
|
+
customer, _ := parsed["customer"].(map[string]any)
|
|
2381
|
+
email, _ := customer["email"].(string)
|
|
2382
|
+
log.Printf("[webhook] Address for %s: %v %v %v %v",
|
|
2383
|
+
email, address["street"], address["suburb"], address["state"], address["postcode"])
|
|
2384
|
+
// TODO: persist address to your database
|
|
2385
|
+
|
|
2386
|
+
case "address.verify":
|
|
2387
|
+
customer, _ := parsed["customer"].(map[string]any)
|
|
2388
|
+
memberName, _ := customer["name"].(string)
|
|
2389
|
+
callbackURL, _ := parsed["callback_url"].(string)
|
|
2390
|
+
token, _ := parsed["callback_token"].(string)
|
|
2391
|
+
batchID, _ := parsed["batch_id"].(string)
|
|
2392
|
+
|
|
2393
|
+
// TODO: Replace "match" with a real DB lookup.
|
|
2394
|
+
// Valid values: "match" | "mismatch" | "not_found"
|
|
2395
|
+
result := "match"
|
|
2396
|
+
|
|
2397
|
+
callbackBody, _ := json.Marshal(map[string]any{
|
|
2398
|
+
"batch_id": batchID,
|
|
2399
|
+
"partner_id": partnerID,
|
|
2400
|
+
"member_name": memberName,
|
|
2401
|
+
"result": result,
|
|
2402
|
+
"token": token,
|
|
2403
|
+
})
|
|
2404
|
+
resp, err := http.Post(callbackURL, "application/json", bytes.NewReader(callbackBody))
|
|
2405
|
+
if err != nil {
|
|
2406
|
+
log.Printf("[webhook] Callback POST failed: %v", err)
|
|
2407
|
+
} else {
|
|
2408
|
+
resp.Body.Close()
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
default:
|
|
2412
|
+
log.Printf("[webhook] Unknown event %q \u2014 acknowledged", event)
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
jsonResp(w, 200, map[string]any{"ok": true})
|
|
2416
|
+
})
|
|
2417
|
+
|
|
2418
|
+
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
|
2419
|
+
w.Header().Set("Content-Type", "application/json")
|
|
2420
|
+
w.Write([]byte(\`{"status":"ok"}\`))
|
|
2421
|
+
})
|
|
2422
|
+
|
|
2423
|
+
log.Printf("OneAddress webhook server on http://localhost:%s/webhook", port)
|
|
2424
|
+
log.Fatal(http.ListenAndServe(":"+port, nil))
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
// \u2500\u2500 Crypto \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2428
|
+
|
|
2429
|
+
func verifyHMAC(rawBody, signature, timestamp, secret string) bool {
|
|
2430
|
+
payload := timestamp + "." + rawBody
|
|
2431
|
+
mac := hmac.New(sha256.New, []byte(secret))
|
|
2432
|
+
mac.Write([]byte(payload))
|
|
2433
|
+
expected := hex.EncodeToString(mac.Sum(nil))
|
|
2434
|
+
sigBytes, err := hex.DecodeString(signature)
|
|
2435
|
+
if err != nil {
|
|
2436
|
+
return false
|
|
2437
|
+
}
|
|
2438
|
+
expBytes, _ := hex.DecodeString(expected)
|
|
2439
|
+
if len(sigBytes) != len(expBytes) {
|
|
2440
|
+
return false
|
|
2441
|
+
}
|
|
2442
|
+
return subtle.ConstantTimeCompare(sigBytes, expBytes) == 1
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
func isFresh(timestamp string, toleranceSecs int) bool {
|
|
2446
|
+
ts, err := strconv.ParseInt(timestamp, 10, 64)
|
|
2447
|
+
if err != nil {
|
|
2448
|
+
return false
|
|
2449
|
+
}
|
|
2450
|
+
diff := time.Now().Unix() - ts
|
|
2451
|
+
if diff < 0 {
|
|
2452
|
+
diff = -diff
|
|
2453
|
+
}
|
|
2454
|
+
return diff <= int64(toleranceSecs)
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
// hkdfSHA256 is a single-block RFC 5869 HKDF-SHA256 expand (length \u2264 32 bytes).
|
|
2458
|
+
func hkdfSHA256(ikm, salt, info []byte) []byte {
|
|
2459
|
+
if len(salt) == 0 {
|
|
2460
|
+
salt = make([]byte, sha256.Size)
|
|
2461
|
+
}
|
|
2462
|
+
h := hmac.New(sha256.New, salt)
|
|
2463
|
+
h.Write(ikm)
|
|
2464
|
+
prk := h.Sum(nil)
|
|
2465
|
+
h = hmac.New(sha256.New, prk)
|
|
2466
|
+
h.Write(info)
|
|
2467
|
+
h.Write([]byte{0x01})
|
|
2468
|
+
return h.Sum(nil) // 32 bytes
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
func decodeB64(s string) ([]byte, error) {
|
|
2472
|
+
s = strings.NewReplacer("-", "+", "_", "/").Replace(s)
|
|
2473
|
+
switch len(s) % 4 {
|
|
2474
|
+
case 2:
|
|
2475
|
+
s += "=="
|
|
2476
|
+
case 3:
|
|
2477
|
+
s += "="
|
|
2478
|
+
}
|
|
2479
|
+
return base64.StdEncoding.DecodeString(s)
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
func decryptAddress(enc map[string]any, privateKeyPEM, partnerID string) (map[string]any, error) {
|
|
2483
|
+
// Parse PKCS#8 private key from PEM
|
|
2484
|
+
keyStr := strings.NewReplacer(
|
|
2485
|
+
"-----BEGIN PRIVATE KEY-----", "",
|
|
2486
|
+
"-----END PRIVATE KEY-----", "",
|
|
2487
|
+
"\\r", "", "\\n", "",
|
|
2488
|
+
).Replace(privateKeyPEM)
|
|
2489
|
+
keyStr = strings.TrimSpace(keyStr)
|
|
2490
|
+
|
|
2491
|
+
var derBytes []byte
|
|
2492
|
+
// Try PEM block first, fall back to raw base64
|
|
2493
|
+
block, _ := pem.Decode([]byte(privateKeyPEM))
|
|
2494
|
+
if block != nil {
|
|
2495
|
+
derBytes = block.Bytes
|
|
2496
|
+
} else {
|
|
2497
|
+
var err error
|
|
2498
|
+
derBytes, err = base64.StdEncoding.DecodeString(keyStr)
|
|
2499
|
+
if err != nil {
|
|
2500
|
+
return nil, err
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
pkcsKey, err := x509.ParsePKCS8PrivateKey(derBytes)
|
|
2505
|
+
if err != nil {
|
|
2506
|
+
return nil, err
|
|
2507
|
+
}
|
|
2508
|
+
ecdsaKey, ok := pkcsKey.(*ecdsa.PrivateKey)
|
|
2509
|
+
if !ok {
|
|
2510
|
+
return nil, io.ErrUnexpectedEOF
|
|
2511
|
+
}
|
|
2512
|
+
ecdhPriv, err := ecdsaKey.ECDH()
|
|
2513
|
+
if err != nil {
|
|
2514
|
+
return nil, err
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
// Parse ephemeral public key (65-byte uncompressed P-256 point)
|
|
2518
|
+
ephB64, _ := enc["ephemeralPublicKey"].(string)
|
|
2519
|
+
ephRaw, err := decodeB64(ephB64)
|
|
2520
|
+
if err != nil {
|
|
2521
|
+
return nil, err
|
|
2522
|
+
}
|
|
2523
|
+
ephPub, err := ecdh.P256().NewPublicKey(ephRaw)
|
|
2524
|
+
if err != nil {
|
|
2525
|
+
return nil, err
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
sharedSecret, err := ecdhPriv.ECDH(ephPub)
|
|
2529
|
+
if err != nil {
|
|
2530
|
+
return nil, err
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
// HKDF-SHA256
|
|
2534
|
+
var salt []byte
|
|
2535
|
+
if hkdfSaltB64, _ := enc["hkdfSalt"].(string); hkdfSaltB64 != "" {
|
|
2536
|
+
salt, err = decodeB64(hkdfSaltB64)
|
|
2537
|
+
if err != nil {
|
|
2538
|
+
return nil, err
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
info := []byte("oneaddress:" + partnerID)
|
|
2542
|
+
aesKey := hkdfSHA256(sharedSecret, salt, info)
|
|
2543
|
+
|
|
2544
|
+
// AES-256-GCM decrypt (ciphertext field = ciphertext || 16-byte auth tag)
|
|
2545
|
+
ivB64, _ := enc["iv"].(string)
|
|
2546
|
+
ciphertextB64, _ := enc["ciphertext"].(string)
|
|
2547
|
+
iv, err := decodeB64(ivB64)
|
|
2548
|
+
if err != nil {
|
|
2549
|
+
return nil, err
|
|
2550
|
+
}
|
|
2551
|
+
ciphertextWithTag, err := decodeB64(ciphertextB64)
|
|
2552
|
+
if err != nil {
|
|
2553
|
+
return nil, err
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2556
|
+
block, err := aes.NewCipher(aesKey)
|
|
2557
|
+
if err != nil {
|
|
2558
|
+
return nil, err
|
|
2559
|
+
}
|
|
2560
|
+
gcm, err := cipher.NewGCM(block)
|
|
2561
|
+
if err != nil {
|
|
2562
|
+
return nil, err
|
|
2563
|
+
}
|
|
2564
|
+
plaintext, err := gcm.Open(nil, iv, ciphertextWithTag, nil)
|
|
2565
|
+
if err != nil {
|
|
2566
|
+
return nil, err
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2569
|
+
var result map[string]any
|
|
2570
|
+
err = json.Unmarshal(plaintext, &result)
|
|
2571
|
+
return result, err
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
func jsonResp(w http.ResponseWriter, status int, data any) {
|
|
2575
|
+
w.Header().Set("Content-Type", "application/json")
|
|
2576
|
+
w.WriteHeader(status)
|
|
2577
|
+
json.NewEncoder(w).Encode(data)
|
|
2578
|
+
}
|
|
2579
|
+
`
|
|
2580
|
+
},
|
|
2581
|
+
{
|
|
2582
|
+
name: "README.md",
|
|
2583
|
+
content: `# OneAddress Webhook Handler (Go)
|
|
2584
|
+
|
|
2585
|
+
Generated by \`npx @oneaddress/setup\` for **%%PARTNER_ID%%**.
|
|
2586
|
+
All crypto is inline \u2014 no external SDK required. Requires Go 1.21+.
|
|
2587
|
+
|
|
2588
|
+
## Quick start
|
|
2589
|
+
|
|
2590
|
+
\`\`\`bash
|
|
2591
|
+
# Load credentials from .env and run
|
|
2592
|
+
set -a; source .env; set +a
|
|
2593
|
+
go run .
|
|
2594
|
+
\`\`\`
|
|
2595
|
+
|
|
2596
|
+
Your webhook endpoint: \`POST http://localhost:3001/webhook\`
|
|
2597
|
+
|
|
2598
|
+
## Events handled
|
|
2599
|
+
|
|
2600
|
+
### address.updated
|
|
2601
|
+
Receive \u2192 verify HMAC \u2192 decrypt \u2192 persist to your DB.
|
|
2602
|
+
|
|
2603
|
+
### address.verify
|
|
2604
|
+
Receive \u2192 verify HMAC \u2192 decrypt \u2192 compare to records \u2192 POST callback_url.
|
|
2605
|
+
|
|
2606
|
+
The stub always returns \`"match"\`. In \`main.go\`, find \`TODO: Replace "match"\`
|
|
2607
|
+
and add your DB lookup there.
|
|
2608
|
+
|
|
2609
|
+
## Run conformance check
|
|
2610
|
+
|
|
2611
|
+
\`\`\`bash
|
|
2612
|
+
npx @oneaddress/conformance test %%WEBHOOK_URL%%
|
|
2613
|
+
\`\`\`
|
|
2614
|
+
|
|
2615
|
+
## Configuration
|
|
2616
|
+
|
|
2617
|
+
Credentials are in \`.env\` (written by the setup wizard). Never commit \`.env\` to source control.
|
|
2618
|
+
`
|
|
2619
|
+
}
|
|
2620
|
+
],
|
|
2621
|
+
// ── PHP (Laravel) ─────────────────────────────────────────────────────────
|
|
2622
|
+
"php-laravel": [
|
|
2623
|
+
{
|
|
2624
|
+
name: ".env.oneaddress",
|
|
2625
|
+
content: `# OneAddress credentials \u2014 add these values to your Laravel .env file.
|
|
2626
|
+
OA_PARTNER_ID=%%PARTNER_ID%%
|
|
2627
|
+
OA_WEBHOOK_SECRET=%%WEBHOOK_SECRET%%
|
|
2628
|
+
OA_PRIVATE_KEY_PEM=%%PRIVATE_KEY%%
|
|
2629
|
+
`
|
|
2630
|
+
},
|
|
2631
|
+
{
|
|
2632
|
+
name: "routes/webhook.php",
|
|
2633
|
+
content: `<?php
|
|
2634
|
+
/**
|
|
2635
|
+
* OneAddress Webhook Handler \u2014 Laravel
|
|
2636
|
+
* Generated by npx @oneaddress/setup for %%PARTNER_ID%%
|
|
2637
|
+
*
|
|
2638
|
+
* Include this file in routes/api.php:
|
|
2639
|
+
* require __DIR__ . '/webhook.php';
|
|
2640
|
+
*
|
|
2641
|
+
* Requires PHP extensions: openssl
|
|
2642
|
+
*/
|
|
2643
|
+
|
|
2644
|
+
use Illuminate\\Support\\Facades\\Route;
|
|
2645
|
+
use Illuminate\\Http\\Request;
|
|
2646
|
+
use Illuminate\\Http\\Response;
|
|
2647
|
+
|
|
2648
|
+
Route::post('/webhook', function (Request $request): Response {
|
|
2649
|
+
$partnerId = env('OA_PARTNER_ID', '%%PARTNER_ID%%');
|
|
2650
|
+
$webhookSecret = env('OA_WEBHOOK_SECRET', '');
|
|
2651
|
+
$privateKeyB64 = env('OA_PRIVATE_KEY_PEM', '');
|
|
2652
|
+
|
|
2653
|
+
$rawBody = $request->getContent();
|
|
2654
|
+
$timestamp = $request->header('X-OneAddress-Timestamp', '');
|
|
2655
|
+
$signature = $request->header('X-OneAddress-Signature', '');
|
|
2656
|
+
|
|
2657
|
+
// \u2500\u2500 Timestamp freshness (\xB15 minutes) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2658
|
+
if (!$timestamp || abs(time() - (int)$timestamp) > 300) {
|
|
2659
|
+
return response()->json(['error' => 'Stale timestamp'], 400);
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
// \u2500\u2500 HMAC-SHA256 signature verification \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2663
|
+
$payload = $timestamp . '.' . $rawBody;
|
|
2664
|
+
$expected = hash_hmac('sha256', $payload, $webhookSecret);
|
|
2665
|
+
if (!hash_equals($expected, strtolower($signature))) {
|
|
2666
|
+
return response()->json(['error' => 'Invalid signature'], 401);
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2669
|
+
// \u2500\u2500 Parse body \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2670
|
+
$body = json_decode($rawBody, true);
|
|
2671
|
+
if ($body === null) {
|
|
2672
|
+
return response()->json(['error' => 'Invalid JSON'], 400);
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2675
|
+
$eventType = $body['event'] ?? '';
|
|
2676
|
+
|
|
2677
|
+
if ($eventType !== 'address.updated' && $eventType !== 'address.verify') {
|
|
2678
|
+
return response()->json(['ok' => true, 'skipped' => true]);
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2681
|
+
$enc = $body['address_encrypted'] ?? null;
|
|
2682
|
+
if (!$enc) {
|
|
2683
|
+
return response()->json(['ok' => true, 'note' => 'No encrypted payload']);
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2686
|
+
// \u2500\u2500 ECDH address decryption \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2687
|
+
|
|
2688
|
+
// Helper: decode standard or URL-safe base64
|
|
2689
|
+
$b64decode = function (string $s): string {
|
|
2690
|
+
return base64_decode(strtr($s, '-_', '+/'));
|
|
2691
|
+
};
|
|
2692
|
+
|
|
2693
|
+
// Load private key \u2014 wrap bare base64 DER in PEM headers if needed
|
|
2694
|
+
$keyStr = trim($privateKeyB64);
|
|
2695
|
+
if (strpos($keyStr, '-----') === false) {
|
|
2696
|
+
$keyStr = "-----BEGIN PRIVATE KEY-----\\n"
|
|
2697
|
+
. chunk_split($keyStr, 64, "\\n")
|
|
2698
|
+
. "-----END PRIVATE KEY-----";
|
|
2699
|
+
}
|
|
2700
|
+
$privateKey = openssl_pkey_get_private($keyStr);
|
|
2701
|
+
if (!$privateKey) {
|
|
2702
|
+
\\Log::error('[OneAddress] Failed to load private key');
|
|
2703
|
+
return response()->json(['ok' => true, 'note' => 'Received \u2014 decryption pending key setup']);
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2706
|
+
// Build ephemeral public key SubjectPublicKeyInfo DER from raw uncompressed P-256 point
|
|
2707
|
+
// DER prefix for P-256 SPKI (26 bytes, fixed for NIST P-256):
|
|
2708
|
+
$spkiPrefix = "\\x30\\x59\\x30\\x13\\x06\\x07\\x2a\\x86\\x48\\xce\\x3d\\x02\\x01"
|
|
2709
|
+
. "\\x06\\x08\\x2a\\x86\\x48\\xce\\x3d\\x03\\x01\\x07\\x03\\x42\\x00";
|
|
2710
|
+
$ephemeralRaw = $b64decode($enc['ephemeralPublicKey']);
|
|
2711
|
+
if (strlen($ephemeralRaw) !== 65 || ord($ephemeralRaw[0]) !== 0x04) {
|
|
2712
|
+
return response()->json(['error' => 'Invalid ephemeral public key'], 400);
|
|
2713
|
+
}
|
|
2714
|
+
$ephemeralDer = $spkiPrefix . $ephemeralRaw;
|
|
2715
|
+
$ephemeralPem = "-----BEGIN PUBLIC KEY-----\\n"
|
|
2716
|
+
. chunk_split(base64_encode($ephemeralDer), 64, "\\n")
|
|
2717
|
+
. "-----END PUBLIC KEY-----";
|
|
2718
|
+
|
|
2719
|
+
$ephemeralKey = openssl_pkey_get_public($ephemeralPem);
|
|
2720
|
+
if (!$ephemeralKey) {
|
|
2721
|
+
return response()->json(['error' => 'Invalid ephemeral public key'], 400);
|
|
2722
|
+
}
|
|
2723
|
+
|
|
2724
|
+
// ECDH: derive shared secret
|
|
2725
|
+
$sharedSecret = openssl_pkey_derive($ephemeralKey, $privateKey);
|
|
2726
|
+
if ($sharedSecret === false) {
|
|
2727
|
+
\\Log::error('[OneAddress] ECDH derive failed');
|
|
2728
|
+
return response()->json(['ok' => true, 'note' => 'Received \u2014 decryption pending key setup']);
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2731
|
+
// HKDF-SHA256
|
|
2732
|
+
$hkdfSaltB64 = $enc['hkdfSalt'] ?? null;
|
|
2733
|
+
$salt = $hkdfSaltB64 ? $b64decode($hkdfSaltB64) : str_repeat("\\x00", 32);
|
|
2734
|
+
$info = 'oneaddress:' . $partnerId;
|
|
2735
|
+
$aesKey = hash_hkdf('sha256', $sharedSecret, 32, $info, $salt);
|
|
2736
|
+
|
|
2737
|
+
// AES-256-GCM decrypt \u2014 ciphertext field = actual_ciphertext || 16-byte auth tag
|
|
2738
|
+
$iv = $b64decode($enc['iv']);
|
|
2739
|
+
$ciphertextFull = $b64decode($enc['ciphertext']);
|
|
2740
|
+
$tag = substr($ciphertextFull, -16);
|
|
2741
|
+
$ciphertext = substr($ciphertextFull, 0, -16);
|
|
2742
|
+
|
|
2743
|
+
$plaintext = openssl_decrypt(
|
|
2744
|
+
$ciphertext,
|
|
2745
|
+
'aes-256-gcm',
|
|
2746
|
+
$aesKey,
|
|
2747
|
+
OPENSSL_RAW_DATA,
|
|
2748
|
+
$iv,
|
|
2749
|
+
$tag
|
|
2750
|
+
);
|
|
2751
|
+
|
|
2752
|
+
if ($plaintext === false) {
|
|
2753
|
+
\\Log::error('[OneAddress] AES-GCM decrypt failed \u2014 check private key');
|
|
2754
|
+
return response()->json(['ok' => true, 'note' => 'Received \u2014 decryption pending key setup']);
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2757
|
+
$address = json_decode($plaintext, true);
|
|
2758
|
+
|
|
2759
|
+
if ($eventType === 'address.updated') {
|
|
2760
|
+
// TODO: persist $address to your database
|
|
2761
|
+
\\Log::info('[OneAddress] Address update received', [
|
|
2762
|
+
'partner_id' => $partnerId,
|
|
2763
|
+
'customer' => $body['customer'] ?? null,
|
|
2764
|
+
'address' => $address,
|
|
2765
|
+
]);
|
|
2766
|
+
} elseif ($eventType === 'address.verify') {
|
|
2767
|
+
$customer = $body['customer'] ?? [];
|
|
2768
|
+
$callbackUrl = $body['callback_url'] ?? '';
|
|
2769
|
+
$callbackToken= $body['callback_token'] ?? '';
|
|
2770
|
+
$batchId = $body['batch_id'] ?? '';
|
|
2771
|
+
|
|
2772
|
+
// TODO: Replace "match" with a real DB lookup.
|
|
2773
|
+
// Valid results: "match" | "mismatch" | "not_found"
|
|
2774
|
+
$result = 'match';
|
|
2775
|
+
|
|
2776
|
+
$callbackPayload = json_encode([
|
|
2777
|
+
'batch_id' => $batchId,
|
|
2778
|
+
'partner_id' => $partnerId,
|
|
2779
|
+
'member_name' => $customer['name'] ?? '',
|
|
2780
|
+
'result' => $result,
|
|
2781
|
+
'token' => $callbackToken,
|
|
2782
|
+
]);
|
|
2783
|
+
|
|
2784
|
+
$ch = curl_init($callbackUrl);
|
|
2785
|
+
curl_setopt_array($ch, [
|
|
2786
|
+
CURLOPT_RETURNTRANSFER => true,
|
|
2787
|
+
CURLOPT_POST => true,
|
|
2788
|
+
CURLOPT_POSTFIELDS => $callbackPayload,
|
|
2789
|
+
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
|
|
2790
|
+
CURLOPT_TIMEOUT => 10,
|
|
2791
|
+
]);
|
|
2792
|
+
curl_exec($ch);
|
|
2793
|
+
curl_close($ch);
|
|
2794
|
+
}
|
|
2795
|
+
|
|
2796
|
+
return response()->json(['ok' => true]);
|
|
2797
|
+
});
|
|
2798
|
+
|
|
2799
|
+
Route::get('/health', function (): \\Illuminate\\Http\\JsonResponse {
|
|
2800
|
+
return response()->json(['status' => 'ok']);
|
|
2801
|
+
});
|
|
2802
|
+
`
|
|
2803
|
+
},
|
|
2804
|
+
{
|
|
2805
|
+
name: ".env.example",
|
|
2806
|
+
content: `# OneAddress webhook credentials \u2014 copy these into your Laravel .env file.
|
|
2807
|
+
OA_PARTNER_ID=your-partner-uuid-here
|
|
2808
|
+
OA_WEBHOOK_SECRET=your-webhook-secret-here
|
|
2809
|
+
OA_PRIVATE_KEY_PEM=<paste PKCS8 PEM private key here - use \\n between lines>
|
|
2810
|
+
`
|
|
2811
|
+
},
|
|
2812
|
+
{
|
|
2813
|
+
name: "README.md",
|
|
2814
|
+
content: `# OneAddress Webhook Handler (PHP / Laravel)
|
|
2815
|
+
|
|
2816
|
+
Generated by \`npx @oneaddress/setup\` for **%%PARTNER_ID%%**.
|
|
2817
|
+
|
|
2818
|
+
## Setup
|
|
2819
|
+
|
|
2820
|
+
1. Add the route file to your Laravel application:
|
|
2821
|
+
|
|
2822
|
+
\`\`\`php
|
|
2823
|
+
// In routes/api.php:
|
|
2824
|
+
require __DIR__ . '/webhook.php';
|
|
2825
|
+
\`\`\`
|
|
2826
|
+
|
|
2827
|
+
2. Copy credentials from \`.env.oneaddress\` into your Laravel \`.env\` file.
|
|
2828
|
+
|
|
2829
|
+
3. Start your server:
|
|
2830
|
+
|
|
2831
|
+
\`\`\`bash
|
|
2832
|
+
php artisan serve --port=3001
|
|
2833
|
+
\`\`\`
|
|
2834
|
+
|
|
2835
|
+
Your webhook endpoint: \`POST http://localhost:3001/api/webhook\`
|
|
2836
|
+
|
|
2837
|
+
## Events handled
|
|
2838
|
+
|
|
2839
|
+
### address.updated
|
|
2840
|
+
Receive \u2192 verify HMAC \u2192 decrypt \u2192 persist to your DB.
|
|
2841
|
+
|
|
2842
|
+
### address.verify
|
|
2843
|
+
Receive \u2192 verify HMAC \u2192 decrypt \u2192 compare to records \u2192 POST callback_url.
|
|
2844
|
+
|
|
2845
|
+
Find the \`TODO: Replace "match"\` comment in \`routes/webhook.php\` and add your DB lookup.
|
|
2846
|
+
|
|
2847
|
+
## Required PHP extensions
|
|
2848
|
+
|
|
2849
|
+
- \`openssl\` (enabled by default on most hosts)
|
|
2850
|
+
- \`curl\` (used to POST the address.verify callback)
|
|
2851
|
+
|
|
2852
|
+
## Run conformance check
|
|
2853
|
+
|
|
2854
|
+
\`\`\`bash
|
|
2855
|
+
npx @oneaddress/conformance test %%WEBHOOK_URL%%
|
|
2856
|
+
\`\`\`
|
|
2857
|
+
|
|
2858
|
+
## Configuration
|
|
2859
|
+
|
|
2860
|
+
| Variable | Description |
|
|
2861
|
+
|---|---|
|
|
2862
|
+
| \`OA_PARTNER_ID\` | Your partner UUID |
|
|
2863
|
+
| \`OA_WEBHOOK_SECRET\` | HMAC-SHA256 signing secret |
|
|
2864
|
+
| \`OA_PRIVATE_KEY_PEM\` | PKCS#8 PEM private key (use \`\\\\n\` between lines) |
|
|
2865
|
+
`
|
|
2866
|
+
}
|
|
2867
|
+
]
|
|
2868
|
+
};
|
|
2869
|
+
|
|
2870
|
+
// src/scaffold.ts
|
|
2871
|
+
function applyTokens(content, partnerId, webhookSecret, webhookUrl, privateKey) {
|
|
2872
|
+
return content.replaceAll("%%PARTNER_ID%%", partnerId).replaceAll("%%WEBHOOK_SECRET%%", webhookSecret).replaceAll("%%WEBHOOK_URL%%", webhookUrl || "<your-webhook-url>").replaceAll("%%PRIVATE_KEY%%", privateKey || "<paste your PKCS8 PEM private key here>");
|
|
2873
|
+
}
|
|
2874
|
+
var SENSITIVE_FILES = /* @__PURE__ */ new Set([".env"]);
|
|
2875
|
+
async function scaffold(platform, outputDir, partnerId, webhookSecret, webhookUrl, privateKey = "") {
|
|
2876
|
+
const templates = TEMPLATES[platform];
|
|
2877
|
+
if (!templates) throw new Error(`Unknown platform: ${platform}`);
|
|
2878
|
+
const written = [];
|
|
2879
|
+
for (const { name, content } of templates) {
|
|
2880
|
+
const dest = (0, import_node_path.join)(outputDir, name);
|
|
2881
|
+
const dir = (0, import_node_path.dirname)(dest);
|
|
2882
|
+
if (!(0, import_node_fs.existsSync)(dir)) {
|
|
2883
|
+
await (0, import_promises.mkdir)(dir, { recursive: true });
|
|
2884
|
+
}
|
|
2885
|
+
const filled = applyTokens(content, partnerId, webhookSecret, webhookUrl, privateKey);
|
|
2886
|
+
const isSensitive = SENSITIVE_FILES.has((0, import_node_path.basename)(name));
|
|
2887
|
+
await (0, import_promises.writeFile)(dest, filled, { encoding: "utf8", mode: isSensitive ? 384 : 420 });
|
|
2888
|
+
if (isSensitive && process.platform !== "win32") {
|
|
2889
|
+
try {
|
|
2890
|
+
await (0, import_promises.chmod)(dest, 384);
|
|
2891
|
+
} catch {
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
written.push(name);
|
|
2895
|
+
}
|
|
2896
|
+
return written;
|
|
2897
|
+
}
|
|
2898
|
+
|
|
2899
|
+
// src/register.ts
|
|
2900
|
+
var import_node_crypto = require("crypto");
|
|
2901
|
+
var PKG_VERSION = "1.0.0";
|
|
2902
|
+
var REGISTER_URL = "https://partners.oneaddress.io/api/partner/installs";
|
|
2903
|
+
function hmacSha256(secret, message) {
|
|
2904
|
+
return (0, import_node_crypto.createHmac)("sha256", secret).update(message).digest("hex");
|
|
2905
|
+
}
|
|
2906
|
+
async function registerInstall(installId, partnerId, webhookSecret, platform, webhookUrl) {
|
|
2907
|
+
try {
|
|
2908
|
+
const ts = String(Math.floor(Date.now() / 1e3));
|
|
2909
|
+
const body = JSON.stringify({
|
|
2910
|
+
installId,
|
|
2911
|
+
platform,
|
|
2912
|
+
webhookUrl,
|
|
2913
|
+
sdkVersion: PKG_VERSION,
|
|
2914
|
+
nodeVersion: process.version,
|
|
2915
|
+
os: process.platform
|
|
2916
|
+
});
|
|
2917
|
+
const sig = hmacSha256(webhookSecret, `${ts}.${body}`);
|
|
2918
|
+
const res = await fetch(REGISTER_URL, {
|
|
2919
|
+
method: "POST",
|
|
2920
|
+
headers: {
|
|
2921
|
+
"Content-Type": "application/json",
|
|
2922
|
+
"X-OneAddress-Partner": partnerId,
|
|
2923
|
+
"X-OneAddress-Timestamp": ts,
|
|
2924
|
+
"X-OneAddress-Signature": sig
|
|
2925
|
+
},
|
|
2926
|
+
body,
|
|
2927
|
+
// 10-second timeout so the wizard doesn't hang
|
|
2928
|
+
signal: AbortSignal.timeout(1e4)
|
|
2929
|
+
});
|
|
2930
|
+
return res.ok;
|
|
2931
|
+
} catch {
|
|
2932
|
+
return false;
|
|
2933
|
+
}
|
|
2934
|
+
}
|
|
2935
|
+
|
|
2936
|
+
// src/conformance.ts
|
|
2937
|
+
var import_node_child_process = require("child_process");
|
|
2938
|
+
function runConformance(webhookUrl, partnerId, webhookSecret) {
|
|
2939
|
+
(0, import_node_child_process.spawnSync)(
|
|
2940
|
+
"npx",
|
|
2941
|
+
[
|
|
2942
|
+
"@oneaddress/conformance",
|
|
2943
|
+
"test",
|
|
2944
|
+
webhookUrl,
|
|
2945
|
+
"--partner-id",
|
|
2946
|
+
partnerId,
|
|
2947
|
+
"--secret",
|
|
2948
|
+
webhookSecret
|
|
2949
|
+
],
|
|
2950
|
+
{ stdio: "inherit" }
|
|
2951
|
+
);
|
|
2952
|
+
}
|
|
2953
|
+
|
|
2954
|
+
// src/install.ts
|
|
2955
|
+
var import_node_child_process2 = require("child_process");
|
|
2956
|
+
var import_node_fs2 = require("fs");
|
|
2957
|
+
var import_node_path2 = require("path");
|
|
2958
|
+
var COMMANDS = {
|
|
2959
|
+
"ts-node": { cmd: "npm", args: ["install"] },
|
|
2960
|
+
"python": { cmd: "pip", args: ["install", "-r", "requirements.txt"] },
|
|
2961
|
+
"go-http": { cmd: "go", args: ["mod", "tidy"] },
|
|
2962
|
+
"php-laravel": { cmd: "composer", args: ["install"] },
|
|
2963
|
+
"csharp-aspnet": { cmd: "dotnet", args: ["restore"] },
|
|
2964
|
+
"java-spring": { cmd: "./mvnw", args: ["dependency:resolve", "-q"] }
|
|
2965
|
+
};
|
|
2966
|
+
function installDependencies(platform, outputDir) {
|
|
2967
|
+
const spec = COMMANDS[platform];
|
|
2968
|
+
if (!spec) {
|
|
2969
|
+
return { ok: true, output: "", manualCommand: "" };
|
|
2970
|
+
}
|
|
2971
|
+
if (platform === "java-spring") {
|
|
2972
|
+
const mvnw = (0, import_node_path2.join)(outputDir, "mvnw");
|
|
2973
|
+
if ((0, import_node_fs2.existsSync)(mvnw)) {
|
|
2974
|
+
try {
|
|
2975
|
+
(0, import_node_fs2.chmodSync)(mvnw, 493);
|
|
2976
|
+
} catch {
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
const cwd = spec.cwd ? (0, import_node_path2.join)(outputDir, spec.cwd) : outputDir;
|
|
2981
|
+
const manualCommand = `cd ${outputDir} && ${spec.cmd} ${spec.args.join(" ")}`;
|
|
2982
|
+
const result = (0, import_node_child_process2.spawnSync)(spec.cmd, spec.args, {
|
|
2983
|
+
cwd,
|
|
2984
|
+
stdio: "pipe",
|
|
2985
|
+
encoding: "utf8",
|
|
2986
|
+
timeout: 5 * 60 * 1e3,
|
|
2987
|
+
// 5 min max
|
|
2988
|
+
shell: process.platform === "win32"
|
|
2989
|
+
});
|
|
2990
|
+
const output = [result.stdout ?? "", result.stderr ?? ""].join("").trim();
|
|
2991
|
+
return {
|
|
2992
|
+
ok: result.status === 0 && !result.error,
|
|
2993
|
+
output,
|
|
2994
|
+
manualCommand
|
|
2995
|
+
};
|
|
2996
|
+
}
|
|
2997
|
+
|
|
2998
|
+
// src/autostart.ts
|
|
2999
|
+
var import_node_child_process3 = require("child_process");
|
|
3000
|
+
var import_node_path3 = require("path");
|
|
3001
|
+
var import_node_fs3 = require("fs");
|
|
3002
|
+
var COMMANDS2 = {
|
|
3003
|
+
"ts-node": { cmd: "npx", args: ["tsx", "src/server.ts"] },
|
|
3004
|
+
"python": { cmd: "uvicorn", args: ["app:app", "--port", "3001"] },
|
|
3005
|
+
"go-http": { cmd: "go", args: ["run", "."] },
|
|
3006
|
+
"php-laravel": { cmd: "php", args: ["artisan", "serve", "--port=3001"] },
|
|
3007
|
+
"csharp-aspnet": { cmd: "dotnet", args: ["run"] },
|
|
3008
|
+
"java-spring": { cmd: "./mvnw", args: ["spring-boot:run"] }
|
|
3009
|
+
};
|
|
3010
|
+
var serverProcess = null;
|
|
3011
|
+
var serverOutput = "";
|
|
3012
|
+
function stopServer() {
|
|
3013
|
+
if (serverProcess) {
|
|
3014
|
+
try {
|
|
3015
|
+
serverProcess.kill("SIGTERM");
|
|
3016
|
+
} catch {
|
|
3017
|
+
}
|
|
3018
|
+
serverProcess = null;
|
|
3019
|
+
}
|
|
3020
|
+
}
|
|
3021
|
+
function watchServer(onLine, onExit) {
|
|
3022
|
+
if (!serverProcess) {
|
|
3023
|
+
onExit();
|
|
3024
|
+
return;
|
|
3025
|
+
}
|
|
3026
|
+
const emit = (chunk) => {
|
|
3027
|
+
chunk.toString().split("\n").forEach((l2) => {
|
|
3028
|
+
if (l2.trim()) onLine(l2);
|
|
3029
|
+
});
|
|
3030
|
+
};
|
|
3031
|
+
serverProcess.stdout?.on("data", emit);
|
|
3032
|
+
serverProcess.stderr?.on("data", emit);
|
|
3033
|
+
serverProcess.once("exit", () => onExit());
|
|
3034
|
+
}
|
|
3035
|
+
async function pollHealth(port, timeoutMs) {
|
|
3036
|
+
const deadline = Date.now() + timeoutMs;
|
|
3037
|
+
while (Date.now() < deadline) {
|
|
3038
|
+
try {
|
|
3039
|
+
const res = await fetch(`http://localhost:${port}/health`, {
|
|
3040
|
+
signal: AbortSignal.timeout(1e3)
|
|
3041
|
+
});
|
|
3042
|
+
if (res.ok) return true;
|
|
3043
|
+
} catch {
|
|
3044
|
+
}
|
|
3045
|
+
await new Promise((r2) => setTimeout(r2, 600));
|
|
3046
|
+
}
|
|
3047
|
+
return false;
|
|
3048
|
+
}
|
|
3049
|
+
async function startServer(platform, outputDir, port = 3001) {
|
|
3050
|
+
const spec = COMMANDS2[platform];
|
|
3051
|
+
if (!spec) {
|
|
3052
|
+
return { ok: false, output: `No start command defined for platform: ${platform}`, manualCommand: "" };
|
|
3053
|
+
}
|
|
3054
|
+
if (platform === "java-spring") {
|
|
3055
|
+
const mvnw = (0, import_node_path3.join)(outputDir, "mvnw");
|
|
3056
|
+
if ((0, import_node_fs3.existsSync)(mvnw)) {
|
|
3057
|
+
try {
|
|
3058
|
+
(0, import_node_fs3.chmodSync)(mvnw, 493);
|
|
3059
|
+
} catch {
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
3062
|
+
}
|
|
3063
|
+
serverOutput = "";
|
|
3064
|
+
const manualCommand = `cd ${outputDir} && ${spec.cmd} ${spec.args.join(" ")}`;
|
|
3065
|
+
serverProcess = (0, import_node_child_process3.spawn)(spec.cmd, spec.args, {
|
|
3066
|
+
cwd: outputDir,
|
|
3067
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3068
|
+
detached: false,
|
|
3069
|
+
shell: process.platform === "win32",
|
|
3070
|
+
env: { ...process.env }
|
|
3071
|
+
});
|
|
3072
|
+
serverProcess.stdout?.on("data", (d2) => {
|
|
3073
|
+
serverOutput += d2.toString();
|
|
3074
|
+
});
|
|
3075
|
+
serverProcess.stderr?.on("data", (d2) => {
|
|
3076
|
+
serverOutput += d2.toString();
|
|
3077
|
+
});
|
|
3078
|
+
serverProcess.on("error", (err) => {
|
|
3079
|
+
serverOutput += `
|
|
3080
|
+
[startup error] ${err.message}`;
|
|
3081
|
+
});
|
|
3082
|
+
const ok = await pollHealth(port, 15e3);
|
|
3083
|
+
return { ok, output: serverOutput, manualCommand };
|
|
3084
|
+
}
|
|
3085
|
+
|
|
3086
|
+
// src/tunnel.ts
|
|
3087
|
+
var import_node_child_process4 = require("child_process");
|
|
3088
|
+
var import_promises2 = require("fs/promises");
|
|
3089
|
+
var import_node_path4 = require("path");
|
|
3090
|
+
var import_node_os = __toESM(require("os"));
|
|
3091
|
+
var tunnelProcess = null;
|
|
3092
|
+
function stopTunnel() {
|
|
3093
|
+
if (tunnelProcess) {
|
|
3094
|
+
try {
|
|
3095
|
+
tunnelProcess.kill("SIGTERM");
|
|
3096
|
+
} catch {
|
|
3097
|
+
}
|
|
3098
|
+
tunnelProcess = null;
|
|
3099
|
+
}
|
|
3100
|
+
}
|
|
3101
|
+
function cloudflaredFilename() {
|
|
3102
|
+
const p2 = process.platform;
|
|
3103
|
+
const a2 = import_node_os.default.arch();
|
|
3104
|
+
if (p2 === "darwin") return a2 === "arm64" ? "cloudflared-darwin-arm64" : "cloudflared-darwin-amd64";
|
|
3105
|
+
if (p2 === "win32") return a2 === "x64" ? "cloudflared-windows-amd64.exe" : "cloudflared-windows-386.exe";
|
|
3106
|
+
if (a2 === "arm64") return "cloudflared-linux-arm64";
|
|
3107
|
+
if (a2 === "arm") return "cloudflared-linux-armhf";
|
|
3108
|
+
return "cloudflared-linux-amd64";
|
|
3109
|
+
}
|
|
3110
|
+
async function downloadCloudflared() {
|
|
3111
|
+
const filename = cloudflaredFilename();
|
|
3112
|
+
const dest = (0, import_node_path4.join)(import_node_os.default.tmpdir(), process.platform === "win32" ? "cloudflared.exe" : "cloudflared");
|
|
3113
|
+
const url = `https://github.com/cloudflare/cloudflared/releases/latest/download/${filename}`;
|
|
3114
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(6e4) });
|
|
3115
|
+
if (!res.ok) throw new Error(`Failed to download cloudflared: HTTP ${res.status}`);
|
|
3116
|
+
const buf = await res.arrayBuffer();
|
|
3117
|
+
await (0, import_promises2.writeFile)(dest, Buffer.from(buf));
|
|
3118
|
+
if (process.platform !== "win32") await (0, import_promises2.chmod)(dest, 493);
|
|
3119
|
+
return dest;
|
|
3120
|
+
}
|
|
3121
|
+
async function resolveBinary() {
|
|
3122
|
+
try {
|
|
3123
|
+
(0, import_node_child_process4.execSync)("cloudflared --version", { stdio: "pipe" });
|
|
3124
|
+
return "cloudflared";
|
|
3125
|
+
} catch {
|
|
3126
|
+
}
|
|
3127
|
+
return downloadCloudflared();
|
|
3128
|
+
}
|
|
3129
|
+
async function startTunnel(port) {
|
|
3130
|
+
const binary = await resolveBinary();
|
|
3131
|
+
return new Promise((resolve2, reject) => {
|
|
3132
|
+
tunnelProcess = (0, import_node_child_process4.spawn)(binary, ["tunnel", "--url", `http://localhost:${port}`], {
|
|
3133
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
3134
|
+
});
|
|
3135
|
+
let resolved = false;
|
|
3136
|
+
const timer = setTimeout(() => {
|
|
3137
|
+
if (!resolved) {
|
|
3138
|
+
resolved = true;
|
|
3139
|
+
reject(new Error("Tunnel URL not received within 45 seconds"));
|
|
3140
|
+
}
|
|
3141
|
+
}, 45e3);
|
|
3142
|
+
const tryMatch = (data) => {
|
|
3143
|
+
const text = data.toString();
|
|
3144
|
+
const match = text.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/);
|
|
3145
|
+
if (match && !resolved) {
|
|
3146
|
+
resolved = true;
|
|
3147
|
+
clearTimeout(timer);
|
|
3148
|
+
resolve2({ url: match[0], temporary: true });
|
|
3149
|
+
}
|
|
3150
|
+
};
|
|
3151
|
+
tunnelProcess.stdout?.on("data", tryMatch);
|
|
3152
|
+
tunnelProcess.stderr?.on("data", tryMatch);
|
|
3153
|
+
tunnelProcess.on("error", (err) => {
|
|
3154
|
+
if (!resolved) {
|
|
3155
|
+
resolved = true;
|
|
3156
|
+
clearTimeout(timer);
|
|
3157
|
+
reject(new Error(`cloudflared failed to start: ${err.message}`));
|
|
3158
|
+
}
|
|
3159
|
+
});
|
|
3160
|
+
tunnelProcess.on("close", (code) => {
|
|
3161
|
+
if (!resolved) {
|
|
3162
|
+
resolved = true;
|
|
3163
|
+
clearTimeout(timer);
|
|
3164
|
+
reject(new Error(`cloudflared exited early with code ${code}`));
|
|
3165
|
+
}
|
|
3166
|
+
});
|
|
3167
|
+
});
|
|
3168
|
+
}
|
|
3169
|
+
|
|
3170
|
+
// src/register-url.ts
|
|
3171
|
+
var import_node_crypto2 = require("crypto");
|
|
3172
|
+
var PORTAL_API = "https://partners.oneaddress.io/api/partner/webhook-url";
|
|
3173
|
+
async function registerWebhookUrl(partnerId, webhookSecret, webhookUrl) {
|
|
3174
|
+
try {
|
|
3175
|
+
const body = JSON.stringify({ webhook_url: webhookUrl });
|
|
3176
|
+
const ts = String(Math.floor(Date.now() / 1e3));
|
|
3177
|
+
const sig = (0, import_node_crypto2.createHmac)("sha256", webhookSecret).update(`${ts}.${body}`).digest("hex");
|
|
3178
|
+
const res = await fetch(PORTAL_API, {
|
|
3179
|
+
method: "PATCH",
|
|
3180
|
+
headers: {
|
|
3181
|
+
"Content-Type": "application/json",
|
|
3182
|
+
"X-OneAddress-Timestamp": ts,
|
|
3183
|
+
"X-OneAddress-Signature": sig,
|
|
3184
|
+
"X-OneAddress-Partner": partnerId
|
|
3185
|
+
},
|
|
3186
|
+
body,
|
|
3187
|
+
signal: AbortSignal.timeout(1e4)
|
|
3188
|
+
});
|
|
3189
|
+
return res.ok;
|
|
3190
|
+
} catch {
|
|
3191
|
+
return false;
|
|
3192
|
+
}
|
|
3193
|
+
}
|
|
3194
|
+
|
|
3195
|
+
// src/prompts.ts
|
|
3196
|
+
var R4 = "\x1B[0m";
|
|
3197
|
+
var AMB2 = "\x1B[38;2;224;162;72m";
|
|
3198
|
+
var MID2 = "\x1B[38;2;155;135;100m";
|
|
3199
|
+
var DIM2 = "\x1B[38;2;70;58;35m";
|
|
3200
|
+
var GRN2 = "\x1B[38;2;80;200;120m";
|
|
3201
|
+
var CRM2 = "\x1B[1m\x1B[38;2;245;238;216m";
|
|
3202
|
+
var cleanupFns = [];
|
|
3203
|
+
function onCleanup(fn2) {
|
|
3204
|
+
cleanupFns.push(fn2);
|
|
3205
|
+
}
|
|
3206
|
+
process.on("SIGINT", () => {
|
|
3207
|
+
for (const fn2 of cleanupFns) {
|
|
3208
|
+
try {
|
|
3209
|
+
fn2();
|
|
3210
|
+
} catch {
|
|
3211
|
+
}
|
|
3212
|
+
}
|
|
3213
|
+
console.log(`
|
|
3214
|
+
${DIM2} Stopped.${R4}
|
|
3215
|
+
`);
|
|
3216
|
+
process.exit(0);
|
|
3217
|
+
});
|
|
3218
|
+
function assertNotCancelled(value) {
|
|
3219
|
+
if (BD(value)) {
|
|
3220
|
+
for (const fn2 of cleanupFns) {
|
|
3221
|
+
try {
|
|
3222
|
+
fn2();
|
|
3223
|
+
} catch {
|
|
3224
|
+
}
|
|
3225
|
+
}
|
|
3226
|
+
ve("Setup cancelled.");
|
|
3227
|
+
process.exit(0);
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
3231
|
+
function wrapAsPkcs8(b64) {
|
|
3232
|
+
const body = b64.match(/.{1,64}/g)?.join("\n") ?? b64;
|
|
3233
|
+
return { pem: `-----BEGIN PRIVATE KEY-----
|
|
3234
|
+
${body}
|
|
3235
|
+
-----END PRIVATE KEY-----
|
|
3236
|
+
` };
|
|
3237
|
+
}
|
|
3238
|
+
function normalisePrivateKey(raw) {
|
|
3239
|
+
const trimmed = raw.trim();
|
|
3240
|
+
const isPath = /^(\/|\.\/|\.\.\/|~\/|[A-Za-z]:[/\\])/.test(trimmed) || trimmed.endsWith(".pem");
|
|
3241
|
+
if (isPath) {
|
|
3242
|
+
const abs = trimmed.startsWith("~/") ? (0, import_node_path5.resolve)((0, import_node_os2.homedir)(), trimmed.slice(2)) : (0, import_node_path5.resolve)(trimmed);
|
|
3243
|
+
if (!(0, import_node_fs4.existsSync)(abs)) return { pem: "", error: `File not found: ${abs}` };
|
|
3244
|
+
return normalisePrivateKey((0, import_node_fs4.readFileSync)(abs, "utf8"));
|
|
3245
|
+
}
|
|
3246
|
+
const unescaped = trimmed.replace(/\\n/g, "\n");
|
|
3247
|
+
if (unescaped.includes("-----BEGIN PRIVATE KEY-----")) {
|
|
3248
|
+
return { pem: unescaped };
|
|
3249
|
+
}
|
|
3250
|
+
if (trimmed.includes("-----BEGIN ONEADDRESS ECDH PRIVATE KEY-----") || trimmed.includes("BEGIN ONEADDRESS")) {
|
|
3251
|
+
const lines = trimmed.split("\n");
|
|
3252
|
+
const b64Lines = [];
|
|
3253
|
+
let inBody = false;
|
|
3254
|
+
for (const line of lines) {
|
|
3255
|
+
if (line.includes("BEGIN ONEADDRESS") || line.includes("END ONEADDRESS")) continue;
|
|
3256
|
+
if (line.trim() === "-----") {
|
|
3257
|
+
inBody = true;
|
|
3258
|
+
continue;
|
|
3259
|
+
}
|
|
3260
|
+
if (!inBody) continue;
|
|
3261
|
+
if (line.trim()) b64Lines.push(line.trim());
|
|
3262
|
+
}
|
|
3263
|
+
if (b64Lines.length === 0) return { pem: "", error: "Could not extract key body from the OneAddress PEM file" };
|
|
3264
|
+
return wrapAsPkcs8(b64Lines.join(""));
|
|
3265
|
+
}
|
|
3266
|
+
const b64clean = trimmed.replace(/\s+/g, "");
|
|
3267
|
+
if (/^[A-Za-z0-9+/]+=*$/.test(b64clean) && b64clean.length >= 50) {
|
|
3268
|
+
return wrapAsPkcs8(b64clean);
|
|
3269
|
+
}
|
|
3270
|
+
return {
|
|
3271
|
+
pem: "",
|
|
3272
|
+
error: 'Could not recognise the key format.\n \u2022 Paste the base64 string from the portal "Copy" button, OR\n \u2022 Enter the path to the .pem file you downloaded, e.g. ~/Downloads/oneaddress-private-key.pem'
|
|
3273
|
+
};
|
|
3274
|
+
}
|
|
3275
|
+
function validatePkcs8Pem(pem) {
|
|
3276
|
+
try {
|
|
3277
|
+
(0, import_node_crypto3.createPrivateKey)({ key: pem, format: "pem", type: "pkcs8" });
|
|
3278
|
+
return void 0;
|
|
3279
|
+
} catch (err) {
|
|
3280
|
+
return `Key validation failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
async function main() {
|
|
3284
|
+
printHeader();
|
|
3285
|
+
we("OneAddress Partner Setup \u2014 v2");
|
|
3286
|
+
v2.info("We will:");
|
|
3287
|
+
v2.info(" 1. Collect your credentials from the Partner Portal");
|
|
3288
|
+
v2.info(" 2. Scaffold a working webhook server for your platform");
|
|
3289
|
+
v2.info(" 3. Get it live with a passing conformance check \u2014 in under 5 minutes");
|
|
3290
|
+
v2.info("\nOpen partners.oneaddress.io \u2192 My Profile to find all three values below.\n");
|
|
3291
|
+
const partnerId = await ue({
|
|
3292
|
+
message: "Your Partner ID (UUID format)",
|
|
3293
|
+
placeholder: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
|
3294
|
+
validate: (v3) => {
|
|
3295
|
+
if (!v3.trim()) return "Partner ID is required";
|
|
3296
|
+
if (!UUID_RE.test(v3.trim()))
|
|
3297
|
+
return "Must be a valid UUID \u2014 find it at partners.oneaddress.io \u2192 My Profile";
|
|
3298
|
+
}
|
|
3299
|
+
});
|
|
3300
|
+
assertNotCancelled(partnerId);
|
|
3301
|
+
const webhookSecret = await $e({
|
|
3302
|
+
message: "Your Webhook Secret (min 20 chars)",
|
|
3303
|
+
validate: (v3) => {
|
|
3304
|
+
if (!v3.trim()) return "Webhook Secret is required";
|
|
3305
|
+
if (v3.length < 20) return "That looks too short \u2014 paste the full secret from My Profile";
|
|
3306
|
+
}
|
|
3307
|
+
});
|
|
3308
|
+
assertNotCancelled(webhookSecret);
|
|
3309
|
+
const privateKeyRaw = await ue({
|
|
3310
|
+
message: "Your ECDH Private Key",
|
|
3311
|
+
placeholder: "Paste the base64 key body from My Profile, or enter a path to the .pem file",
|
|
3312
|
+
validate: (v3) => {
|
|
3313
|
+
if (!v3.trim()) return "Private key is required";
|
|
3314
|
+
const { pem, error } = normalisePrivateKey(v3);
|
|
3315
|
+
if (error) return error;
|
|
3316
|
+
return validatePkcs8Pem(pem);
|
|
3317
|
+
}
|
|
3318
|
+
});
|
|
3319
|
+
assertNotCancelled(privateKeyRaw);
|
|
3320
|
+
const pid = partnerId.trim();
|
|
3321
|
+
const secret = webhookSecret.trim();
|
|
3322
|
+
const { pem: privateKey } = normalisePrivateKey(privateKeyRaw.trim());
|
|
3323
|
+
const platform = await de({
|
|
3324
|
+
message: "Which platform?",
|
|
3325
|
+
options: [
|
|
3326
|
+
{
|
|
3327
|
+
value: "ts-node",
|
|
3328
|
+
label: "TypeScript / Node.js (Express) \u2014 RECOMMENDED",
|
|
3329
|
+
hint: "Best supported, auto-installs, clean store interface"
|
|
3330
|
+
},
|
|
3331
|
+
{ value: "python", label: "Python (FastAPI)" },
|
|
3332
|
+
{ value: "go-http", label: "Go (net/http)" },
|
|
3333
|
+
{ value: "php-laravel", label: "PHP (Laravel)" },
|
|
3334
|
+
{ value: "java-spring", label: "Java (Spring Boot)" },
|
|
3335
|
+
{ value: "csharp-aspnet", label: "C# (ASP.NET Core)" }
|
|
3336
|
+
]
|
|
3337
|
+
});
|
|
3338
|
+
assertNotCancelled(platform);
|
|
3339
|
+
const outputDir = await ue({
|
|
3340
|
+
message: "Where to write the files?",
|
|
3341
|
+
placeholder: "./oneaddress-webhook",
|
|
3342
|
+
defaultValue: "./oneaddress-webhook"
|
|
3343
|
+
});
|
|
3344
|
+
assertNotCancelled(outputDir);
|
|
3345
|
+
const outDir = outputDir.trim() || "./oneaddress-webhook";
|
|
3346
|
+
if ((0, import_node_fs4.existsSync)(outDir)) {
|
|
3347
|
+
const entries = (0, import_node_fs4.readdirSync)(outDir);
|
|
3348
|
+
if (entries.length > 0) {
|
|
3349
|
+
const overwrite = await me({
|
|
3350
|
+
message: `${outDir} already has files \u2014 overwrite?`,
|
|
3351
|
+
initialValue: false
|
|
3352
|
+
});
|
|
3353
|
+
assertNotCancelled(overwrite);
|
|
3354
|
+
if (!overwrite) {
|
|
3355
|
+
ve("Choose a different output directory and run again.");
|
|
3356
|
+
process.exit(0);
|
|
3357
|
+
}
|
|
3358
|
+
}
|
|
3359
|
+
}
|
|
3360
|
+
const s1 = L2();
|
|
3361
|
+
s1.start("Scaffolding files\u2026");
|
|
3362
|
+
let written;
|
|
3363
|
+
try {
|
|
3364
|
+
written = await scaffold(
|
|
3365
|
+
platform,
|
|
3366
|
+
outDir,
|
|
3367
|
+
pid,
|
|
3368
|
+
secret,
|
|
3369
|
+
"",
|
|
3370
|
+
// webhookUrl — filled in after server starts
|
|
3371
|
+
privateKey
|
|
3372
|
+
);
|
|
3373
|
+
s1.stop(`Scaffolded ${written.length} files in ${outDir}`);
|
|
3374
|
+
for (const f2 of written) v2.success(` ${f2}`);
|
|
3375
|
+
} catch (err) {
|
|
3376
|
+
s1.stop("Scaffold failed");
|
|
3377
|
+
v2.error(`Could not write files: ${err instanceof Error ? err.message : String(err)}`);
|
|
3378
|
+
process.exit(1);
|
|
3379
|
+
}
|
|
3380
|
+
const s2 = L2();
|
|
3381
|
+
s2.start("Installing dependencies\u2026");
|
|
3382
|
+
const install = installDependencies(platform, outDir);
|
|
3383
|
+
if (install.ok) {
|
|
3384
|
+
s2.stop("Dependencies installed");
|
|
3385
|
+
} else {
|
|
3386
|
+
s2.stop("Dependency install failed \u2014 continuing anyway");
|
|
3387
|
+
v2.warn(`Install failed. Run manually:
|
|
3388
|
+
${install.manualCommand}`);
|
|
3389
|
+
if (install.output) v2.warn(install.output.slice(0, 500));
|
|
3390
|
+
}
|
|
3391
|
+
const s3 = L2();
|
|
3392
|
+
s3.start("Starting server (waiting up to 15 s for /health)\u2026");
|
|
3393
|
+
onCleanup(stopServer);
|
|
3394
|
+
const start = await startServer(platform, outDir, 3001);
|
|
3395
|
+
let serverRunning = false;
|
|
3396
|
+
if (start.ok) {
|
|
3397
|
+
s3.stop("Server is healthy on port 3001");
|
|
3398
|
+
serverRunning = true;
|
|
3399
|
+
} else {
|
|
3400
|
+
s3.stop("Server did not respond \u2014 continuing anyway");
|
|
3401
|
+
v2.warn(`Could not auto-start the server. Start it manually:
|
|
3402
|
+
${start.manualCommand}`);
|
|
3403
|
+
if (start.output) v2.warn(start.output.slice(0, 600));
|
|
3404
|
+
}
|
|
3405
|
+
let webhookUrl = "";
|
|
3406
|
+
const hasPublicUrl = await me({
|
|
3407
|
+
message: "Do you have a public HTTPS URL where this server is reachable?",
|
|
3408
|
+
initialValue: false
|
|
3409
|
+
});
|
|
3410
|
+
assertNotCancelled(hasPublicUrl);
|
|
3411
|
+
if (hasPublicUrl) {
|
|
3412
|
+
const providedUrl = await ue({
|
|
3413
|
+
message: "Your public HTTPS webhook URL",
|
|
3414
|
+
placeholder: "https://my-api.example.com/webhook",
|
|
3415
|
+
validate: (v3) => {
|
|
3416
|
+
if (!v3.trim()) return "URL is required";
|
|
3417
|
+
if (!v3.trim().startsWith("https://"))
|
|
3418
|
+
return "Must start with https:// \u2014 OneAddress enforces HTTPS for outbound webhooks";
|
|
3419
|
+
}
|
|
3420
|
+
});
|
|
3421
|
+
assertNotCancelled(providedUrl);
|
|
3422
|
+
webhookUrl = providedUrl.trim();
|
|
3423
|
+
} else {
|
|
3424
|
+
v2.info("\nSetting up a free HTTPS tunnel via Cloudflare Tunnel\u2026");
|
|
3425
|
+
const s4 = L2();
|
|
3426
|
+
s4.start("Starting Cloudflare Tunnel\u2026");
|
|
3427
|
+
onCleanup(stopTunnel);
|
|
3428
|
+
try {
|
|
3429
|
+
const tunnel = await startTunnel(3001);
|
|
3430
|
+
webhookUrl = `${tunnel.url}/webhook`;
|
|
3431
|
+
s4.stop(`Tunnel active: ${tunnel.url}`);
|
|
3432
|
+
v2.warn(
|
|
3433
|
+
"IMPORTANT: This tunnel URL is temporary \u2014 it expires when this process stops.\nBefore going to production, deploy to a permanent server and update the webhook URL in the portal."
|
|
3434
|
+
);
|
|
3435
|
+
} catch (err) {
|
|
3436
|
+
s4.stop("Tunnel setup failed");
|
|
3437
|
+
v2.warn(`Could not start tunnel: ${err instanceof Error ? err.message : String(err)}`);
|
|
3438
|
+
v2.warn(
|
|
3439
|
+
"To get a public URL manually, try: npx cloudflared tunnel --url http://localhost:3001\nOr deploy to any server and set the URL in partners.oneaddress.io \u2192 My Profile."
|
|
3440
|
+
);
|
|
3441
|
+
}
|
|
3442
|
+
}
|
|
3443
|
+
if (webhookUrl) {
|
|
3444
|
+
const s5 = L2();
|
|
3445
|
+
s5.start("Registering webhook URL in the Partner Portal\u2026");
|
|
3446
|
+
const registered = await registerWebhookUrl(pid, secret, webhookUrl);
|
|
3447
|
+
if (registered) {
|
|
3448
|
+
s5.stop(`Webhook URL registered: ${webhookUrl}`);
|
|
3449
|
+
} else {
|
|
3450
|
+
s5.stop("Could not auto-register URL \u2014 update it manually");
|
|
3451
|
+
v2.warn(
|
|
3452
|
+
`Open partners.oneaddress.io \u2192 My Profile \u2192 Webhook URL and paste:
|
|
3453
|
+
${webhookUrl}`
|
|
3454
|
+
);
|
|
3455
|
+
}
|
|
3456
|
+
registerInstall(
|
|
3457
|
+
`OA-${(0, import_node_crypto3.randomUUID)()}`,
|
|
3458
|
+
pid,
|
|
3459
|
+
secret,
|
|
3460
|
+
platform,
|
|
3461
|
+
webhookUrl
|
|
3462
|
+
).catch(() => {
|
|
3463
|
+
});
|
|
3464
|
+
}
|
|
3465
|
+
if (webhookUrl && serverRunning) {
|
|
3466
|
+
v2.info("\nRunning conformance checks \u2014 this may take up to 60 seconds\u2026\n");
|
|
3467
|
+
runConformance(webhookUrl, pid, secret);
|
|
3468
|
+
} else if (webhookUrl) {
|
|
3469
|
+
v2.info("\nStart your server, then run the conformance check:");
|
|
3470
|
+
v2.info(` npx @oneaddress/conformance test ${webhookUrl}`);
|
|
3471
|
+
} else {
|
|
3472
|
+
v2.info("\nOnce your endpoint is live, run the conformance check:");
|
|
3473
|
+
v2.info(" npx @oneaddress/conformance test <your-https-url>/webhook");
|
|
3474
|
+
}
|
|
3475
|
+
const summary = [
|
|
3476
|
+
`Partner ID: ${pid}`,
|
|
3477
|
+
`Webhook URL: ${webhookUrl || "(not set \u2014 update in portal)"}`,
|
|
3478
|
+
`Server port: 3001`,
|
|
3479
|
+
`Files in: ${outDir}`
|
|
3480
|
+
].join("\n");
|
|
3481
|
+
const nextSteps = [
|
|
3482
|
+
"1. Open src/store.ts and wire up saveAddress() + verifyAddress() to your database",
|
|
3483
|
+
"2. Update your address at app.oneaddress.io to trigger a live address.updated event",
|
|
3484
|
+
"3. For production: deploy to a permanent server and update the webhook URL in the portal",
|
|
3485
|
+
"4. Questions? partners.oneaddress.io or partners@oneaddress.io"
|
|
3486
|
+
].join("\n\n");
|
|
3487
|
+
ye(summary, "Setup summary");
|
|
3488
|
+
ye(nextSteps, "Next steps");
|
|
3489
|
+
fe("Setup complete! Welcome to the OneAddress Partner Network.");
|
|
3490
|
+
if (serverRunning) {
|
|
3491
|
+
console.log("");
|
|
3492
|
+
console.log(` ${DIM2}\u250C${"\u2500".repeat(56)}\u2510${R4}`);
|
|
3493
|
+
console.log(` ${DIM2}\u2502${R4} ${GRN2}\u25C8${R4} ${CRM2}Server live${R4} ${MID2}on http://localhost:3001/webhook${R4} ${DIM2}\u2502${R4}`);
|
|
3494
|
+
console.log(` ${DIM2}\u2502${R4} ${AMB2}\u25C8${R4} ${MID2}Edit ${CRM2}src/store.ts${R4} ${MID2}to wire your database${R4} ${DIM2}\u2502${R4}`);
|
|
3495
|
+
console.log(` ${DIM2}\u2502${R4} ${MID2}Press Ctrl+C to stop the server and exit.${R4} ${DIM2} \u2502${R4}`);
|
|
3496
|
+
console.log(` ${DIM2}\u2514${"\u2500".repeat(56)}\u2518${R4}`);
|
|
3497
|
+
console.log("");
|
|
3498
|
+
watchServer(
|
|
3499
|
+
(line) => console.log(` ${DIM2}\u2502${R4} ${MID2}${line}${R4}`),
|
|
3500
|
+
() => {
|
|
3501
|
+
console.log(`
|
|
3502
|
+
${DIM2}Server process exited.${R4}
|
|
3503
|
+
`);
|
|
3504
|
+
process.exit(0);
|
|
3505
|
+
}
|
|
3506
|
+
);
|
|
3507
|
+
process.stdin.resume();
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3510
|
+
|
|
3511
|
+
// src/index.ts
|
|
3512
|
+
main().catch((err) => {
|
|
3513
|
+
console.error("\n[oneaddress/setup] Unexpected error:", err instanceof Error ? err.message : err);
|
|
3514
|
+
process.exit(1);
|
|
3515
|
+
});
|