@executor-js/emulate 0.6.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 (48) hide show
  1. package/README.md +1044 -0
  2. package/dist/api.d.ts +24 -0
  3. package/dist/api.js +2665 -0
  4. package/dist/api.js.map +1 -0
  5. package/dist/chunk-D6EKRYGP.js +1615 -0
  6. package/dist/chunk-D6EKRYGP.js.map +1 -0
  7. package/dist/chunk-WVQMFHQM.js +83 -0
  8. package/dist/chunk-WVQMFHQM.js.map +1 -0
  9. package/dist/dist-7FDUSG5I.js +24368 -0
  10. package/dist/dist-7FDUSG5I.js.map +1 -0
  11. package/dist/dist-7N4COJHK.js +1814 -0
  12. package/dist/dist-7N4COJHK.js.map +1 -0
  13. package/dist/dist-BTEY33DJ.js +2334 -0
  14. package/dist/dist-BTEY33DJ.js.map +1 -0
  15. package/dist/dist-DK26ESP2.js +595 -0
  16. package/dist/dist-DK26ESP2.js.map +1 -0
  17. package/dist/dist-IYZPDKJW.js +1284 -0
  18. package/dist/dist-IYZPDKJW.js.map +1 -0
  19. package/dist/dist-JJ2ZRCAX.js +189 -0
  20. package/dist/dist-JJ2ZRCAX.js.map +1 -0
  21. package/dist/dist-K4CVTD6K.js +1570 -0
  22. package/dist/dist-K4CVTD6K.js.map +1 -0
  23. package/dist/dist-M3GVASMR.js +1254 -0
  24. package/dist/dist-M3GVASMR.js.map +1 -0
  25. package/dist/dist-OYYGWKZQ.js +1533 -0
  26. package/dist/dist-OYYGWKZQ.js.map +1 -0
  27. package/dist/dist-P3SBBRFR.js +3169 -0
  28. package/dist/dist-P3SBBRFR.js.map +1 -0
  29. package/dist/dist-RMPDKZUA.js +1183 -0
  30. package/dist/dist-RMPDKZUA.js.map +1 -0
  31. package/dist/dist-WBKONLOE.js +2154 -0
  32. package/dist/dist-WBKONLOE.js.map +1 -0
  33. package/dist/dist-XM5HSBDC.js +1090 -0
  34. package/dist/dist-XM5HSBDC.js.map +1 -0
  35. package/dist/dist-XVVIYXQG.js +4241 -0
  36. package/dist/dist-XVVIYXQG.js.map +1 -0
  37. package/dist/dist-YPRJYQHW.js +5109 -0
  38. package/dist/dist-YPRJYQHW.js.map +1 -0
  39. package/dist/dist-ZEC77OKZ.js +913 -0
  40. package/dist/dist-ZEC77OKZ.js.map +1 -0
  41. package/dist/fonts/GeistPixel-Square.woff2 +0 -0
  42. package/dist/fonts/favicon.ico +0 -0
  43. package/dist/fonts/geist-sans.woff2 +0 -0
  44. package/dist/helpers-LXLP3DFE-LBOTATT5.js +17 -0
  45. package/dist/helpers-LXLP3DFE-LBOTATT5.js.map +1 -0
  46. package/dist/index.js +3005 -0
  47. package/dist/index.js.map +1 -0
  48. package/package.json +83 -0
