oft-agt 0.0.0-stage → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli.mjs +332 -0
- package/compile-plan.mjs +133 -0
- package/compiler.mjs +55 -0
- package/instance.mjs +49 -0
- package/npm-shrinkwrap.json +320 -0
- package/package.json +35 -6
- package/release-key.mjs +1 -0
- package/runtime/agent-auth.mjs +48 -0
- package/runtime/command-options.mjs +20 -0
- package/runtime/conditions.mjs +80 -0
- package/runtime/device-login.mjs +58 -0
- package/runtime/errors.mjs +85 -0
- package/runtime/execute.mjs +75 -0
- package/runtime/functions.mjs +48 -0
- package/runtime/lifecycle.mjs +124 -0
- package/runtime/message-config.mjs +73 -0
- package/runtime/options.mjs +33 -0
- package/secret.mjs +16 -0
- package/supervisor.mjs +34 -0
- package/templates/bot.mjs +46 -0
- package/update.mjs +96 -0
- package/usage.mjs +39 -0
- package/version.mjs +1 -0
- package/README.md +0 -3
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "oft-agt",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"lockfileVersion": 3,
|
|
5
|
+
"requires": true,
|
|
6
|
+
"packages": {
|
|
7
|
+
"": {
|
|
8
|
+
"name": "oft-agt",
|
|
9
|
+
"version": "0.5.0",
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"discord.js": "^14.14.0",
|
|
12
|
+
"ws": "^8.18.0"
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"oft-agt": "cli.mjs",
|
|
16
|
+
"oftone-agent": "cli.mjs"
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=22"
|
|
20
|
+
},
|
|
21
|
+
"license": "UNLICENSED"
|
|
22
|
+
},
|
|
23
|
+
"node_modules/@discordjs/builders": {
|
|
24
|
+
"version": "1.14.1",
|
|
25
|
+
"resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.14.1.tgz",
|
|
26
|
+
"integrity": "sha512-gSKkhXLqs96TCzk66VZuHHl8z2bQMJFGwrXC0f33ngK+FLNau4hU1PYny3DNJfNdSH+gVMzE85/d5FQ2BpcNwQ==",
|
|
27
|
+
"license": "Apache-2.0",
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@discordjs/formatters": "^0.6.2",
|
|
30
|
+
"@discordjs/util": "^1.2.0",
|
|
31
|
+
"@sapphire/shapeshift": "^4.0.0",
|
|
32
|
+
"discord-api-types": "^0.38.40",
|
|
33
|
+
"fast-deep-equal": "^3.1.3",
|
|
34
|
+
"ts-mixer": "^6.0.4",
|
|
35
|
+
"tslib": "^2.6.3"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=16.11.0"
|
|
39
|
+
},
|
|
40
|
+
"funding": {
|
|
41
|
+
"url": "https://github.com/discordjs/discord.js?sponsor"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"node_modules/@discordjs/collection": {
|
|
45
|
+
"version": "1.5.3",
|
|
46
|
+
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz",
|
|
47
|
+
"integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==",
|
|
48
|
+
"license": "Apache-2.0",
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=16.11.0"
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"node_modules/@discordjs/formatters": {
|
|
54
|
+
"version": "0.6.2",
|
|
55
|
+
"resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.2.tgz",
|
|
56
|
+
"integrity": "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ==",
|
|
57
|
+
"license": "Apache-2.0",
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"discord-api-types": "^0.38.33"
|
|
60
|
+
},
|
|
61
|
+
"engines": {
|
|
62
|
+
"node": ">=16.11.0"
|
|
63
|
+
},
|
|
64
|
+
"funding": {
|
|
65
|
+
"url": "https://github.com/discordjs/discord.js?sponsor"
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"node_modules/@discordjs/rest": {
|
|
69
|
+
"version": "2.6.3",
|
|
70
|
+
"resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.3.tgz",
|
|
71
|
+
"integrity": "sha512-wvOylxNYJkwKjctS/Mn5GP1w9r3/rzyH+ThD1JlAca6zEdlHs8QWBBUQJpU5Q+W6DoIj/Ljh1IPlZs7hTU+UAg==",
|
|
72
|
+
"license": "Apache-2.0",
|
|
73
|
+
"dependencies": {
|
|
74
|
+
"@discordjs/collection": "^2.1.1",
|
|
75
|
+
"@discordjs/util": "^1.2.0",
|
|
76
|
+
"@sapphire/async-queue": "^1.5.3",
|
|
77
|
+
"@sapphire/snowflake": "^3.5.5",
|
|
78
|
+
"@vladfrangu/async_event_emitter": "^2.4.6",
|
|
79
|
+
"discord-api-types": "^0.38.50",
|
|
80
|
+
"magic-bytes.js": "^1.13.0",
|
|
81
|
+
"tslib": "^2.6.3",
|
|
82
|
+
"undici": "^6.27.0"
|
|
83
|
+
},
|
|
84
|
+
"engines": {
|
|
85
|
+
"node": ">=18"
|
|
86
|
+
},
|
|
87
|
+
"funding": {
|
|
88
|
+
"url": "https://github.com/discordjs/discord.js?sponsor"
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
"node_modules/@discordjs/rest/node_modules/@discordjs/collection": {
|
|
92
|
+
"version": "2.1.1",
|
|
93
|
+
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz",
|
|
94
|
+
"integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==",
|
|
95
|
+
"license": "Apache-2.0",
|
|
96
|
+
"engines": {
|
|
97
|
+
"node": ">=18"
|
|
98
|
+
},
|
|
99
|
+
"funding": {
|
|
100
|
+
"url": "https://github.com/discordjs/discord.js?sponsor"
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
"node_modules/@discordjs/util": {
|
|
104
|
+
"version": "1.2.0",
|
|
105
|
+
"resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.2.0.tgz",
|
|
106
|
+
"integrity": "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg==",
|
|
107
|
+
"license": "Apache-2.0",
|
|
108
|
+
"dependencies": {
|
|
109
|
+
"discord-api-types": "^0.38.33"
|
|
110
|
+
},
|
|
111
|
+
"engines": {
|
|
112
|
+
"node": ">=18"
|
|
113
|
+
},
|
|
114
|
+
"funding": {
|
|
115
|
+
"url": "https://github.com/discordjs/discord.js?sponsor"
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
"node_modules/@discordjs/ws": {
|
|
119
|
+
"version": "1.2.3",
|
|
120
|
+
"resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.3.tgz",
|
|
121
|
+
"integrity": "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==",
|
|
122
|
+
"license": "Apache-2.0",
|
|
123
|
+
"dependencies": {
|
|
124
|
+
"@discordjs/collection": "^2.1.0",
|
|
125
|
+
"@discordjs/rest": "^2.5.1",
|
|
126
|
+
"@discordjs/util": "^1.1.0",
|
|
127
|
+
"@sapphire/async-queue": "^1.5.2",
|
|
128
|
+
"@types/ws": "^8.5.10",
|
|
129
|
+
"@vladfrangu/async_event_emitter": "^2.2.4",
|
|
130
|
+
"discord-api-types": "^0.38.1",
|
|
131
|
+
"tslib": "^2.6.2",
|
|
132
|
+
"ws": "^8.17.0"
|
|
133
|
+
},
|
|
134
|
+
"engines": {
|
|
135
|
+
"node": ">=16.11.0"
|
|
136
|
+
},
|
|
137
|
+
"funding": {
|
|
138
|
+
"url": "https://github.com/discordjs/discord.js?sponsor"
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
"node_modules/@discordjs/ws/node_modules/@discordjs/collection": {
|
|
142
|
+
"version": "2.1.1",
|
|
143
|
+
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz",
|
|
144
|
+
"integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==",
|
|
145
|
+
"license": "Apache-2.0",
|
|
146
|
+
"engines": {
|
|
147
|
+
"node": ">=18"
|
|
148
|
+
},
|
|
149
|
+
"funding": {
|
|
150
|
+
"url": "https://github.com/discordjs/discord.js?sponsor"
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
"node_modules/@sapphire/async-queue": {
|
|
154
|
+
"version": "1.5.5",
|
|
155
|
+
"resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz",
|
|
156
|
+
"integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==",
|
|
157
|
+
"license": "MIT",
|
|
158
|
+
"engines": {
|
|
159
|
+
"node": ">=v14.0.0",
|
|
160
|
+
"npm": ">=7.0.0"
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
"node_modules/@sapphire/shapeshift": {
|
|
164
|
+
"version": "4.0.0",
|
|
165
|
+
"resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz",
|
|
166
|
+
"integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==",
|
|
167
|
+
"license": "MIT",
|
|
168
|
+
"dependencies": {
|
|
169
|
+
"fast-deep-equal": "^3.1.3",
|
|
170
|
+
"lodash": "^4.17.21"
|
|
171
|
+
},
|
|
172
|
+
"engines": {
|
|
173
|
+
"node": ">=v16"
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
"node_modules/@sapphire/snowflake": {
|
|
177
|
+
"version": "3.5.5",
|
|
178
|
+
"resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.5.tgz",
|
|
179
|
+
"integrity": "sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ==",
|
|
180
|
+
"license": "MIT",
|
|
181
|
+
"engines": {
|
|
182
|
+
"node": ">=v14.0.0",
|
|
183
|
+
"npm": ">=7.0.0"
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
"node_modules/@types/node": {
|
|
187
|
+
"version": "26.6.6",
|
|
188
|
+
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.6.tgz",
|
|
189
|
+
"integrity": "sha512-MzQj+8Kx3XI40OmUAE6Qoh58ZMNstNNMwn2nfd3qWsfe7nDNSqJNzes4njhz6AHwrSHOffSLwXU6yb474Q4aog==",
|
|
190
|
+
"license": "MIT",
|
|
191
|
+
"dependencies": {
|
|
192
|
+
"undici-types": "~8.9.0"
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
"node_modules/@types/ws": {
|
|
196
|
+
"version": "8.18.2",
|
|
197
|
+
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.2.tgz",
|
|
198
|
+
"integrity": "sha512-67MQl+fpWKVTT1NYdnmo3U4sc/xPo/zQBncVnI74qmQa0z/b+1g6iYqNmGCPbxO+zz2aklb08a0oHfegiVd0/w==",
|
|
199
|
+
"license": "MIT",
|
|
200
|
+
"dependencies": {
|
|
201
|
+
"@types/node": "*"
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
"node_modules/@vladfrangu/async_event_emitter": {
|
|
205
|
+
"version": "2.4.7",
|
|
206
|
+
"resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.7.tgz",
|
|
207
|
+
"integrity": "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==",
|
|
208
|
+
"license": "MIT",
|
|
209
|
+
"engines": {
|
|
210
|
+
"node": ">=v14.0.0",
|
|
211
|
+
"npm": ">=7.0.0"
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
"node_modules/discord-api-types": {
|
|
215
|
+
"version": "0.38.57",
|
|
216
|
+
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.57.tgz",
|
|
217
|
+
"integrity": "sha512-R1CTSSt4RshqCq23j2Xq2ZscvzB4LzG9OB1CYQdKUGdw8Ti/oF/72EEtK2mNpagG/Vj9NcBT7dw9lKLf+Dacfg==",
|
|
218
|
+
"license": "MIT"
|
|
219
|
+
},
|
|
220
|
+
"node_modules/discord.js": {
|
|
221
|
+
"version": "14.27.0",
|
|
222
|
+
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.27.0.tgz",
|
|
223
|
+
"integrity": "sha512-qHbFlFG2N7y3LjPySYsL6A1+BnX6bkTVgo842EX0CqVPk/KTMwZkojPHEXKsQUpWZNyz5BISNHK1cPpQw0+m4A==",
|
|
224
|
+
"license": "Apache-2.0",
|
|
225
|
+
"dependencies": {
|
|
226
|
+
"@discordjs/builders": "^1.14.1",
|
|
227
|
+
"@discordjs/collection": "1.5.3",
|
|
228
|
+
"@discordjs/formatters": "^0.6.2",
|
|
229
|
+
"@discordjs/rest": "^2.6.2",
|
|
230
|
+
"@discordjs/util": "^1.2.0",
|
|
231
|
+
"@discordjs/ws": "^1.2.3",
|
|
232
|
+
"@sapphire/snowflake": "3.5.5",
|
|
233
|
+
"discord-api-types": "^0.38.49",
|
|
234
|
+
"fast-deep-equal": "3.1.3",
|
|
235
|
+
"lodash.snakecase": "4.1.1",
|
|
236
|
+
"magic-bytes.js": "^1.13.0",
|
|
237
|
+
"tslib": "^2.6.3",
|
|
238
|
+
"undici": "^6.27.0"
|
|
239
|
+
},
|
|
240
|
+
"engines": {
|
|
241
|
+
"node": ">=18"
|
|
242
|
+
},
|
|
243
|
+
"funding": {
|
|
244
|
+
"url": "https://github.com/discordjs/discord.js?sponsor"
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
"node_modules/fast-deep-equal": {
|
|
248
|
+
"version": "3.1.3",
|
|
249
|
+
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
|
250
|
+
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
|
251
|
+
"license": "MIT"
|
|
252
|
+
},
|
|
253
|
+
"node_modules/lodash": {
|
|
254
|
+
"version": "4.18.1",
|
|
255
|
+
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
|
|
256
|
+
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
|
|
257
|
+
"license": "MIT"
|
|
258
|
+
},
|
|
259
|
+
"node_modules/lodash.snakecase": {
|
|
260
|
+
"version": "4.1.1",
|
|
261
|
+
"resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz",
|
|
262
|
+
"integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==",
|
|
263
|
+
"license": "MIT"
|
|
264
|
+
},
|
|
265
|
+
"node_modules/magic-bytes.js": {
|
|
266
|
+
"version": "1.13.1",
|
|
267
|
+
"resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.13.1.tgz",
|
|
268
|
+
"integrity": "sha512-x5sn4UX2k5gCWlcfmoFwG4TPie8+dctESyqOBdhB5p6MsgWXdBKGmt9nXPObj/JI50TTL928lc5Yt1WntMn1bw==",
|
|
269
|
+
"license": "MIT"
|
|
270
|
+
},
|
|
271
|
+
"node_modules/ts-mixer": {
|
|
272
|
+
"version": "6.0.4",
|
|
273
|
+
"resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz",
|
|
274
|
+
"integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==",
|
|
275
|
+
"license": "MIT"
|
|
276
|
+
},
|
|
277
|
+
"node_modules/tslib": {
|
|
278
|
+
"version": "2.8.1",
|
|
279
|
+
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
|
280
|
+
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
|
281
|
+
"license": "0BSD"
|
|
282
|
+
},
|
|
283
|
+
"node_modules/undici": {
|
|
284
|
+
"version": "6.29.1",
|
|
285
|
+
"resolved": "https://registry.npmjs.org/undici/-/undici-6.29.1.tgz",
|
|
286
|
+
"integrity": "sha512-EvRlUt/QqAEvXdQeRMYUfwOQsfzpDztykrG2owGEE/rBOBoYv9uF39ZzsNDh4lTFHhTr2XOZjKkYgueD12ZmGA==",
|
|
287
|
+
"license": "MIT",
|
|
288
|
+
"engines": {
|
|
289
|
+
"node": ">=18.17"
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
"node_modules/undici-types": {
|
|
293
|
+
"version": "8.9.0",
|
|
294
|
+
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz",
|
|
295
|
+
"integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==",
|
|
296
|
+
"license": "MIT"
|
|
297
|
+
},
|
|
298
|
+
"node_modules/ws": {
|
|
299
|
+
"version": "8.22.0",
|
|
300
|
+
"resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz",
|
|
301
|
+
"integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==",
|
|
302
|
+
"license": "MIT",
|
|
303
|
+
"engines": {
|
|
304
|
+
"node": ">=10.0.0"
|
|
305
|
+
},
|
|
306
|
+
"peerDependencies": {
|
|
307
|
+
"bufferutil": "^4.0.1",
|
|
308
|
+
"utf-8-validate": ">=5.0.2"
|
|
309
|
+
},
|
|
310
|
+
"peerDependenciesMeta": {
|
|
311
|
+
"bufferutil": {
|
|
312
|
+
"optional": true
|
|
313
|
+
},
|
|
314
|
+
"utf-8-validate": {
|
|
315
|
+
"optional": true
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,35 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "oft-agt",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "oft-agt",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=22"
|
|
8
|
+
},
|
|
9
|
+
"bin": {
|
|
10
|
+
"oft-agt": "cli.mjs",
|
|
11
|
+
"oftone-agent": "cli.mjs"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"cli.mjs",
|
|
15
|
+
"instance.mjs",
|
|
16
|
+
"version.mjs",
|
|
17
|
+
"release-key.mjs",
|
|
18
|
+
"update.mjs",
|
|
19
|
+
"supervisor.mjs",
|
|
20
|
+
"secret.mjs",
|
|
21
|
+
"usage.mjs",
|
|
22
|
+
"compiler.mjs",
|
|
23
|
+
"compile-plan.mjs",
|
|
24
|
+
"runtime",
|
|
25
|
+
"templates",
|
|
26
|
+
"npm-shrinkwrap.json"
|
|
27
|
+
],
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"ws": "^8.18.0",
|
|
30
|
+
"discord.js": "^14.14.0"
|
|
31
|
+
},
|
|
32
|
+
"license": "UNLICENSED",
|
|
33
|
+
"description": "Agent de liaison serveur pour Oftone Builder",
|
|
34
|
+
"homepage": "https://doc-builder.oftone.fr/agent"
|
|
35
|
+
}
|
package/release-key.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const RELEASE_PUBLIC_KEY = "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAyAW2g5wSb82+on2XW1ie9amtHxtX/EEsZhGqw9FNFsI=\n-----END PUBLIC KEY-----\n";
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { randomBytes, randomUUID } from 'node:crypto';
|
|
2
|
+
import { readFile, writeFile, rename, unlink, link, stat } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { alive } from './lifecycle.mjs';
|
|
5
|
+
|
|
6
|
+
async function save(file, value) { await writeFile(file + '.tmp', JSON.stringify(value), { mode: 0o600 }); await rename(file + '.tmp', file); }
|
|
7
|
+
async function rotationLock(home, action) {
|
|
8
|
+
const nonce = randomUUID(), file = path.join(home, 'auth-operation.lock'), temporary = path.join(home, `auth-${nonce}.tmp`);
|
|
9
|
+
await writeFile(temporary, JSON.stringify({ pid: process.pid, nonce }), { flag: 'wx', mode: 0o600 });
|
|
10
|
+
try {
|
|
11
|
+
try { await link(temporary, file); }
|
|
12
|
+
catch (error) {
|
|
13
|
+
if (error.code !== 'EEXIST') throw error;
|
|
14
|
+
let previous;
|
|
15
|
+
try { previous = JSON.parse(await readFile(file, 'utf8')); } catch {}
|
|
16
|
+
if (alive(previous?.pid) || (!previous && Date.now() - (await stat(file)).mtimeMs < 30000)) throw new Error('AUTH_ROTATION_BUSY');
|
|
17
|
+
await unlink(file); await link(temporary, file);
|
|
18
|
+
}
|
|
19
|
+
} finally { await unlink(temporary); }
|
|
20
|
+
try { return await action(); }
|
|
21
|
+
finally { try { if (JSON.parse(await readFile(file, 'utf8')).nonce === nonce) await unlink(file); } catch {} }
|
|
22
|
+
}
|
|
23
|
+
async function complete(home, config, fetchImpl) {
|
|
24
|
+
const file = path.join(home, 'auth-rotation.json');
|
|
25
|
+
let pending;
|
|
26
|
+
try { pending = JSON.parse(await readFile(file, 'utf8')); } catch (error) { if (error.code === 'ENOENT') return config; throw error; }
|
|
27
|
+
if (!/^[a-f0-9]{64}$/.test(pending.credential || '') || pending.agentId !== config.agentId) throw new Error('AUTH_ROTATION_INVALID');
|
|
28
|
+
const response = await fetchImpl(`${config.relay}/v1/agents/${config.object}/rotate`, { method: 'POST', headers: { Authorization: `Bearer ${config.credential}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ credential: pending.credential }), signal: AbortSignal.timeout(20000) });
|
|
29
|
+
if (!response.ok) throw new Error('AUTH_ROTATION_FAILED');
|
|
30
|
+
const next = { ...config, credential: pending.credential };
|
|
31
|
+
await save(path.join(home, 'agent.json'), next);
|
|
32
|
+
await unlink(file);
|
|
33
|
+
return next;
|
|
34
|
+
}
|
|
35
|
+
export async function completeCredentialRotation(home, config, fetchImpl = fetch) {
|
|
36
|
+
try { await readFile(path.join(home, 'auth-rotation.json')); } catch (error) { if (error.code === 'ENOENT') return config; throw error; }
|
|
37
|
+
return rotationLock(home, () => complete(home, config, fetchImpl));
|
|
38
|
+
}
|
|
39
|
+
export async function rotateCredential(home, config, credential = randomBytes(32).toString('hex'), fetchImpl = fetch) {
|
|
40
|
+
if (!/^[a-f0-9]{64}$/.test(credential) || credential === config.credential) throw new Error('AUTH_ROTATION_INVALID');
|
|
41
|
+
return rotationLock(home, async () => {
|
|
42
|
+
const file = path.join(home, 'auth-rotation.json');
|
|
43
|
+
// Preserve a pending operation after an interrupted response; start/token can resume it.
|
|
44
|
+
try { await readFile(file); return complete(home, config, fetchImpl); } catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
45
|
+
await save(file, { credential, agentId: config.agentId });
|
|
46
|
+
return complete(home, config, fetchImpl);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const OPTION_TYPES = { textOption: { label: 'Texte', type: 3, help: 'Saisir un texte libre.' }, userOption: { label: 'Utilisateur', type: 6, help: 'Sélectionner un utilisateur Discord.' }, channelOption: { label: 'Salon', type: 7, help: 'Sélectionner un salon Discord.' } };
|
|
2
|
+
export const isOption = kind => Object.hasOwn(OPTION_TYPES, kind || '');
|
|
3
|
+
export function optionVariables(kind, name) {
|
|
4
|
+
const base = `${name || 'nom'}_option`;
|
|
5
|
+
return [base, ...(kind === 'userOption' ? ['id', 'name', 'displayName', 'avatar'] : kind === 'channelOption' ? ['id', 'name'] : []).map(suffix => `${base}_${suffix}`)];
|
|
6
|
+
}
|
|
7
|
+
export function compileOptions(blocks) {
|
|
8
|
+
const options = blocks.filter(block => isOption(block.type));
|
|
9
|
+
if (options.length > 25) throw new Error('Une commande accepte 25 options maximum.');
|
|
10
|
+
const names = new Set();
|
|
11
|
+
return options.map(block => {
|
|
12
|
+
const { name, description, required = true } = block.config || {};
|
|
13
|
+
if (typeof name !== 'string' || !/^[a-z0-9_-]{1,32}$/.test(name)) throw new Error('Nom d’option : 1 à 32 minuscules, chiffres, tirets ou underscores.');
|
|
14
|
+
if (names.has(name)) throw new Error(`Option dupliquée : ${name}.`);
|
|
15
|
+
names.add(name);
|
|
16
|
+
if (typeof description !== 'string' || !description.trim() || description.length > 100) throw new Error(`Option ${name} : une description de 1 à 100 caractères est requise.`);
|
|
17
|
+
if (typeof required !== 'boolean') throw new Error(`Option ${name} : le champ requis doit être un booléen.`);
|
|
18
|
+
return { id: block.id, kind: block.type, type: OPTION_TYPES[block.type].type, name, description, required };
|
|
19
|
+
}).sort((a, b) => Number(b.required) - Number(a.required));
|
|
20
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export const CONDITION_TYPES = {
|
|
2
|
+
comparisonCondition: { label: 'Comparaison', help: 'Comparer des valeurs, du texte ou des variables.' },
|
|
3
|
+
userCondition: { label: 'Utilisateur', help: 'Vérifier qui exécute la commande.' },
|
|
4
|
+
channelCondition: { label: 'Salon', help: 'Vérifier dans quel salon la commande est exécutée.' },
|
|
5
|
+
permissionCondition: { label: 'Permissions', help: 'Vérifier les permissions Discord de l’auteur.' },
|
|
6
|
+
};
|
|
7
|
+
export const isCondition = kind => Object.hasOwn(CONDITION_TYPES, kind || '');
|
|
8
|
+
export const COMPARISONS = { eq: 'Égal à', ne: 'Différent de', lt: 'Inférieur à', lte: 'Inférieur ou égal à', gt: 'Supérieur à', gte: 'Supérieur ou égal à', contains: 'Contient', notContains: 'Ne contient pas', startsWith: 'Commence par', endsWith: 'Se termine par', empty: 'Est vide', notEmpty: 'N’est pas vide' };
|
|
9
|
+
// Discord permission flags: https://docs.discord.com/developers/topics/permissions
|
|
10
|
+
const permissionNames = [
|
|
11
|
+
['CreateInstantInvite','Créer des invitations'],['KickMembers','Expulser des membres'],['BanMembers','Bannir des membres'],['Administrator','Administrateur'],['ManageChannels','Gérer les salons'],['ManageGuild','Gérer le serveur'],['AddReactions','Ajouter des réactions'],['ViewAuditLog','Voir les logs du serveur'],['PrioritySpeaker','Voix prioritaire'],['Stream','Diffuser une vidéo'],['ViewChannel','Voir les salons'],['SendMessages','Envoyer des messages'],['SendTTSMessages','Envoyer des messages vocaux TTS'],['ManageMessages','Gérer les messages'],['EmbedLinks','Intégrer des liens'],['AttachFiles','Joindre des fichiers'],['ReadMessageHistory','Voir les anciens messages'],['MentionEveryone','Mentionner everyone et here'],['UseExternalEmojis','Utiliser des emojis externes'],['ViewGuildInsights','Voir les statistiques du serveur'],['Connect','Se connecter au vocal'],['Speak','Parler'],['MuteMembers','Rendre des membres muets'],['DeafenMembers','Mettre des membres en sourdine'],['MoveMembers','Déplacer des membres'],['UseVAD','Utiliser la détection de voix'],['ChangeNickname','Changer son pseudo'],['ManageNicknames','Gérer les pseudos'],['ManageRoles','Gérer les rôles'],['ManageWebhooks','Gérer les webhooks'],['ManageGuildExpressions','Gérer les expressions du serveur'],['UseApplicationCommands','Utiliser les commandes des applications'],['RequestToSpeak','Demander la parole'],['ManageEvents','Gérer les événements'],['ManageThreads','Gérer les fils'],['CreatePublicThreads','Créer des fils publics'],['CreatePrivateThreads','Créer des fils privés'],['UseExternalStickers','Utiliser des stickers externes'],['SendMessagesInThreads','Envoyer des messages dans les fils'],['UseEmbeddedActivities','Utiliser les activités'],['ModerateMembers','Exclure temporairement des membres'],['ViewCreatorMonetizationAnalytics','Voir les statistiques de monétisation'],['UseSoundboard','Utiliser la soundboard'],['CreateGuildExpressions','Créer des expressions du serveur'],['CreateEvents','Créer des événements'],['UseExternalSounds','Utiliser des sons externes'],['SendVoiceMessages','Envoyer des messages vocaux'],null,['SetVoiceChannelStatus','Définir le statut du salon vocal'],['SendPolls','Envoyer des sondages'],['UseExternalApps','Utiliser des applications externes'],['PinMessages','Épingler des messages'],['BypassSlowmode','Contourner le mode lent'],
|
|
12
|
+
];
|
|
13
|
+
export const DISCORD_PERMISSIONS = permissionNames.flatMap((entry, bit) => entry ? [{ key: entry[0], label: entry[1], value: (1n << BigInt(bit)).toString() }] : []);
|
|
14
|
+
const permissions = new Map(DISCORD_PERMISSIONS.map(permission => [permission.key, permission.value]));
|
|
15
|
+
export function validateCondition(kind, config) {
|
|
16
|
+
if (!isCondition(kind) || !config || !Array.isArray(config.rules) || config.rules.length > 25) throw new Error('Un bloc accepte 25 conditions maximum.');
|
|
17
|
+
const ids = new Set(['else']);
|
|
18
|
+
return { rules: config.rules.map(rule => {
|
|
19
|
+
if (!rule || typeof rule.id !== 'string' || !/^[a-zA-Z0-9_-]{1,100}$/.test(rule.id) || ids.has(rule.id)) throw new Error('Identifiant de condition invalide ou dupliqué.');
|
|
20
|
+
ids.add(rule.id);
|
|
21
|
+
const string = (value, name, required = true) => {
|
|
22
|
+
if (typeof value !== 'string' || value.length > 6000 || (required && !value.trim())) throw new Error(`${name} : renseignez une valeur (6 000 caractères maximum).`);
|
|
23
|
+
return value;
|
|
24
|
+
};
|
|
25
|
+
if (kind === 'comparisonCondition') {
|
|
26
|
+
if (!Object.hasOwn(COMPARISONS, rule.operator)) throw new Error('Comparaison inconnue.');
|
|
27
|
+
return { id: rule.id, value: string(rule.value, 'Valeur', false), operator: rule.operator, compare: string(rule.compare ?? '', 'Valeur de comparaison', false) };
|
|
28
|
+
}
|
|
29
|
+
if (!['is', 'isNot'].includes(rule.mode)) throw new Error('Choisissez si la condition doit être satisfaite ou inversée.');
|
|
30
|
+
if (kind === 'permissionCondition') {
|
|
31
|
+
if (!Array.isArray(rule.permissions) || !rule.permissions.length || rule.permissions.length > DISCORD_PERMISSIONS.length || rule.permissions.some(key => !permissions.has(key)) || new Set(rule.permissions).size !== rule.permissions.length) throw new Error('Choisissez des permissions Discord valides.');
|
|
32
|
+
return { id: rule.id, mode: rule.mode, permissions: [...rule.permissions] };
|
|
33
|
+
}
|
|
34
|
+
const value = string(rule.value, 'Identifiant ou variable');
|
|
35
|
+
if (!/^\d{17,20}$/.test(value) && !/^<[@#]!?\d{17,20}>$/.test(value) && !/\{[^{}]+\}/.test(value)) throw new Error('Renseignez un identifiant Discord, une mention ou une variable.');
|
|
36
|
+
return { id: rule.id, mode: rule.mode, value };
|
|
37
|
+
}) };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function conditionLabel(kind, rule, index) {
|
|
41
|
+
if (kind === 'comparisonCondition') return `${rule.value || 'Valeur vide'} ${COMPARISONS[rule.operator]?.toLowerCase() || ''} ${['empty', 'notEmpty'].includes(rule.operator) ? '' : rule.compare || 'valeur vide'}`.trim();
|
|
42
|
+
if (kind === 'permissionCondition') return `${rule.mode === 'isNot' ? 'Sans' : 'Avec'} ${rule.permissions.length} permission(s)`;
|
|
43
|
+
return `${rule.mode === 'isNot' ? 'Tout sauf' : kind === 'userCondition' ? 'Utilisateur' : 'Salon'} ${rule.value || index + 1}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function matchesCondition(kind, rule, interaction) {
|
|
47
|
+
if (kind === 'comparisonCondition') {
|
|
48
|
+
const a = String(rule.value ?? ''), b = String(rule.compare ?? '');
|
|
49
|
+
const numeric = value => value.trim() !== '' && Number.isFinite(Number(value));
|
|
50
|
+
const integers = /^[+-]?\d+$/.test(a.trim()) && /^[+-]?\d+$/.test(b.trim());
|
|
51
|
+
const left = integers ? BigInt(a.trim()) : Number(a), right = integers ? BigInt(b.trim()) : Number(b);
|
|
52
|
+
switch (rule.operator) {
|
|
53
|
+
case 'eq': return numeric(a) && numeric(b) ? left === right : a === b;
|
|
54
|
+
case 'ne': return numeric(a) && numeric(b) ? left !== right : a !== b;
|
|
55
|
+
case 'lt': return numeric(a) && numeric(b) && left < right;
|
|
56
|
+
case 'lte': return numeric(a) && numeric(b) && left <= right;
|
|
57
|
+
case 'gt': return numeric(a) && numeric(b) && left > right;
|
|
58
|
+
case 'gte': return numeric(a) && numeric(b) && left >= right;
|
|
59
|
+
case 'contains': return a.includes(b);
|
|
60
|
+
case 'notContains': return !a.includes(b);
|
|
61
|
+
case 'startsWith': return a.startsWith(b);
|
|
62
|
+
case 'endsWith': return a.endsWith(b);
|
|
63
|
+
case 'empty': return a.length === 0;
|
|
64
|
+
case 'notEmpty': return a.length !== 0;
|
|
65
|
+
default: throw new Error('CONDITION_OPERATOR_INVALID');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (kind === 'permissionCondition') {
|
|
69
|
+
// Fail closed in DMs or when Discord did not supply member permissions, even for negations.
|
|
70
|
+
if (!interaction.guildId || !interaction.memberPermissions) return false;
|
|
71
|
+
const mask = BigInt(interaction.memberPermissions.bitfield ?? interaction.memberPermissions);
|
|
72
|
+
const has = key => Boolean(mask & 8n) || (mask & BigInt(permissions.get(key))) !== 0n;
|
|
73
|
+
return rule.mode === 'isNot' ? rule.permissions.every(key => !has(key)) : rule.permissions.every(has);
|
|
74
|
+
}
|
|
75
|
+
const id = String(rule.value || '').replace(/^<[@#]!?([0-9]+)>$/, '$1');
|
|
76
|
+
if (!/^\d{17,20}$/.test(id)) return false;
|
|
77
|
+
const actual = kind === 'userCondition' ? interaction.user?.id : interaction.channelId;
|
|
78
|
+
if (!actual) return false;
|
|
79
|
+
return rule.mode === 'isNot' ? actual !== id : actual === id;
|
|
80
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { generateKeyPairSync, randomBytes, createHash } from 'node:crypto';
|
|
2
|
+
import { readFile, writeFile, mkdir, rename, unlink, open, stat } from 'node:fs/promises';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { alive } from './lifecycle.mjs';
|
|
6
|
+
|
|
7
|
+
export const RELAY = 'https://oftone-agent-relay.hugocoutant2008.workers.dev';
|
|
8
|
+
const hash = value => createHash('sha256').update(value).digest('hex');
|
|
9
|
+
async function save(file, value) { await writeFile(file + '.tmp', JSON.stringify(value), { mode: 0o600 }); await rename(file + '.tmp', file); }
|
|
10
|
+
export function openLoginPage(url) {
|
|
11
|
+
const [command, args] = process.platform === 'win32' ? ['rundll32.exe', ['url.dll,FileProtocolHandler', url]] : process.platform === 'darwin' ? ['open', [url]] : ['xdg-open', [url]];
|
|
12
|
+
const child = spawn(command, args, { detached: true, windowsHide: true, shell: false, stdio: 'ignore' }); child.on('error', () => {}); child.unref();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function authorizeDevice(base, { dashboard = 'https://builder.oftone.fr', noOpen = false, fetchImpl = fetch, sleep = ms => new Promise(resolve => setTimeout(resolve, ms)), now = Date.now, instructions = (url, code) => { console.log('Connectez-vous avec Discord depuis cet appareil ou un autre :\n' + url); console.log('Code de confirmation : ' + code); console.log('En attente du choix du bot dans le navigateur…'); }, openPage = openLoginPage } = {}) {
|
|
16
|
+
const website = new URL(dashboard);
|
|
17
|
+
if (website.origin !== 'https://builder.oftone.fr' && !(website.protocol === 'http:' && ['localhost', '127.0.0.1'].includes(website.hostname))) throw new Error('LOGIN_ORIGIN_INVALID');
|
|
18
|
+
await mkdir(base, { recursive: true, mode: 0o700 });
|
|
19
|
+
const lockPath = path.join(base, 'login.lock'); let lock;
|
|
20
|
+
try { lock = await open(lockPath, 'wx', 0o600); }
|
|
21
|
+
catch (error) {
|
|
22
|
+
if (error.code !== 'EEXIST') throw error;
|
|
23
|
+
const pid = Number(await readFile(lockPath, 'utf8'));
|
|
24
|
+
if (alive(pid) || (!pid && now() - (await stat(lockPath)).mtimeMs < 30000)) throw new Error('LOGIN_BUSY');
|
|
25
|
+
await unlink(lockPath); lock = await open(lockPath, 'wx', 0o600);
|
|
26
|
+
}
|
|
27
|
+
await lock.writeFile(String(process.pid));
|
|
28
|
+
const file = path.join(base, 'login-pending.json');
|
|
29
|
+
try {
|
|
30
|
+
let pending;
|
|
31
|
+
try { pending = JSON.parse(await readFile(file, 'utf8')); } catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
32
|
+
if (!pending || pending.expiresAt <= now()) {
|
|
33
|
+
const keys = generateKeyPairSync('rsa', { modulusLength: 4096 });
|
|
34
|
+
const credential = randomBytes(32).toString('hex'), pollSecret = randomBytes(32).toString('hex');
|
|
35
|
+
const response = await fetchImpl(RELAY + '/v1/device/start', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ credentialHash: hash(credential), pollHash: hash(pollSecret), publicKey: keys.publicKey.export({ format: 'jwk' }) }), signal: AbortSignal.timeout(20000) });
|
|
36
|
+
if (!response.ok) throw new Error('LOGIN_START_FAILED');
|
|
37
|
+
const result = await response.json();
|
|
38
|
+
if (!/^[a-f0-9]{64}$/.test(result.deviceId || '') || !/^[0-9A-HJKMNP-TV-Z]{4}-[0-9A-HJKMNP-TV-Z]{4}$/.test(result.userCode || '') || result.expiresIn !== 600) throw new Error('LOGIN_RESPONSE_INVALID');
|
|
39
|
+
pending = { deviceId: result.deviceId, userCode: result.userCode, expiresAt: now() + 600000, credential, pollSecret, privateKey: keys.privateKey.export({ type: 'pkcs8', format: 'pem' }) };
|
|
40
|
+
await save(file, pending);
|
|
41
|
+
}
|
|
42
|
+
const url = new URL('/agent-connect', website); url.searchParams.set('device', pending.deviceId);
|
|
43
|
+
instructions(url.href, pending.userCode); if (!noOpen) openPage(url.href);
|
|
44
|
+
while (now() < pending.expiresAt) {
|
|
45
|
+
await sleep(3000);
|
|
46
|
+
let response;
|
|
47
|
+
try { response = await fetchImpl(`${RELAY}/v1/device/${pending.deviceId}/poll`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ pollSecret: pending.pollSecret }), signal: AbortSignal.timeout(20000) }); }
|
|
48
|
+
catch { continue; }
|
|
49
|
+
if (response.status === 429) { await sleep(3000); continue; }
|
|
50
|
+
if (!response.ok) throw new Error(response.status === 410 ? 'LOGIN_EXPIRED' : 'LOGIN_FAILED');
|
|
51
|
+
const result = await response.json();
|
|
52
|
+
if (result.state !== 'approved') continue;
|
|
53
|
+
if (!/^[a-f0-9]{64}$/.test(result.objectId || '') || !/^[a-f0-9-]{36}$/.test(result.agentId || '')) throw new Error('LOGIN_RESPONSE_INVALID');
|
|
54
|
+
return { config: { relay: RELAY, object: result.objectId, agentId: result.agentId, credential: pending.credential }, privateKey: pending.privateKey, pendingFile: file };
|
|
55
|
+
}
|
|
56
|
+
throw new Error('LOGIN_EXPIRED');
|
|
57
|
+
} finally { await lock.close(); await unlink(lockPath).catch(() => {}); }
|
|
58
|
+
}
|