@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.
Files changed (3) hide show
  1. package/README.md +56 -0
  2. package/dist/index.js +3515 -0
  3. 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
+ });