@@ -0,0 +1,1533 @@
1
+ // ../@emulators/x/dist/index.js
2
+ import { createHash, randomBytes } from "crypto";
3
+ import { timingSafeEqual } from "crypto";
4
+ function getXStore(store) {
5
+ return {
6
+ users: store.collection("x.users", ["user_id", "username"]),
7
+ tweets: store.collection("x.tweets", ["tweet_id", "author_id"]),
8
+ oauthClients: store.collection("x.oauth_clients", ["client_id"]),
9
+ authCodes: store.collection("x.auth_codes", ["code", "client_id"]),
10
+ accessTokens: store.collection("x.access_tokens", ["token", "client_id"]),
11
+ refreshTokens: store.collection("x.refresh_tokens", ["token", "client_id"])
12
+ };
13
+ }
14
+ function lookupAccessToken(store, token) {
15
+ const xs = getXStore(store);
16
+ const row = xs.accessTokens.findOneBy("token", token);
17
+ if (!row) return void 0;
18
+ if (row.expires > 0 && Date.now() > row.expires) {
19
+ xs.accessTokens.delete(row.id);
20
+ return void 0;
21
+ }
22
+ return row;
23
+ }
24
+ function xNumericId() {
25
+ let s = "";
26
+ for (let i = 0; i < 19; i++) {
27
+ s += Math.floor(Math.random() * 10).toString();
28
+ }
29
+ return s[0] === "0" ? "1" + s.slice(1) : s;
30
+ }
31
+ function createErrorHandler(documentationUrl) {
32
+ return async (c, next) => {
33
+ if (documentationUrl) {
34
+ c.set("docsUrl", documentationUrl);
35
+ }
36
+ await next();
37
+ };
38
+ }
39
+ var errorHandler = createErrorHandler();
40
+ var isDebug = typeof process !== "undefined" && (process.env.DEBUG === "1" || process.env.DEBUG === "true" || process.env.EMULATE_DEBUG === "1");
41
+ function debug(label, ...args) {
42
+ if (isDebug) {
43
+ console.log(`[${label}]`, ...args);
44
+ }
45
+ }
46
+ function escapeHtml(s) {
47
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
48
+ }
49
+ function escapeAttr(s) {
50
+ return escapeHtml(s).replace(/'/g, "&#39;");
51
+ }
52
+ var CSS = `
53
+ @font-face{
54
+ font-family:'Geist';font-style:normal;font-weight:100 900;font-display:swap;
55
+ src:url('/_emulate/fonts/geist-sans.woff2') format('woff2');
56
+ }
57
+ @font-face{
58
+ font-family:'Geist Pixel';font-style:normal;font-weight:400;font-display:swap;
59
+ src:url('/_emulate/fonts/GeistPixel-Square.woff2') format('woff2');
60
+ }
61
+ *{box-sizing:border-box;margin:0;padding:0}
62
+ body{
63
+ font-family:'Geist',-apple-system,BlinkMacSystemFont,sans-serif;
64
+ background:#000;color:#33ff00;min-height:100vh;
65
+ -webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;
66
+ }
67
+ .emu-bar{
68
+ border-bottom:1px solid #0a3300;padding:10px 20px;
69
+ display:flex;align-items:center;gap:10px;font-size:.8125rem;color:#1a8c00;
70
+ }
71
+ .emu-bar-title{font-weight:600;color:#33ff00;font-family:'Geist Pixel',monospace;}
72
+ .emu-bar-links{margin-left:auto;display:flex;gap:16px;}
73
+ .emu-bar-links a{
74
+ color:#1a8c00;font-size:.75rem;text-decoration:none;transition:color .15s;
75
+ }
76
+ .emu-bar-links a:hover{color:#33ff00;}
77
+ .emu-bar-links a .full{display:inline;}
78
+ .emu-bar-links a .short{display:none;}
79
+ @media(max-width:600px){
80
+ .emu-bar-links a .full{display:none;}
81
+ .emu-bar-links a .short{display:inline;}
82
+ }
83
+
84
+ .content{
85
+ display:flex;align-items:center;justify-content:center;
86
+ min-height:calc(100vh - 42px);padding:24px 16px;
87
+ }
88
+ .content-inner{width:100%;max-width:420px;}
89
+ .card-title{
90
+ font-family:'Geist Pixel',monospace;
91
+ font-size:1.125rem;font-weight:600;margin-bottom:4px;color:#33ff00;
92
+ }
93
+ .card-subtitle{color:#1a8c00;font-size:.8125rem;margin-bottom:18px;line-height:1.45;}
94
+ .powered-by{
95
+ position:fixed;bottom:0;left:0;right:0;
96
+ text-align:center;padding:12px;font-size:.6875rem;color:#0a3300;
97
+ font-family:'Geist Pixel',monospace;
98
+ }
99
+ .powered-by a{color:#1a8c00;text-decoration:none;transition:color .15s;}
100
+ .powered-by a:hover{color:#33ff00;}
101
+
102
+ .error-title{
103
+ font-family:'Geist Pixel',monospace;
104
+ color:#ff4444;font-size:1.125rem;font-weight:600;margin-bottom:8px;
105
+ }
106
+ .error-msg{color:#1a8c00;font-size:.875rem;line-height:1.5;}
107
+ .error-card{text-align:center;}
108
+
109
+ .user-form{margin-bottom:8px;}
110
+ .user-form:last-of-type{margin-bottom:0;}
111
+ .user-btn{
112
+ width:100%;display:flex;align-items:center;gap:12px;
113
+ padding:10px 12px;border:1px solid #0a3300;border-radius:8px;
114
+ background:#000;color:inherit;cursor:pointer;text-align:left;
115
+ font:inherit;transition:border-color .15s;
116
+ }
117
+ .user-btn:hover{border-color:#33ff00;}
118
+ .avatar{
119
+ width:36px;height:36px;border-radius:50%;
120
+ background:#0a3300;color:#33ff00;font-weight:600;font-size:.875rem;
121
+ display:flex;align-items:center;justify-content:center;flex-shrink:0;
122
+ font-family:'Geist Pixel',monospace;
123
+ }
124
+ .user-text{min-width:0;}
125
+ .user-login{font-weight:600;font-size:.875rem;display:block;color:#33ff00;}
126
+ .user-meta{color:#1a8c00;font-size:.75rem;margin-top:1px;}
127
+ .user-email{font-size:.6875rem;color:#116600;word-break:break-all;margin-top:1px;}
128
+
129
+ .settings-layout{
130
+ max-width:920px;margin:0 auto;padding:28px 20px;
131
+ display:flex;gap:28px;
132
+ }
133
+ .settings-sidebar{width:200px;flex-shrink:0;}
134
+ .settings-sidebar a{
135
+ display:block;padding:6px 10px;border-radius:6px;color:#1a8c00;
136
+ text-decoration:none;font-size:.8125rem;transition:color .15s;
137
+ }
138
+ .settings-sidebar a:hover{color:#33ff00;}
139
+ .settings-sidebar a.active{color:#33ff00;font-weight:600;}
140
+ .settings-main{flex:1;min-width:0;}
141
+
142
+ .s-card{
143
+ padding:18px 0;margin-bottom:14px;border-bottom:1px solid #0a3300;
144
+ }
145
+ .s-card:last-child{border-bottom:none;}
146
+ .s-card-header{display:flex;align-items:center;gap:14px;margin-bottom:14px;}
147
+ .s-icon{
148
+ width:42px;height:42px;border-radius:8px;
149
+ background:#0a3300;display:flex;align-items:center;justify-content:center;
150
+ font-size:1.125rem;font-weight:700;color:#116600;flex-shrink:0;
151
+ font-family:'Geist Pixel',monospace;
152
+ }
153
+ .s-title{
154
+ font-family:'Geist Pixel',monospace;
155
+ font-size:1.25rem;font-weight:600;color:#33ff00;
156
+ }
157
+ .s-subtitle{font-size:.75rem;color:#1a8c00;margin-top:2px;}
158
+ .section-heading{
159
+ font-size:.9375rem;font-weight:600;margin-bottom:10px;color:#33ff00;
160
+ display:flex;align-items:center;justify-content:space-between;
161
+ }
162
+ .perm-list{list-style:none;}
163
+ .perm-list li{padding:5px 0;font-size:.8125rem;display:flex;align-items:center;gap:6px;color:#1a8c00;}
164
+ .check{color:#33ff00;}
165
+ .org-row{
166
+ display:flex;align-items:center;gap:8px;padding:7px 0;
167
+ border-bottom:1px solid #0a3300;font-size:.8125rem;
168
+ }
169
+ .org-row:last-child{border-bottom:none;}
170
+ .org-icon{
171
+ width:22px;height:22px;border-radius:4px;background:#0a3300;
172
+ display:flex;align-items:center;justify-content:center;
173
+ font-size:.625rem;font-weight:700;color:#116600;flex-shrink:0;
174
+ font-family:'Geist Pixel',monospace;
175
+ }
176
+ .org-name{font-weight:600;color:#33ff00;}
177
+ .badge{font-size:.6875rem;padding:1px 7px;border-radius:999px;font-weight:500;}
178
+ .badge-granted{background:#0a3300;color:#33ff00;}
179
+ .badge-denied{background:#1a0a0a;color:#ff4444;}
180
+ .badge-requested{background:#0a3300;color:#1a8c00;}
181
+ .btn-revoke{
182
+ display:inline-block;padding:5px 14px;border-radius:6px;
183
+ border:1px solid #0a3300;background:transparent;color:#ff4444;
184
+ font-size:.75rem;font-weight:600;cursor:pointer;transition:border-color .15s;
185
+ }
186
+ .btn-revoke:hover{border-color:#ff4444;}
187
+ .info-text{color:#1a8c00;font-size:.75rem;line-height:1.5;margin-top:10px;}
188
+ .info-text a,.section-heading a{color:#1a8c00;text-decoration:none;transition:color .15s;}
189
+ .info-text a:hover,.section-heading a:hover{color:#33ff00;}
190
+ code{font-family:'Geist Mono','SF Mono',ui-monospace,monospace;font-size:.8125rem;color:#33ff00;word-break:break-all;}
191
+ .code-block{
192
+ background:#020;border:1px solid #0a3300;border-radius:6px;padding:10px 12px;
193
+ margin:8px 0 12px;overflow-x:auto;
194
+ }
195
+ .code-block code{white-space:pre;word-break:normal;display:block;line-height:1.5;}
196
+ .app-link{
197
+ display:flex;align-items:center;gap:12px;padding:12px;
198
+ border:1px solid #0a3300;border-radius:8px;background:#000;
199
+ text-decoration:none;color:inherit;margin-bottom:8px;transition:border-color .15s;
200
+ }
201
+ .app-link:hover{border-color:#33ff00;}
202
+ .app-link-name{font-weight:600;font-size:.875rem;color:#33ff00;}
203
+ .app-link-scopes{font-size:.6875rem;color:#1a8c00;margin-top:1px;}
204
+ .empty{color:#1a8c00;text-align:center;padding:28px 0;font-size:.875rem;}
205
+
206
+ .inspector-layout{max-width:960px;margin:0 auto;padding:28px 20px;}
207
+ .inspector-tabs{display:flex;gap:4px;margin-bottom:20px;}
208
+ .inspector-tabs a{
209
+ padding:7px 16px;border-radius:6px;text-decoration:none;
210
+ font-size:.8125rem;color:#1a8c00;border:1px solid transparent;
211
+ transition:color .15s,border-color .15s;
212
+ }
213
+ .inspector-tabs a:hover{color:#33ff00;}
214
+ .inspector-tabs a.active{color:#33ff00;font-weight:600;border-color:#0a3300;background:#0a3300;}
215
+ .inspector-section{margin-bottom:24px;}
216
+ .inspector-section h2{
217
+ font-family:'Geist Pixel',monospace;
218
+ font-size:1rem;font-weight:600;color:#33ff00;margin-bottom:10px;
219
+ }
220
+ .inspector-section h3{
221
+ font-family:'Geist Pixel',monospace;
222
+ font-size:.875rem;font-weight:600;color:#1a8c00;margin:16px 0 8px;
223
+ }
224
+ .inspector-table{width:100%;border-collapse:collapse;margin-bottom:12px;}
225
+ .inspector-table th,.inspector-table td{
226
+ text-align:left;padding:8px 12px;border-bottom:1px solid #0a3300;
227
+ font-size:.8125rem;
228
+ }
229
+ .inspector-table th{color:#1a8c00;font-weight:600;font-size:.75rem;text-transform:uppercase;letter-spacing:.04em;}
230
+ .inspector-table td{color:#33ff00;}
231
+ .inspector-table tbody tr{transition:background .1s;}
232
+ .inspector-table tbody tr:hover{background:#0a3300;}
233
+ .inspector-empty{color:#1a8c00;text-align:center;padding:20px 0;font-size:.8125rem;}
234
+
235
+ .checkout-layout{
236
+ display:flex;min-height:calc(100vh - 42px);
237
+ }
238
+ .checkout-summary{
239
+ flex:1;background:#020;padding:48px 40px 48px 10%;
240
+ display:flex;flex-direction:column;justify-content:center;
241
+ border-right:1px solid #0a3300;
242
+ }
243
+ .checkout-form-side{
244
+ flex:1;background:#000;padding:48px 10% 48px 40px;
245
+ display:flex;flex-direction:column;justify-content:center;
246
+ }
247
+ .checkout-merchant{
248
+ display:flex;align-items:center;gap:10px;margin-bottom:6px;
249
+ }
250
+ .checkout-merchant-name{
251
+ font-family:'Geist Pixel',monospace;
252
+ font-size:.9375rem;font-weight:600;color:#33ff00;
253
+ }
254
+ .checkout-test-badge{
255
+ font-size:.625rem;font-weight:700;letter-spacing:.04em;text-transform:uppercase;
256
+ background:#0a3300;color:#1a8c00;padding:2px 8px;border-radius:4px;
257
+ }
258
+ .checkout-total{
259
+ font-family:'Geist Pixel',monospace;
260
+ font-size:2rem;font-weight:700;color:#33ff00;margin:8px 0 28px;
261
+ }
262
+ .checkout-line-item{
263
+ display:flex;align-items:center;gap:14px;padding:14px 0;
264
+ border-bottom:1px solid #0a3300;
265
+ }
266
+ .checkout-line-item:first-child{border-top:1px solid #0a3300;}
267
+ .checkout-item-icon{
268
+ width:42px;height:42px;border-radius:6px;background:#0a3300;
269
+ display:flex;align-items:center;justify-content:center;flex-shrink:0;
270
+ font-family:'Geist Pixel',monospace;font-size:.875rem;font-weight:700;color:#116600;
271
+ }
272
+ .checkout-item-details{flex:1;min-width:0;}
273
+ .checkout-item-name{font-size:.875rem;font-weight:600;color:#33ff00;}
274
+ .checkout-item-qty{font-size:.75rem;color:#1a8c00;margin-top:2px;}
275
+ .checkout-item-price{
276
+ font-size:.875rem;font-weight:600;color:#33ff00;text-align:right;white-space:nowrap;
277
+ }
278
+ .checkout-item-unit{font-size:.6875rem;color:#1a8c00;text-align:right;margin-top:2px;}
279
+ .checkout-totals{margin-top:20px;}
280
+ .checkout-totals-row{
281
+ display:flex;justify-content:space-between;padding:6px 0;
282
+ font-size:.8125rem;color:#1a8c00;
283
+ }
284
+ .checkout-totals-row.total{
285
+ border-top:1px solid #0a3300;margin-top:8px;padding-top:14px;
286
+ font-size:.9375rem;font-weight:600;color:#33ff00;
287
+ }
288
+ .checkout-form-section{margin-bottom:24px;}
289
+ .checkout-form-label{
290
+ font-size:.8125rem;font-weight:600;color:#33ff00;margin-bottom:8px;display:block;
291
+ }
292
+ .checkout-input{
293
+ width:100%;padding:10px 12px;border:1px solid #0a3300;border-radius:6px;
294
+ background:#020;color:#33ff00;font:inherit;font-size:.875rem;
295
+ transition:border-color .15s;outline:none;
296
+ }
297
+ .checkout-input:focus{border-color:#33ff00;}
298
+ .checkout-input::placeholder{color:#116600;}
299
+ .checkout-card-box{
300
+ border:1px solid #0a3300;border-radius:6px;padding:14px;
301
+ background:#020;
302
+ }
303
+ .checkout-card-row{
304
+ display:flex;gap:12px;margin-top:10px;
305
+ }
306
+ .checkout-card-row .checkout-input{flex:1;}
307
+ .checkout-sim-note{
308
+ font-size:.6875rem;color:#1a8c00;margin-top:10px;text-align:center;
309
+ font-style:italic;
310
+ }
311
+ .checkout-pay-btn{
312
+ width:100%;padding:14px;border:none;border-radius:8px;
313
+ background:#33ff00;color:#000;font:inherit;font-size:.9375rem;font-weight:700;
314
+ cursor:pointer;transition:background .15s;
315
+ font-family:'Geist Pixel',monospace;
316
+ }
317
+ .checkout-pay-btn:hover{background:#44ff22;}
318
+ .checkout-cancel{
319
+ text-align:center;margin-top:14px;
320
+ }
321
+ .checkout-cancel a{
322
+ color:#1a8c00;text-decoration:none;font-size:.8125rem;
323
+ transition:color .15s;
324
+ }
325
+ .checkout-cancel a:hover{color:#33ff00;}
326
+ @media(max-width:768px){
327
+ .checkout-layout{flex-direction:column;}
328
+ .checkout-summary{padding:32px 20px;border-right:none;border-bottom:1px solid #0a3300;}
329
+ .checkout-form-side{padding:32px 20px;}
330
+ }
331
+ `;
332
+ var POWERED_BY = `<div class="powered-by">Powered by <a href="https://emulate.dev" target="_blank" rel="noopener">emulate</a></div>`;
333
+ function emuBar(service) {
334
+ const title = service ? `${escapeHtml(service)} Emulator` : "Emulator";
335
+ return `<div class="emu-bar">
336
+ <span class="emu-bar-title">${title}</span>
337
+ <nav class="emu-bar-links">
338
+ <a href="https://github.com/vercel-labs/emulate/issues" target="_blank" rel="noopener"><span class="full">Report Issue</span><span class="short">Report</span></a>
339
+ <a href="https://github.com/vercel-labs/emulate" target="_blank" rel="noopener"><span class="full">Source Code</span><span class="short">Source</span></a>
340
+ <a href="https://emulate.dev" target="_blank" rel="noopener"><span class="full">Learn More</span><span class="short">Learn</span></a>
341
+ </nav>
342
+ </div>`;
343
+ }
344
+ function head(title) {
345
+ return `<!DOCTYPE html>
346
+ <html lang="en">
347
+ <head>
348
+ <meta charset="utf-8"/>
349
+ <meta name="viewport" content="width=device-width,initial-scale=1"/>
350
+ <link rel="icon" href="/_emulate/favicon.ico"/>
351
+ <title>${escapeHtml(title)} | emulate</title>
352
+ <style>${CSS}</style>
353
+ </head>`;
354
+ }
355
+ function renderCardPage(title, subtitle, body, service) {
356
+ return `${head(title)}
357
+ <body>
358
+ ${emuBar(service)}
359
+ <div class="content">
360
+ <div class="content-inner">
361
+ <div class="card-title">${escapeHtml(title)}</div>
362
+ <div class="card-subtitle">${subtitle}</div>
363
+ ${body}
364
+ </div>
365
+ </div>
366
+ ${POWERED_BY}
367
+ </body></html>`;
368
+ }
369
+ function renderErrorPage(title, message, service) {
370
+ return `${head(title)}
371
+ <body>
372
+ ${emuBar(service)}
373
+ <div class="content">
374
+ <div class="content-inner error-card">
375
+ <div class="error-title">${escapeHtml(title)}</div>
376
+ <div class="error-msg">${escapeHtml(message)}</div>
377
+ </div>
378
+ </div>
379
+ ${POWERED_BY}
380
+ </body></html>`;
381
+ }
382
+ function renderUserButton(opts) {
383
+ const hiddens = Object.entries(opts.hiddenFields).map(([k, v]) => `<input type="hidden" name="${escapeAttr(k)}" value="${escapeAttr(v)}"/>`).join("");
384
+ const nameLine = opts.name ? `<div class="user-meta">${escapeHtml(opts.name)}</div>` : "";
385
+ const emailLine = opts.email ? `<div class="user-email">${escapeHtml(opts.email)}</div>` : "";
386
+ return `<form class="user-form" method="post" action="${escapeAttr(opts.formAction)}">
387
+ ${hiddens}
388
+ <button type="submit" class="user-btn">
389
+ <span class="avatar">${escapeHtml(opts.letter)}</span>
390
+ <span class="user-text">
391
+ <span class="user-login">${escapeHtml(opts.login)}</span>
392
+ ${nameLine}${emailLine}
393
+ </span>
394
+ </button>
395
+ </form>`;
396
+ }
397
+ function normalizeUri(uri) {
398
+ try {
399
+ const u = new URL(uri);
400
+ return `${u.origin}${u.pathname.replace(/\/+$/, "")}`;
401
+ } catch {
402
+ return uri.replace(/\/+$/, "").split("?")[0];
403
+ }
404
+ }
405
+ function matchesRedirectUri(incoming, registered) {
406
+ const normalized = normalizeUri(incoming);
407
+ return registered.some((r) => normalizeUri(r) === normalized);
408
+ }
409
+ function constantTimeSecretEqual(a, b) {
410
+ const bufA = Buffer.from(a, "utf-8");
411
+ const bufB = Buffer.from(b, "utf-8");
412
+ if (bufA.length !== bufB.length) return false;
413
+ return timingSafeEqual(bufA, bufB);
414
+ }
415
+ function bodyStr(v) {
416
+ if (typeof v === "string") return v;
417
+ if (Array.isArray(v) && typeof v[0] === "string") return v[0];
418
+ return "";
419
+ }
420
+ var SERVICE_LABEL = "X";
421
+ var AUTH_CODE_TTL_MS = 10 * 60 * 1e3;
422
+ var ACCESS_TOKEN_TTL_SECONDS = 7200;
423
+ var X_SCOPES = [
424
+ "tweet.read",
425
+ "tweet.write",
426
+ "tweet.moderate.write",
427
+ "users.read",
428
+ "follows.read",
429
+ "follows.write",
430
+ "offline.access",
431
+ "space.read",
432
+ "mute.read",
433
+ "mute.write",
434
+ "like.read",
435
+ "like.write",
436
+ "list.read",
437
+ "list.write",
438
+ "block.read",
439
+ "block.write",
440
+ "bookmark.read",
441
+ "bookmark.write"
442
+ ];
443
+ function parseClientCredentials(c, body) {
444
+ const basic = /^Basic\s+(.+)$/i.exec(c.req.header("Authorization") ?? "");
445
+ if (basic) {
446
+ try {
447
+ const decoded = Buffer.from(basic[1].trim(), "base64").toString("utf-8");
448
+ const sep = decoded.indexOf(":");
449
+ if (sep >= 0) {
450
+ return {
451
+ clientId: decodeURIComponent(decoded.slice(0, sep)),
452
+ clientSecret: decodeURIComponent(decoded.slice(sep + 1)),
453
+ fromBasic: true
454
+ };
455
+ }
456
+ } catch {
457
+ }
458
+ }
459
+ const clientId = typeof body.client_id === "string" ? body.client_id : "";
460
+ const clientSecret = typeof body.client_secret === "string" ? body.client_secret : null;
461
+ return { clientId, clientSecret, fromBasic: false };
462
+ }
463
+ function s256(verifier) {
464
+ return createHash("sha256").update(verifier).digest("base64url");
465
+ }
466
+ async function parseTokenBody(c) {
467
+ const contentType = c.req.header("Content-Type") ?? "";
468
+ const rawText = await c.req.text();
469
+ if (contentType.includes("application/json")) {
470
+ try {
471
+ return JSON.parse(rawText);
472
+ } catch {
473
+ return {};
474
+ }
475
+ }
476
+ return Object.fromEntries(new URLSearchParams(rawText));
477
+ }
478
+ function opaqueToken() {
479
+ return randomBytes(32).toString("base64url");
480
+ }
481
+ function oauthRoutes({ app, store, baseUrl }) {
482
+ const xs = getXStore(store);
483
+ function authenticateClient(creds, _body) {
484
+ if (!creds.clientId) {
485
+ return {
486
+ error: "invalid_request",
487
+ description: "A client_id is required. Public clients must include client_id in the request body.",
488
+ status: 400
489
+ };
490
+ }
491
+ const client = xs.oauthClients.findOneBy("client_id", creds.clientId);
492
+ if (!client) {
493
+ return { error: "invalid_client", description: "Unknown client_id.", status: 401 };
494
+ }
495
+ if (client.client_type === "confidential") {
496
+ if (!creds.fromBasic) {
497
+ return {
498
+ error: "invalid_client",
499
+ description: "Confidential clients must authenticate with HTTP Basic.",
500
+ status: 401
501
+ };
502
+ }
503
+ if (!constantTimeSecretEqual(creds.clientSecret ?? "", client.client_secret ?? "")) {
504
+ return { error: "invalid_client", description: "Invalid client credentials.", status: 401 };
505
+ }
506
+ } else {
507
+ if (creds.fromBasic || creds.clientSecret != null) {
508
+ return {
509
+ error: "invalid_client",
510
+ description: "Public clients have no client_secret and must authenticate with PKCE only.",
511
+ status: 401
512
+ };
513
+ }
514
+ }
515
+ return { client };
516
+ }
517
+ app.get("/2/oauth2/authorize", (c) => {
518
+ const client_id = c.req.query("client_id") ?? "";
519
+ const redirect_uri = c.req.query("redirect_uri") ?? "";
520
+ const scope = c.req.query("scope") ?? "";
521
+ const state = c.req.query("state") ?? "";
522
+ const response_type = c.req.query("response_type") ?? "";
523
+ const code_challenge = c.req.query("code_challenge") ?? "";
524
+ const code_challenge_method = c.req.query("code_challenge_method") ?? "";
525
+ const client = xs.oauthClients.findOneBy("client_id", client_id);
526
+ if (!client) {
527
+ return c.html(
528
+ renderErrorPage(
529
+ "Application not found",
530
+ `The client_id '${escapeHtml(client_id)}' is not registered.`,
531
+ SERVICE_LABEL
532
+ ),
533
+ 400
534
+ );
535
+ }
536
+ if (redirect_uri && !matchesRedirectUri(redirect_uri, client.redirect_uris)) {
537
+ return c.html(
538
+ renderErrorPage(
539
+ "Redirect URI mismatch",
540
+ "The redirect_uri is not registered for this application.",
541
+ SERVICE_LABEL
542
+ ),
543
+ 400
544
+ );
545
+ }
546
+ if (response_type && response_type !== "code") {
547
+ return c.html(
548
+ renderErrorPage("Unsupported response_type", "Only response_type=code is supported.", SERVICE_LABEL),
549
+ 400
550
+ );
551
+ }
552
+ if (!code_challenge) {
553
+ return c.html(
554
+ renderErrorPage(
555
+ "PKCE required",
556
+ "A code_challenge is required for the authorization code flow.",
557
+ SERVICE_LABEL
558
+ ),
559
+ 400
560
+ );
561
+ }
562
+ if (code_challenge_method.toUpperCase() !== "S256") {
563
+ return c.html(
564
+ renderErrorPage(
565
+ "Unsupported PKCE method",
566
+ "code_challenge_method must be S256 for the X authorization code flow.",
567
+ SERVICE_LABEL
568
+ ),
569
+ 400
570
+ );
571
+ }
572
+ const requestedScopes = scope.split(/[\s+]+/).filter(Boolean);
573
+ const unknownScope = requestedScopes.find((s) => !X_SCOPES.includes(s));
574
+ if (unknownScope) {
575
+ return c.html(
576
+ renderErrorPage("Invalid scope", `The scope '${escapeHtml(unknownScope)}' is not supported.`, SERVICE_LABEL),
577
+ 400
578
+ );
579
+ }
580
+ const users = [...xs.users.all()].sort((a, b) => a.username.localeCompare(b.username));
581
+ const subtitleText = `Authorize <strong>${escapeHtml(client.name)}</strong> to access your X account.`;
582
+ const userButtons = users.map(
583
+ (u) => renderUserButton({
584
+ letter: (u.name[0] ?? u.username[0] ?? "?").toUpperCase(),
585
+ login: `@${u.username}`,
586
+ name: u.name,
587
+ formAction: `${baseUrl}/2/oauth2/authorize/consent`,
588
+ hiddenFields: {
589
+ user_id: u.user_id,
590
+ redirect_uri,
591
+ scope,
592
+ state,
593
+ client_id,
594
+ code_challenge,
595
+ code_challenge_method
596
+ }
597
+ })
598
+ ).join("\n");
599
+ const body = users.length === 0 ? '<p class="empty">No users in the emulator store.</p>' : userButtons;
600
+ return c.html(renderCardPage("Sign in to X", subtitleText, body, SERVICE_LABEL));
601
+ });
602
+ app.post("/2/oauth2/authorize/consent", async (c) => {
603
+ const form = await c.req.parseBody();
604
+ const user_id = bodyStr(form.user_id);
605
+ const redirect_uri = bodyStr(form.redirect_uri);
606
+ const scope = bodyStr(form.scope);
607
+ const state = bodyStr(form.state);
608
+ const client_id = bodyStr(form.client_id);
609
+ const code_challenge = bodyStr(form.code_challenge);
610
+ const code_challenge_method = bodyStr(form.code_challenge_method);
611
+ const user = xs.users.findOneBy("user_id", user_id);
612
+ if (!user) {
613
+ return c.html(renderErrorPage("User not found", "The selected user no longer exists.", SERVICE_LABEL), 400);
614
+ }
615
+ const code = randomBytes(24).toString("base64url");
616
+ xs.authCodes.insert({
617
+ code,
618
+ client_id,
619
+ redirect_uri,
620
+ code_challenge,
621
+ code_challenge_method: code_challenge_method || "S256",
622
+ scopes: scope.split(/[\s+]+/).filter(Boolean),
623
+ user_id,
624
+ expires: Date.now() + AUTH_CODE_TTL_MS
625
+ });
626
+ debug("x.oauth", `[authorize] minted code for user ${user_id} client ${client_id}`);
627
+ const url = new URL(redirect_uri);
628
+ url.searchParams.set("code", code);
629
+ if (state) url.searchParams.set("state", state);
630
+ return c.redirect(url.toString(), 302);
631
+ });
632
+ app.post("/2/oauth2/token", async (c) => {
633
+ const body = await parseTokenBody(c);
634
+ const grant_type = typeof body.grant_type === "string" ? body.grant_type : "";
635
+ const creds = parseClientCredentials(c, body);
636
+ if (grant_type === "client_credentials") {
637
+ const auth = authenticateClient(creds, body);
638
+ if ("error" in auth) {
639
+ return c.json({ error: auth.error, error_description: auth.description }, auth.status);
640
+ }
641
+ if (auth.client.client_type !== "confidential") {
642
+ return c.json(
643
+ {
644
+ error: "unauthorized_client",
645
+ error_description: "Only confidential clients may use the client_credentials grant."
646
+ },
647
+ 400
648
+ );
649
+ }
650
+ const token = opaqueToken();
651
+ const expires = Date.now() + ACCESS_TOKEN_TTL_SECONDS * 1e3;
652
+ xs.accessTokens.insert({
653
+ token,
654
+ client_id: auth.client.client_id,
655
+ user_id: null,
656
+ scopes: [],
657
+ app_only: true,
658
+ expires
659
+ });
660
+ return c.json({
661
+ token_type: "bearer",
662
+ access_token: token,
663
+ expires_in: ACCESS_TOKEN_TTL_SECONDS
664
+ });
665
+ }
666
+ if (grant_type === "authorization_code") {
667
+ const auth = authenticateClient(creds, body);
668
+ if ("error" in auth) {
669
+ return c.json({ error: auth.error, error_description: auth.description }, auth.status);
670
+ }
671
+ const client = auth.client;
672
+ const code = typeof body.code === "string" ? body.code : "";
673
+ const redirect_uri = typeof body.redirect_uri === "string" ? body.redirect_uri : "";
674
+ const code_verifier = typeof body.code_verifier === "string" ? body.code_verifier : "";
675
+ const codeRow = xs.authCodes.findOneBy("code", code);
676
+ if (!codeRow || codeRow.expires < Date.now()) {
677
+ if (codeRow) xs.authCodes.delete(codeRow.id);
678
+ return c.json(
679
+ { error: "invalid_grant", error_description: "The authorization code is invalid or expired." },
680
+ 400
681
+ );
682
+ }
683
+ if (codeRow.client_id !== client.client_id) {
684
+ return c.json(
685
+ { error: "invalid_grant", error_description: "The authorization code was issued to another client." },
686
+ 400
687
+ );
688
+ }
689
+ if (codeRow.redirect_uri !== redirect_uri) {
690
+ return c.json(
691
+ { error: "invalid_grant", error_description: "The redirect_uri does not match the authorization request." },
692
+ 400
693
+ );
694
+ }
695
+ if (!code_verifier) {
696
+ return c.json({ error: "invalid_request", error_description: "A code_verifier is required (PKCE)." }, 400);
697
+ }
698
+ if (s256(code_verifier) !== codeRow.code_challenge) {
699
+ return c.json({ error: "invalid_grant", error_description: "PKCE verification failed." }, 400);
700
+ }
701
+ xs.authCodes.delete(codeRow.id);
702
+ const scopes = codeRow.scopes;
703
+ const token = opaqueToken();
704
+ const expires = Date.now() + ACCESS_TOKEN_TTL_SECONDS * 1e3;
705
+ xs.accessTokens.insert({
706
+ token,
707
+ client_id: client.client_id,
708
+ user_id: codeRow.user_id,
709
+ scopes,
710
+ app_only: false,
711
+ expires
712
+ });
713
+ const response = {
714
+ token_type: "bearer",
715
+ access_token: token,
716
+ scope: scopes.join(" "),
717
+ expires_in: ACCESS_TOKEN_TTL_SECONDS
718
+ };
719
+ if (scopes.includes("offline.access")) {
720
+ const refreshToken = opaqueToken();
721
+ xs.refreshTokens.insert({
722
+ token: refreshToken,
723
+ client_id: client.client_id,
724
+ user_id: codeRow.user_id,
725
+ scopes
726
+ });
727
+ response.refresh_token = refreshToken;
728
+ }
729
+ debug("x.oauth", `[token] authorization_code \u2192 user ${codeRow.user_id} scopes ${scopes.join(",")}`);
730
+ return c.json(response);
731
+ }
732
+ if (grant_type === "refresh_token") {
733
+ const auth = authenticateClient(creds, body);
734
+ if ("error" in auth) {
735
+ return c.json({ error: auth.error, error_description: auth.description }, auth.status);
736
+ }
737
+ const client = auth.client;
738
+ const refresh_token = typeof body.refresh_token === "string" ? body.refresh_token : "";
739
+ const row = xs.refreshTokens.findOneBy("token", refresh_token);
740
+ if (!row || row.client_id !== client.client_id) {
741
+ return c.json({ error: "invalid_grant", error_description: "The refresh token is invalid." }, 400);
742
+ }
743
+ if (!row.scopes.includes("offline.access")) {
744
+ return c.json(
745
+ { error: "invalid_grant", error_description: "The refresh token does not have offline.access." },
746
+ 400
747
+ );
748
+ }
749
+ const token = opaqueToken();
750
+ const expires = Date.now() + ACCESS_TOKEN_TTL_SECONDS * 1e3;
751
+ xs.accessTokens.insert({
752
+ token,
753
+ client_id: client.client_id,
754
+ user_id: row.user_id,
755
+ scopes: row.scopes,
756
+ app_only: false,
757
+ expires
758
+ });
759
+ const newRefresh = opaqueToken();
760
+ xs.refreshTokens.update(row.id, { token: newRefresh });
761
+ return c.json({
762
+ token_type: "bearer",
763
+ access_token: token,
764
+ scope: row.scopes.join(" "),
765
+ expires_in: ACCESS_TOKEN_TTL_SECONDS,
766
+ refresh_token: newRefresh
767
+ });
768
+ }
769
+ return c.json(
770
+ {
771
+ error: "unsupported_grant_type",
772
+ error_description: "grant_type must be authorization_code, refresh_token, or client_credentials."
773
+ },
774
+ 400
775
+ );
776
+ });
777
+ app.post("/2/oauth2/revoke", async (c) => {
778
+ const body = await parseTokenBody(c);
779
+ const creds = parseClientCredentials(c, body);
780
+ const auth = authenticateClient(creds, body);
781
+ if ("error" in auth) {
782
+ return c.json({ error: auth.error, error_description: auth.description }, auth.status);
783
+ }
784
+ const token = typeof body.token === "string" ? body.token : "";
785
+ if (token) {
786
+ const access = xs.accessTokens.findOneBy("token", token);
787
+ if (access && access.client_id === auth.client.client_id) {
788
+ xs.accessTokens.delete(access.id);
789
+ }
790
+ const refresh = xs.refreshTokens.findOneBy("token", token);
791
+ if (refresh && refresh.client_id === auth.client.client_id) {
792
+ xs.refreshTokens.delete(refresh.id);
793
+ }
794
+ }
795
+ return c.json({ revoked: true });
796
+ });
797
+ }
798
+ function resolveToken(store, c) {
799
+ const m = /^Bearer\s+(.+)$/i.exec(c.req.header("Authorization") ?? "");
800
+ if (!m) return null;
801
+ return lookupAccessToken(store, m[1].trim()) ?? null;
802
+ }
803
+ function unauthorized(c) {
804
+ return c.json(
805
+ {
806
+ title: "Unauthorized",
807
+ type: "about:blank",
808
+ status: 401,
809
+ detail: "Unauthorized"
810
+ },
811
+ 401
812
+ );
813
+ }
814
+ function forbidden(c, detail) {
815
+ return c.json(
816
+ {
817
+ title: "Forbidden",
818
+ type: "https://api.twitter.com/2/problems/oauth2-insufficient-scope",
819
+ status: 403,
820
+ detail
821
+ },
822
+ 403
823
+ );
824
+ }
825
+ function hasUserScope(token, ...required) {
826
+ if (token.app_only) return false;
827
+ return required.every((s) => token.scopes.includes(s));
828
+ }
829
+ function formatUser(u) {
830
+ return {
831
+ id: u.user_id,
832
+ name: u.name,
833
+ username: u.username,
834
+ created_at: u.created_at_x,
835
+ description: u.description,
836
+ location: u.location,
837
+ url: u.url,
838
+ protected: u.protected,
839
+ verified: u.verified,
840
+ profile_image_url: u.profile_image_url,
841
+ public_metrics: {
842
+ followers_count: u.followers_count,
843
+ following_count: u.following_count,
844
+ tweet_count: u.tweet_count,
845
+ listed_count: u.listed_count
846
+ }
847
+ };
848
+ }
849
+ function notFound(c, detail) {
850
+ return c.json(
851
+ {
852
+ errors: [
853
+ {
854
+ title: "Not Found Error",
855
+ type: "https://api.twitter.com/2/problems/resource-not-found",
856
+ detail
857
+ }
858
+ ]
859
+ },
860
+ 404
861
+ );
862
+ }
863
+ function usersRoutes({ app, store }) {
864
+ const xs = getXStore(store);
865
+ app.get("/2/users/me", (c) => {
866
+ const token = resolveToken(store, c);
867
+ if (!token) return unauthorized(c);
868
+ if (token.app_only) {
869
+ return forbidden(c, "This endpoint requires a user-context OAuth 2.0 token; an app-only token has no user.");
870
+ }
871
+ if (!hasUserScope(token, "users.read")) {
872
+ return forbidden(c, "Your token is missing the users.read scope required by this endpoint.");
873
+ }
874
+ const user = token.user_id ? xs.users.findOneBy("user_id", token.user_id) : void 0;
875
+ if (!user) return unauthorized(c);
876
+ return c.json({ data: formatUser(user) });
877
+ });
878
+ app.get("/2/users/:id", (c) => {
879
+ const token = resolveToken(store, c);
880
+ if (!token) return unauthorized(c);
881
+ if (!token.app_only && !hasUserScope(token, "users.read")) {
882
+ return forbidden(c, "Your token is missing the users.read scope required by this endpoint.");
883
+ }
884
+ const user = xs.users.findOneBy("user_id", c.req.param("id"));
885
+ if (!user) return notFound(c, `Could not find user with id: [${c.req.param("id")}].`);
886
+ return c.json({ data: formatUser(user) });
887
+ });
888
+ app.get("/2/users/by/username/:username", (c) => {
889
+ const token = resolveToken(store, c);
890
+ if (!token) return unauthorized(c);
891
+ if (!token.app_only && !hasUserScope(token, "users.read")) {
892
+ return forbidden(c, "Your token is missing the users.read scope required by this endpoint.");
893
+ }
894
+ const username = c.req.param("username").toLowerCase();
895
+ const user = xs.users.findOneBy("username", username);
896
+ if (!user) return notFound(c, `Could not find user with username: [${c.req.param("username")}].`);
897
+ return c.json({ data: formatUser(user) });
898
+ });
899
+ }
900
+ function formatTweet(t) {
901
+ return {
902
+ id: t.tweet_id,
903
+ text: t.text,
904
+ author_id: t.author_id,
905
+ created_at: t.created_at_x,
906
+ conversation_id: t.conversation_id,
907
+ lang: t.lang,
908
+ possibly_sensitive: t.possibly_sensitive,
909
+ in_reply_to_user_id: t.in_reply_to_user_id,
910
+ edit_history_tweet_ids: [t.tweet_id],
911
+ public_metrics: {
912
+ retweet_count: t.retweet_count,
913
+ reply_count: t.reply_count,
914
+ like_count: t.like_count,
915
+ quote_count: t.quote_count,
916
+ impression_count: t.impression_count
917
+ }
918
+ };
919
+ }
920
+ function notFound2(c, detail) {
921
+ return c.json(
922
+ {
923
+ errors: [
924
+ {
925
+ title: "Not Found Error",
926
+ type: "https://api.twitter.com/2/problems/resource-not-found",
927
+ detail
928
+ }
929
+ ]
930
+ },
931
+ 404
932
+ );
933
+ }
934
+ function tweetsRoutes({ app, store }) {
935
+ const xs = getXStore(store);
936
+ app.get("/2/tweets/:id", (c) => {
937
+ const token = resolveToken(store, c);
938
+ if (!token) return unauthorized(c);
939
+ if (!token.app_only && !hasUserScope(token, "tweet.read")) {
940
+ return forbidden(c, "Your token is missing the tweet.read scope required by this endpoint.");
941
+ }
942
+ const tweet = xs.tweets.findOneBy("tweet_id", c.req.param("id"));
943
+ if (!tweet) return notFound2(c, `Could not find tweet with id: [${c.req.param("id")}].`);
944
+ return c.json({ data: formatTweet(tweet) });
945
+ });
946
+ app.get("/2/tweets", (c) => {
947
+ const token = resolveToken(store, c);
948
+ if (!token) return unauthorized(c);
949
+ if (!token.app_only && !hasUserScope(token, "tweet.read")) {
950
+ return forbidden(c, "Your token is missing the tweet.read scope required by this endpoint.");
951
+ }
952
+ const idsParam = c.req.query("ids") ?? "";
953
+ const ids = idsParam.split(",").map((s) => s.trim()).filter(Boolean);
954
+ if (ids.length === 0) {
955
+ return c.json(
956
+ {
957
+ errors: [
958
+ {
959
+ parameters: { ids: [] },
960
+ message: "The `ids` query parameter is required and must be a comma-separated list of Tweet IDs."
961
+ }
962
+ ],
963
+ title: "Invalid Request",
964
+ detail: "One or more parameters to your request was invalid.",
965
+ type: "https://api.twitter.com/2/problems/invalid-request"
966
+ },
967
+ 400
968
+ );
969
+ }
970
+ const data = [];
971
+ const errors = [];
972
+ for (const id of ids) {
973
+ const tweet = xs.tweets.findOneBy("tweet_id", id);
974
+ if (tweet) {
975
+ data.push(formatTweet(tweet));
976
+ } else {
977
+ errors.push({
978
+ value: id,
979
+ detail: `Could not find tweet with ids: [${id}].`,
980
+ title: "Not Found Error",
981
+ resource_type: "tweet",
982
+ parameter: "ids",
983
+ resource_id: id,
984
+ type: "https://api.twitter.com/2/problems/resource-not-found"
985
+ });
986
+ }
987
+ }
988
+ const out = { data };
989
+ if (errors.length > 0) out.errors = errors;
990
+ return c.json(out);
991
+ });
992
+ app.get("/2/users/:id/tweets", (c) => {
993
+ const token = resolveToken(store, c);
994
+ if (!token) return unauthorized(c);
995
+ if (!token.app_only && !hasUserScope(token, "tweet.read")) {
996
+ return forbidden(c, "Your token is missing the tweet.read scope required by this endpoint.");
997
+ }
998
+ const authorId = c.req.param("id");
999
+ const author = xs.users.findOneBy("user_id", authorId);
1000
+ if (!author) return notFound2(c, `Could not find user with id: [${authorId}].`);
1001
+ const tweets = xs.tweets.findBy("author_id", authorId).sort((a, b) => b.created_at_x.localeCompare(a.created_at_x));
1002
+ return c.json({
1003
+ data: tweets.map(formatTweet),
1004
+ meta: {
1005
+ result_count: tweets.length,
1006
+ newest_id: tweets[0]?.tweet_id,
1007
+ oldest_id: tweets[tweets.length - 1]?.tweet_id
1008
+ }
1009
+ });
1010
+ });
1011
+ app.post("/2/tweets", async (c) => {
1012
+ const token = resolveToken(store, c);
1013
+ if (!token) return unauthorized(c);
1014
+ if (token.app_only) {
1015
+ return forbidden(c, "Creating a Tweet requires a user-context OAuth 2.0 token; an app-only token cannot post.");
1016
+ }
1017
+ if (!hasUserScope(token, "tweet.write")) {
1018
+ return forbidden(c, "Your token is missing the tweet.write scope required to create a Tweet.");
1019
+ }
1020
+ const author = token.user_id ? xs.users.findOneBy("user_id", token.user_id) : void 0;
1021
+ if (!author) return unauthorized(c);
1022
+ const body = await c.req.json().catch(() => ({}));
1023
+ const text = typeof body.text === "string" ? body.text : "";
1024
+ if (!text.trim()) {
1025
+ return c.json(
1026
+ {
1027
+ errors: [{ message: "text or media is required", parameters: {} }],
1028
+ title: "Invalid Request",
1029
+ detail: "One or more parameters to your request was invalid.",
1030
+ type: "https://api.twitter.com/2/problems/invalid-request"
1031
+ },
1032
+ 400
1033
+ );
1034
+ }
1035
+ const reply = body.reply;
1036
+ const inReplyToTweetId = reply && typeof reply.in_reply_to_tweet_id === "string" ? reply.in_reply_to_tweet_id : null;
1037
+ const parent = inReplyToTweetId ? xs.tweets.findOneBy("tweet_id", inReplyToTweetId) : void 0;
1038
+ const tweetId = xNumericId();
1039
+ const tweet = xs.tweets.insert({
1040
+ tweet_id: tweetId,
1041
+ author_id: author.user_id,
1042
+ text,
1043
+ reply_count: 0,
1044
+ retweet_count: 0,
1045
+ like_count: 0,
1046
+ quote_count: 0,
1047
+ impression_count: 0,
1048
+ in_reply_to_user_id: parent ? parent.author_id : null,
1049
+ conversation_id: parent ? parent.conversation_id : tweetId,
1050
+ lang: "en",
1051
+ possibly_sensitive: false,
1052
+ created_at_x: (/* @__PURE__ */ new Date()).toISOString()
1053
+ });
1054
+ xs.users.update(author.id, { tweet_count: author.tweet_count + 1 });
1055
+ if (parent) xs.tweets.update(parent.id, { reply_count: parent.reply_count + 1 });
1056
+ return c.json({ data: { id: tweet.tweet_id, text: tweet.text } }, 201);
1057
+ });
1058
+ app.delete("/2/tweets/:id", (c) => {
1059
+ const token = resolveToken(store, c);
1060
+ if (!token) return unauthorized(c);
1061
+ if (token.app_only) {
1062
+ return forbidden(c, "Deleting a Tweet requires a user-context OAuth 2.0 token; an app-only token cannot delete.");
1063
+ }
1064
+ if (!hasUserScope(token, "tweet.write")) {
1065
+ return forbidden(c, "Your token is missing the tweet.write scope required to delete a Tweet.");
1066
+ }
1067
+ const tweet = xs.tweets.findOneBy("tweet_id", c.req.param("id"));
1068
+ if (!tweet) return notFound2(c, `Could not find tweet with id: [${c.req.param("id")}].`);
1069
+ if (token.user_id && tweet.author_id !== token.user_id) {
1070
+ return forbidden(c, "You may only delete your own Tweets.");
1071
+ }
1072
+ xs.tweets.delete(tweet.id);
1073
+ return c.json({ data: { deleted: true } });
1074
+ });
1075
+ }
1076
+ function openapiRoutes({ app, baseUrl }) {
1077
+ const handler = (c) => c.json(buildSpec(baseUrl));
1078
+ app.get("/2/openapi.json", handler);
1079
+ app.get("/openapi.json", handler);
1080
+ }
1081
+ var ok = (description) => ({
1082
+ description,
1083
+ content: { "application/json": { schema: { type: "object" } } }
1084
+ });
1085
+ function buildSpec(baseUrl) {
1086
+ const userScopes = Object.fromEntries(X_SCOPES.map((s) => [s, `Scope: ${s}`]));
1087
+ return {
1088
+ openapi: "3.0.3",
1089
+ info: {
1090
+ title: "X API v2 (Emulated)",
1091
+ version: "2.0.0",
1092
+ description: "Emulated subset of the X (formerly Twitter) API v2. Supports app-only Bearer tokens (client_credentials), the OAuth 2.0 Authorization Code flow with PKCE (user context), and a documented-partial legacy OAuth 1.0a user context."
1093
+ },
1094
+ servers: [{ url: baseUrl }],
1095
+ components: {
1096
+ securitySchemes: {
1097
+ // App-only Bearer token, minted via POST /2/oauth2/token grant_type=client_credentials.
1098
+ BearerToken: {
1099
+ type: "http",
1100
+ scheme: "bearer",
1101
+ description: "App-only Bearer token (OAuth 2.0 client_credentials). Minted with client_secret_basic (HTTP Basic) only; client_secret_post is not supported. Read-only public endpoints."
1102
+ },
1103
+ // OAuth 2.0 Authorization Code with PKCE (S256). User context.
1104
+ OAuth2UserToken: {
1105
+ type: "oauth2",
1106
+ description: "OAuth 2.0 Authorization Code with PKCE (S256). User-context access and writes. Confidential clients authenticate with client_secret_basic (HTTP Basic) only; client_secret_post is not supported.",
1107
+ flows: {
1108
+ authorizationCode: {
1109
+ authorizationUrl: `${baseUrl}/2/oauth2/authorize`,
1110
+ tokenUrl: `${baseUrl}/2/oauth2/token`,
1111
+ scopes: userScopes
1112
+ }
1113
+ }
1114
+ },
1115
+ // Legacy OAuth 1.0a user context — declared faithfully but NOT implemented.
1116
+ UserToken: {
1117
+ type: "http",
1118
+ scheme: "OAuth",
1119
+ description: "Legacy OAuth 1.0a user context. Declared for fidelity but emulated as a partial/unsupported surface; the emulator does not validate OAuth 1.0a signatures."
1120
+ }
1121
+ }
1122
+ },
1123
+ security: [{ BearerToken: [] }, { OAuth2UserToken: [...X_SCOPES] }],
1124
+ paths: {
1125
+ "/2/oauth2/token": {
1126
+ post: {
1127
+ operationId: "oauth2Token",
1128
+ summary: "Token endpoint (authorization_code, refresh_token, client_credentials)",
1129
+ responses: { "200": ok("Token response."), "400": ok("OAuth error."), "401": ok("invalid_client.") }
1130
+ }
1131
+ },
1132
+ "/2/oauth2/revoke": {
1133
+ post: { operationId: "oauth2Revoke", summary: "Revoke a token", responses: { "200": ok("Revoked.") } }
1134
+ },
1135
+ "/2/users/me": {
1136
+ get: {
1137
+ operationId: "findMyUser",
1138
+ summary: "Get the authenticated user",
1139
+ security: [{ OAuth2UserToken: ["users.read"] }],
1140
+ responses: { "200": ok("User object."), "401": ok("Unauthorized."), "403": ok("Insufficient scope.") }
1141
+ }
1142
+ },
1143
+ "/2/users/{id}": {
1144
+ get: {
1145
+ operationId: "findUserById",
1146
+ summary: "Get a user by id",
1147
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
1148
+ security: [{ BearerToken: [] }, { OAuth2UserToken: ["users.read"] }],
1149
+ responses: { "200": ok("User object."), "404": ok("Not found.") }
1150
+ }
1151
+ },
1152
+ "/2/users/by/username/{username}": {
1153
+ get: {
1154
+ operationId: "findUserByUsername",
1155
+ summary: "Get a user by username",
1156
+ parameters: [{ name: "username", in: "path", required: true, schema: { type: "string" } }],
1157
+ security: [{ BearerToken: [] }, { OAuth2UserToken: ["users.read"] }],
1158
+ responses: { "200": ok("User object."), "404": ok("Not found.") }
1159
+ }
1160
+ },
1161
+ "/2/users/{id}/tweets": {
1162
+ get: {
1163
+ operationId: "usersIdTweets",
1164
+ summary: "Get a user's Tweets timeline",
1165
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
1166
+ security: [{ BearerToken: [] }, { OAuth2UserToken: ["tweet.read", "users.read"] }],
1167
+ responses: { "200": ok("Tweet list."), "404": ok("Not found.") }
1168
+ }
1169
+ },
1170
+ "/2/tweets/{id}": {
1171
+ get: {
1172
+ operationId: "findTweetById",
1173
+ summary: "Get a Tweet by id",
1174
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
1175
+ security: [{ BearerToken: [] }, { OAuth2UserToken: ["tweet.read"] }],
1176
+ responses: { "200": ok("Tweet object."), "404": ok("Not found.") }
1177
+ },
1178
+ delete: {
1179
+ operationId: "deleteTweetById",
1180
+ summary: "Delete a Tweet",
1181
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
1182
+ security: [{ OAuth2UserToken: ["tweet.write"] }],
1183
+ responses: { "200": ok("Deletion result."), "403": ok("Insufficient scope.") }
1184
+ }
1185
+ },
1186
+ "/2/tweets": {
1187
+ get: {
1188
+ operationId: "findTweetsById",
1189
+ summary: "Get Tweets by ids",
1190
+ parameters: [{ name: "ids", in: "query", required: true, schema: { type: "string" } }],
1191
+ security: [{ BearerToken: [] }, { OAuth2UserToken: ["tweet.read"] }],
1192
+ responses: { "200": ok("Tweet list."), "400": ok("Invalid request.") }
1193
+ },
1194
+ post: {
1195
+ operationId: "createTweet",
1196
+ summary: "Create a Tweet",
1197
+ security: [{ OAuth2UserToken: ["tweet.write"] }],
1198
+ requestBody: {
1199
+ required: true,
1200
+ content: {
1201
+ "application/json": {
1202
+ schema: { type: "object", required: ["text"], properties: { text: { type: "string" } } }
1203
+ }
1204
+ }
1205
+ },
1206
+ responses: { "201": ok("Created Tweet."), "403": ok("Insufficient scope.") }
1207
+ }
1208
+ }
1209
+ }
1210
+ };
1211
+ }
1212
+ var manifest = {
1213
+ id: "x",
1214
+ name: "X",
1215
+ description: "Stateful X (formerly Twitter) API v2 emulator focused on faithful auth: app-only Bearer tokens, OAuth 2.0 Authorization Code with PKCE, and a documented-partial legacy OAuth 1.0a surface.",
1216
+ docsUrl: "https://docs.emulators.dev/x",
1217
+ surfaces: [
1218
+ { id: "rest", kind: "rest", title: "X API v2 (REST)", status: "partial", basePath: "/2" },
1219
+ {
1220
+ id: "oauth2",
1221
+ kind: "oauth",
1222
+ title: "OAuth 2.0 Authorization Code (PKCE)",
1223
+ status: "supported",
1224
+ basePath: "/2/oauth2"
1225
+ },
1226
+ {
1227
+ id: "app-only",
1228
+ kind: "oauth",
1229
+ title: "App-only Bearer token (client credentials)",
1230
+ status: "supported",
1231
+ basePath: "/2/oauth2/token"
1232
+ },
1233
+ {
1234
+ id: "oauth1",
1235
+ kind: "provider-specific",
1236
+ title: "Legacy OAuth 1.0a user context",
1237
+ status: "unsupported",
1238
+ notes: "Declared for fidelity. Signature validation is not implemented."
1239
+ }
1240
+ ],
1241
+ auth: [
1242
+ {
1243
+ id: "bearer-token",
1244
+ title: "App-only Bearer token",
1245
+ type: "bearer-token",
1246
+ status: "supported",
1247
+ notes: "Minted via POST /2/oauth2/token grant_type=client_credentials with HTTP Basic client auth."
1248
+ },
1249
+ {
1250
+ id: "oauth2-user",
1251
+ title: "OAuth 2.0 Authorization Code with PKCE",
1252
+ type: "oauth-authorization-code",
1253
+ status: "supported",
1254
+ notes: "Confidential clients authenticate with client_secret_basic (HTTP Basic header) only; X does not support client_secret_post, so a secret in the request body is rejected. Public clients send client_id in the body and rely on PKCE (S256). offline.access yields a refresh token."
1255
+ },
1256
+ {
1257
+ id: "oauth1-user",
1258
+ title: "Legacy OAuth 1.0a user context",
1259
+ type: "provider-specific",
1260
+ status: "unsupported",
1261
+ notes: "Accepted honestly as a partial surface; OAuth 1.0a request signing is not emulated."
1262
+ }
1263
+ ],
1264
+ specs: [
1265
+ {
1266
+ kind: "openapi",
1267
+ title: "X API v2 subset",
1268
+ coverage: "hand-authored",
1269
+ url: "/2/openapi.json",
1270
+ operations: [
1271
+ {
1272
+ operationId: "oauth2Token",
1273
+ method: "POST",
1274
+ path: "/2/oauth2/token",
1275
+ status: "hand-authored",
1276
+ summary: "authorization_code (PKCE), refresh_token, and client_credentials grants."
1277
+ },
1278
+ { operationId: "oauth2Revoke", method: "POST", path: "/2/oauth2/revoke", status: "hand-authored" },
1279
+ {
1280
+ operationId: "findMyUser",
1281
+ method: "GET",
1282
+ path: "/2/users/me",
1283
+ status: "hand-authored",
1284
+ summary: "User-context token with users.read."
1285
+ },
1286
+ { operationId: "findUserById", method: "GET", path: "/2/users/:id", status: "hand-authored" },
1287
+ {
1288
+ operationId: "findUserByUsername",
1289
+ method: "GET",
1290
+ path: "/2/users/by/username/:username",
1291
+ status: "hand-authored"
1292
+ },
1293
+ { operationId: "usersIdTweets", method: "GET", path: "/2/users/:id/tweets", status: "hand-authored" },
1294
+ { operationId: "findTweetById", method: "GET", path: "/2/tweets/:id", status: "hand-authored" },
1295
+ { operationId: "findTweetsById", method: "GET", path: "/2/tweets", status: "hand-authored" },
1296
+ {
1297
+ operationId: "createTweet",
1298
+ method: "POST",
1299
+ path: "/2/tweets",
1300
+ status: "hand-authored",
1301
+ summary: "User-context token with tweet.write."
1302
+ },
1303
+ { operationId: "deleteTweetById", method: "DELETE", path: "/2/tweets/:id", status: "hand-authored" },
1304
+ {
1305
+ operationId: "tweetsRecentSearch",
1306
+ method: "GET",
1307
+ path: "/2/tweets/search/recent",
1308
+ status: "unsupported"
1309
+ },
1310
+ { operationId: "usersIdFollow", method: "POST", path: "/2/users/:id/following", status: "unsupported" },
1311
+ { operationId: "usersIdLike", method: "POST", path: "/2/users/:id/likes", status: "unsupported" }
1312
+ ]
1313
+ },
1314
+ {
1315
+ kind: "oauth-metadata",
1316
+ title: "OAuth 2.0 client-auth behavior (confidential clients use client_secret_basic only)",
1317
+ coverage: "hand-authored"
1318
+ }
1319
+ ],
1320
+ scenarios: [
1321
+ {
1322
+ id: "default",
1323
+ title: "Single developer account",
1324
+ description: "One verified user with a seeded confidential and public OAuth client."
1325
+ }
1326
+ ],
1327
+ seedSchema: {
1328
+ description: "Seed users, OAuth 2.0 clients (confidential or public), and Tweets.",
1329
+ fields: [
1330
+ {
1331
+ key: "users",
1332
+ title: "Users",
1333
+ description: "X accounts addressable by username (@handle) and numeric id.",
1334
+ example: [{ username: "developer", name: "Developer", verified: true }]
1335
+ },
1336
+ {
1337
+ key: "oauth_clients",
1338
+ title: "OAuth 2.0 clients",
1339
+ description: "Confidential clients have a client_secret and use HTTP Basic auth; public clients omit it and use PKCE only.",
1340
+ example: [
1341
+ {
1342
+ client_id: "x-confidential-client",
1343
+ client_secret: "x-confidential-secret",
1344
+ client_type: "confidential",
1345
+ name: "My X App",
1346
+ redirect_uris: ["http://localhost:3000/api/auth/callback/twitter"]
1347
+ },
1348
+ { client_id: "x-public-client", client_type: "public", name: "My X SPA" }
1349
+ ]
1350
+ },
1351
+ {
1352
+ key: "tweets",
1353
+ title: "Tweets",
1354
+ description: "Tweets authored by a seeded user (referenced by username or id).",
1355
+ example: [{ text: "Hello from X.", author: "developer", like_count: 42 }]
1356
+ }
1357
+ ],
1358
+ example: {
1359
+ users: [{ username: "developer", name: "Developer", verified: true, followers_count: 1200 }],
1360
+ oauth_clients: [
1361
+ {
1362
+ client_id: "x-confidential-client",
1363
+ client_secret: "x-confidential-secret",
1364
+ client_type: "confidential",
1365
+ name: "My X App",
1366
+ redirect_uris: ["http://localhost:3000/api/auth/callback/twitter"]
1367
+ },
1368
+ { client_id: "x-public-client", client_type: "public", name: "My X SPA" }
1369
+ ],
1370
+ tweets: [{ text: "Hello from the X API v2 emulator.", author: "developer", like_count: 42, retweet_count: 7 }]
1371
+ }
1372
+ },
1373
+ stateModel: {
1374
+ description: "Entities seeded and mutated by X provider calls.",
1375
+ collections: [
1376
+ { name: "x.users", title: "Users" },
1377
+ { name: "x.tweets", title: "Tweets" },
1378
+ { name: "x.oauth_clients", title: "OAuth 2.0 clients" },
1379
+ { name: "x.auth_codes", title: "Authorization codes" },
1380
+ { name: "x.access_tokens", title: "Access tokens" },
1381
+ { name: "x.refresh_tokens", title: "Refresh tokens" }
1382
+ ]
1383
+ },
1384
+ connections: [
1385
+ {
1386
+ id: "twitter-api-v2",
1387
+ title: "twitter-api-v2 (TypeScript)",
1388
+ kind: "sdk",
1389
+ language: "typescript",
1390
+ description: "Point the twitter-api-v2 SDK at the emulator for both app-only and user-context auth.",
1391
+ template: 'import { TwitterApi } from "twitter-api-v2";\n\nconst base = "{{baseUrl}}";\n\n// App-only Bearer token (client_credentials).\nconst appClient = new TwitterApi("{{token}}", { baseUrl: base });\nconst user = await appClient.v2.userByUsername("developer");\n\n// OAuth 2.0 user-context client (Authorization Code with PKCE).\nconst oauthClient = new TwitterApi({ clientId: "{{clientId}}", clientSecret: "{{clientSecret}}" });\nconst { url, codeVerifier, state } = oauthClient.generateOAuth2AuthLink(\n "http://localhost:3000/api/auth/callback/twitter",\n { scope: ["tweet.read", "tweet.write", "users.read", "offline.access"] },\n);\n// Send the user to `url` (against `${base}/2/oauth2/authorize`), then exchange the code at `${base}/2/oauth2/token`.'
1392
+ },
1393
+ {
1394
+ id: "x-env",
1395
+ title: "X base URL and credentials (env)",
1396
+ kind: "env",
1397
+ language: "bash",
1398
+ description: "Point your app at the emulator instead of api.x.com.",
1399
+ template: "X_API_BASE_URL={{baseUrl}}\nX_CLIENT_ID={{clientId}}\nX_CLIENT_SECRET={{clientSecret}}\nX_BEARER_TOKEN={{token}}"
1400
+ },
1401
+ {
1402
+ id: "curl-app-only",
1403
+ title: "curl (app-only Bearer)",
1404
+ kind: "curl",
1405
+ language: "bash",
1406
+ description: "Mint an app-only Bearer token (confidential client, HTTP Basic auth) and read a user.",
1407
+ template: 'curl -s -X POST {{baseUrl}}/2/oauth2/token \\\n -u "{{clientId}}:{{clientSecret}}" \\\n -d grant_type=client_credentials\n\ncurl -s {{baseUrl}}/2/users/by/username/developer \\\n -H "authorization: Bearer {{token}}"'
1408
+ },
1409
+ {
1410
+ id: "curl-token-exchange",
1411
+ title: "curl (authorization code + PKCE)",
1412
+ kind: "curl",
1413
+ language: "bash",
1414
+ description: "Exchange an authorization code for a user-context token. Confidential clients use -u (Basic); public clients send client_id in the body with no secret.",
1415
+ template: 'curl -s -X POST {{baseUrl}}/2/oauth2/token \\\n -u "{{clientId}}:{{clientSecret}}" \\\n -d grant_type=authorization_code \\\n -d code=$CODE \\\n -d redirect_uri=http://localhost:3000/api/auth/callback/twitter \\\n -d code_verifier=$CODE_VERIFIER'
1416
+ }
1417
+ ]
1418
+ };
1419
+ function resolveAuthor(store, ref) {
1420
+ const xs = getXStore(store);
1421
+ const byId = xs.users.findOneBy("user_id", ref);
1422
+ if (byId) return byId.user_id;
1423
+ const byUsername = xs.users.findOneBy("username", ref.toLowerCase().replace(/^@/, ""));
1424
+ return byUsername ? byUsername.user_id : null;
1425
+ }
1426
+ function seedFromConfig(store, baseUrl, config) {
1427
+ const xs = getXStore(store);
1428
+ for (const u of config.users ?? []) {
1429
+ const username = u.username.toLowerCase().replace(/^@/, "");
1430
+ if (xs.users.findOneBy("username", username)) continue;
1431
+ const userId = u.user_id ?? xNumericId();
1432
+ xs.users.insert({
1433
+ user_id: userId,
1434
+ username,
1435
+ name: u.name ?? u.username,
1436
+ description: u.description ?? "",
1437
+ verified: u.verified ?? false,
1438
+ protected: u.protected ?? false,
1439
+ location: u.location ?? null,
1440
+ url: u.url ?? null,
1441
+ profile_image_url: u.profile_image_url ?? `${baseUrl}/profile_images/${username}.png`,
1442
+ followers_count: u.followers_count ?? 0,
1443
+ following_count: u.following_count ?? 0,
1444
+ tweet_count: 0,
1445
+ listed_count: u.listed_count ?? 0,
1446
+ created_at_x: u.created_at ?? (/* @__PURE__ */ new Date()).toISOString()
1447
+ });
1448
+ }
1449
+ for (const cl of config.oauth_clients ?? []) {
1450
+ if (xs.oauthClients.findOneBy("client_id", cl.client_id)) continue;
1451
+ const clientType = cl.client_type ?? (cl.client_secret ? "confidential" : "public");
1452
+ xs.oauthClients.insert({
1453
+ client_id: cl.client_id,
1454
+ client_secret: clientType === "confidential" ? cl.client_secret ?? null : null,
1455
+ client_type: clientType,
1456
+ name: cl.name ?? cl.client_id,
1457
+ redirect_uris: cl.redirect_uris ?? ["http://localhost:3000/callback"]
1458
+ });
1459
+ }
1460
+ for (const t of config.tweets ?? []) {
1461
+ const authorId = resolveAuthor(store, t.author);
1462
+ if (!authorId) continue;
1463
+ const tweetId = t.tweet_id ?? xNumericId();
1464
+ if (xs.tweets.findOneBy("tweet_id", tweetId)) continue;
1465
+ xs.tweets.insert({
1466
+ tweet_id: tweetId,
1467
+ author_id: authorId,
1468
+ text: t.text,
1469
+ reply_count: t.reply_count ?? 0,
1470
+ retweet_count: t.retweet_count ?? 0,
1471
+ like_count: t.like_count ?? 0,
1472
+ quote_count: t.quote_count ?? 0,
1473
+ impression_count: t.impression_count ?? 0,
1474
+ in_reply_to_user_id: null,
1475
+ conversation_id: tweetId,
1476
+ lang: t.lang ?? "en",
1477
+ possibly_sensitive: false,
1478
+ created_at_x: t.created_at ?? (/* @__PURE__ */ new Date()).toISOString()
1479
+ });
1480
+ const author = xs.users.findOneBy("user_id", authorId);
1481
+ if (author) xs.users.update(author.id, { tweet_count: author.tweet_count + 1 });
1482
+ }
1483
+ }
1484
+ var xPlugin = {
1485
+ name: "x",
1486
+ register(app, store, webhooks, baseUrl, tokenMap) {
1487
+ const ctx = { app, store, webhooks, baseUrl, tokenMap };
1488
+ oauthRoutes(ctx);
1489
+ usersRoutes(ctx);
1490
+ tweetsRoutes(ctx);
1491
+ openapiRoutes(ctx);
1492
+ },
1493
+ seed(store, baseUrl) {
1494
+ seedFromConfig(store, baseUrl, {
1495
+ users: [
1496
+ {
1497
+ username: "developer",
1498
+ name: "Developer",
1499
+ description: "Building with the X API v2 emulator.",
1500
+ verified: true,
1501
+ followers_count: 1200,
1502
+ following_count: 320
1503
+ }
1504
+ ],
1505
+ oauth_clients: [
1506
+ {
1507
+ client_id: "x-confidential-client",
1508
+ client_secret: "x-confidential-secret",
1509
+ client_type: "confidential",
1510
+ name: "My X App (confidential)",
1511
+ redirect_uris: ["http://localhost:3000/api/auth/callback/twitter"]
1512
+ },
1513
+ {
1514
+ client_id: "x-public-client",
1515
+ client_type: "public",
1516
+ name: "My X App (public)",
1517
+ redirect_uris: ["http://localhost:3000/api/auth/callback/twitter"]
1518
+ }
1519
+ ],
1520
+ tweets: [{ text: "Hello from the X API v2 emulator.", author: "developer", like_count: 42, retweet_count: 7 }]
1521
+ });
1522
+ }
1523
+ };
1524
+ var index_default = xPlugin;
1525
+ export {
1526
+ index_default as default,
1527
+ getXStore,
1528
+ lookupAccessToken,
1529
+ manifest,
1530
+ seedFromConfig,
1531
+ xPlugin
1532
+ };
1533
+ //# sourceMappingURL=dist-OYYGWKZQ.js.map