@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.
- package/README.md +1044 -0
- package/dist/api.d.ts +24 -0
- package/dist/api.js +2665 -0
- package/dist/api.js.map +1 -0
- package/dist/chunk-D6EKRYGP.js +1615 -0
- package/dist/chunk-D6EKRYGP.js.map +1 -0
- package/dist/chunk-WVQMFHQM.js +83 -0
- package/dist/chunk-WVQMFHQM.js.map +1 -0
- package/dist/dist-7FDUSG5I.js +24368 -0
- package/dist/dist-7FDUSG5I.js.map +1 -0
- package/dist/dist-7N4COJHK.js +1814 -0
- package/dist/dist-7N4COJHK.js.map +1 -0
- package/dist/dist-BTEY33DJ.js +2334 -0
- package/dist/dist-BTEY33DJ.js.map +1 -0
- package/dist/dist-DK26ESP2.js +595 -0
- package/dist/dist-DK26ESP2.js.map +1 -0
- package/dist/dist-IYZPDKJW.js +1284 -0
- package/dist/dist-IYZPDKJW.js.map +1 -0
- package/dist/dist-JJ2ZRCAX.js +189 -0
- package/dist/dist-JJ2ZRCAX.js.map +1 -0
- package/dist/dist-K4CVTD6K.js +1570 -0
- package/dist/dist-K4CVTD6K.js.map +1 -0
- package/dist/dist-M3GVASMR.js +1254 -0
- package/dist/dist-M3GVASMR.js.map +1 -0
- package/dist/dist-OYYGWKZQ.js +1533 -0
- package/dist/dist-OYYGWKZQ.js.map +1 -0
- package/dist/dist-P3SBBRFR.js +3169 -0
- package/dist/dist-P3SBBRFR.js.map +1 -0
- package/dist/dist-RMPDKZUA.js +1183 -0
- package/dist/dist-RMPDKZUA.js.map +1 -0
- package/dist/dist-WBKONLOE.js +2154 -0
- package/dist/dist-WBKONLOE.js.map +1 -0
- package/dist/dist-XM5HSBDC.js +1090 -0
- package/dist/dist-XM5HSBDC.js.map +1 -0
- package/dist/dist-XVVIYXQG.js +4241 -0
- package/dist/dist-XVVIYXQG.js.map +1 -0
- package/dist/dist-YPRJYQHW.js +5109 -0
- package/dist/dist-YPRJYQHW.js.map +1 -0
- package/dist/dist-ZEC77OKZ.js +913 -0
- package/dist/dist-ZEC77OKZ.js.map +1 -0
- package/dist/fonts/GeistPixel-Square.woff2 +0 -0
- package/dist/fonts/favicon.ico +0 -0
- package/dist/fonts/geist-sans.woff2 +0 -0
- package/dist/helpers-LXLP3DFE-LBOTATT5.js +17 -0
- package/dist/helpers-LXLP3DFE-LBOTATT5.js.map +1 -0
- package/dist/index.js +3005 -0
- package/dist/index.js.map +1 -0
- package/package.json +83 -0
|
@@ -0,0 +1,1284 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SignJWT,
|
|
3
|
+
exportJWK,
|
|
4
|
+
generateKeyPair
|
|
5
|
+
} from "./chunk-D6EKRYGP.js";
|
|
6
|
+
|
|
7
|
+
// ../@emulators/workos/dist/index.js
|
|
8
|
+
function getWorkosStore(store) {
|
|
9
|
+
return {
|
|
10
|
+
users: store.collection("workos.users", ["workos_id", "email"]),
|
|
11
|
+
organizations: store.collection("workos.organizations", ["workos_id"]),
|
|
12
|
+
memberships: store.collection("workos.memberships", [
|
|
13
|
+
"workos_id",
|
|
14
|
+
"user_id",
|
|
15
|
+
"organization_id"
|
|
16
|
+
]),
|
|
17
|
+
invitations: store.collection("workos.invitations", [
|
|
18
|
+
"workos_id",
|
|
19
|
+
"email",
|
|
20
|
+
"organization_id",
|
|
21
|
+
"token"
|
|
22
|
+
]),
|
|
23
|
+
apiKeys: store.collection("workos.api_keys", ["workos_id", "value", "user_id"]),
|
|
24
|
+
authCodes: store.collection("workos.auth_codes", ["code"]),
|
|
25
|
+
sessions: store.collection("workos.sessions", ["refresh_token", "workos_id"]),
|
|
26
|
+
vaultObjects: store.collection("workos.vault_objects", [
|
|
27
|
+
"workos_id",
|
|
28
|
+
"name"
|
|
29
|
+
]),
|
|
30
|
+
oauthClients: store.collection("workos.oauth_clients", ["client_id"]),
|
|
31
|
+
oauthCodes: store.collection("workos.oauth_codes", ["code"])
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function createErrorHandler(documentationUrl) {
|
|
35
|
+
return async (c, next) => {
|
|
36
|
+
if (documentationUrl) {
|
|
37
|
+
c.set("docsUrl", documentationUrl);
|
|
38
|
+
}
|
|
39
|
+
await next();
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
var errorHandler = createErrorHandler();
|
|
43
|
+
var isDebug = typeof process !== "undefined" && (process.env.DEBUG === "1" || process.env.DEBUG === "true" || process.env.EMULATE_DEBUG === "1");
|
|
44
|
+
function escapeHtml(s) {
|
|
45
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
46
|
+
}
|
|
47
|
+
function escapeAttr(s) {
|
|
48
|
+
return escapeHtml(s).replace(/'/g, "'");
|
|
49
|
+
}
|
|
50
|
+
var CSS = `
|
|
51
|
+
@font-face{
|
|
52
|
+
font-family:'Geist';font-style:normal;font-weight:100 900;font-display:swap;
|
|
53
|
+
src:url('/_emulate/fonts/geist-sans.woff2') format('woff2');
|
|
54
|
+
}
|
|
55
|
+
@font-face{
|
|
56
|
+
font-family:'Geist Pixel';font-style:normal;font-weight:400;font-display:swap;
|
|
57
|
+
src:url('/_emulate/fonts/GeistPixel-Square.woff2') format('woff2');
|
|
58
|
+
}
|
|
59
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
60
|
+
body{
|
|
61
|
+
font-family:'Geist',-apple-system,BlinkMacSystemFont,sans-serif;
|
|
62
|
+
background:#000;color:#33ff00;min-height:100vh;
|
|
63
|
+
-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;
|
|
64
|
+
}
|
|
65
|
+
.emu-bar{
|
|
66
|
+
border-bottom:1px solid #0a3300;padding:10px 20px;
|
|
67
|
+
display:flex;align-items:center;gap:10px;font-size:.8125rem;color:#1a8c00;
|
|
68
|
+
}
|
|
69
|
+
.emu-bar-title{font-weight:600;color:#33ff00;font-family:'Geist Pixel',monospace;}
|
|
70
|
+
.emu-bar-links{margin-left:auto;display:flex;gap:16px;}
|
|
71
|
+
.emu-bar-links a{
|
|
72
|
+
color:#1a8c00;font-size:.75rem;text-decoration:none;transition:color .15s;
|
|
73
|
+
}
|
|
74
|
+
.emu-bar-links a:hover{color:#33ff00;}
|
|
75
|
+
.emu-bar-links a .full{display:inline;}
|
|
76
|
+
.emu-bar-links a .short{display:none;}
|
|
77
|
+
@media(max-width:600px){
|
|
78
|
+
.emu-bar-links a .full{display:none;}
|
|
79
|
+
.emu-bar-links a .short{display:inline;}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.content{
|
|
83
|
+
display:flex;align-items:center;justify-content:center;
|
|
84
|
+
min-height:calc(100vh - 42px);padding:24px 16px;
|
|
85
|
+
}
|
|
86
|
+
.content-inner{width:100%;max-width:420px;}
|
|
87
|
+
.card-title{
|
|
88
|
+
font-family:'Geist Pixel',monospace;
|
|
89
|
+
font-size:1.125rem;font-weight:600;margin-bottom:4px;color:#33ff00;
|
|
90
|
+
}
|
|
91
|
+
.card-subtitle{color:#1a8c00;font-size:.8125rem;margin-bottom:18px;line-height:1.45;}
|
|
92
|
+
.powered-by{
|
|
93
|
+
position:fixed;bottom:0;left:0;right:0;
|
|
94
|
+
text-align:center;padding:12px;font-size:.6875rem;color:#0a3300;
|
|
95
|
+
font-family:'Geist Pixel',monospace;
|
|
96
|
+
}
|
|
97
|
+
.powered-by a{color:#1a8c00;text-decoration:none;transition:color .15s;}
|
|
98
|
+
.powered-by a:hover{color:#33ff00;}
|
|
99
|
+
|
|
100
|
+
.error-title{
|
|
101
|
+
font-family:'Geist Pixel',monospace;
|
|
102
|
+
color:#ff4444;font-size:1.125rem;font-weight:600;margin-bottom:8px;
|
|
103
|
+
}
|
|
104
|
+
.error-msg{color:#1a8c00;font-size:.875rem;line-height:1.5;}
|
|
105
|
+
.error-card{text-align:center;}
|
|
106
|
+
|
|
107
|
+
.user-form{margin-bottom:8px;}
|
|
108
|
+
.user-form:last-of-type{margin-bottom:0;}
|
|
109
|
+
.user-btn{
|
|
110
|
+
width:100%;display:flex;align-items:center;gap:12px;
|
|
111
|
+
padding:10px 12px;border:1px solid #0a3300;border-radius:8px;
|
|
112
|
+
background:#000;color:inherit;cursor:pointer;text-align:left;
|
|
113
|
+
font:inherit;transition:border-color .15s;
|
|
114
|
+
}
|
|
115
|
+
.user-btn:hover{border-color:#33ff00;}
|
|
116
|
+
.avatar{
|
|
117
|
+
width:36px;height:36px;border-radius:50%;
|
|
118
|
+
background:#0a3300;color:#33ff00;font-weight:600;font-size:.875rem;
|
|
119
|
+
display:flex;align-items:center;justify-content:center;flex-shrink:0;
|
|
120
|
+
font-family:'Geist Pixel',monospace;
|
|
121
|
+
}
|
|
122
|
+
.user-text{min-width:0;}
|
|
123
|
+
.user-login{font-weight:600;font-size:.875rem;display:block;color:#33ff00;}
|
|
124
|
+
.user-meta{color:#1a8c00;font-size:.75rem;margin-top:1px;}
|
|
125
|
+
.user-email{font-size:.6875rem;color:#116600;word-break:break-all;margin-top:1px;}
|
|
126
|
+
|
|
127
|
+
.settings-layout{
|
|
128
|
+
max-width:920px;margin:0 auto;padding:28px 20px;
|
|
129
|
+
display:flex;gap:28px;
|
|
130
|
+
}
|
|
131
|
+
.settings-sidebar{width:200px;flex-shrink:0;}
|
|
132
|
+
.settings-sidebar a{
|
|
133
|
+
display:block;padding:6px 10px;border-radius:6px;color:#1a8c00;
|
|
134
|
+
text-decoration:none;font-size:.8125rem;transition:color .15s;
|
|
135
|
+
}
|
|
136
|
+
.settings-sidebar a:hover{color:#33ff00;}
|
|
137
|
+
.settings-sidebar a.active{color:#33ff00;font-weight:600;}
|
|
138
|
+
.settings-main{flex:1;min-width:0;}
|
|
139
|
+
|
|
140
|
+
.s-card{
|
|
141
|
+
padding:18px 0;margin-bottom:14px;border-bottom:1px solid #0a3300;
|
|
142
|
+
}
|
|
143
|
+
.s-card:last-child{border-bottom:none;}
|
|
144
|
+
.s-card-header{display:flex;align-items:center;gap:14px;margin-bottom:14px;}
|
|
145
|
+
.s-icon{
|
|
146
|
+
width:42px;height:42px;border-radius:8px;
|
|
147
|
+
background:#0a3300;display:flex;align-items:center;justify-content:center;
|
|
148
|
+
font-size:1.125rem;font-weight:700;color:#116600;flex-shrink:0;
|
|
149
|
+
font-family:'Geist Pixel',monospace;
|
|
150
|
+
}
|
|
151
|
+
.s-title{
|
|
152
|
+
font-family:'Geist Pixel',monospace;
|
|
153
|
+
font-size:1.25rem;font-weight:600;color:#33ff00;
|
|
154
|
+
}
|
|
155
|
+
.s-subtitle{font-size:.75rem;color:#1a8c00;margin-top:2px;}
|
|
156
|
+
.section-heading{
|
|
157
|
+
font-size:.9375rem;font-weight:600;margin-bottom:10px;color:#33ff00;
|
|
158
|
+
display:flex;align-items:center;justify-content:space-between;
|
|
159
|
+
}
|
|
160
|
+
.perm-list{list-style:none;}
|
|
161
|
+
.perm-list li{padding:5px 0;font-size:.8125rem;display:flex;align-items:center;gap:6px;color:#1a8c00;}
|
|
162
|
+
.check{color:#33ff00;}
|
|
163
|
+
.org-row{
|
|
164
|
+
display:flex;align-items:center;gap:8px;padding:7px 0;
|
|
165
|
+
border-bottom:1px solid #0a3300;font-size:.8125rem;
|
|
166
|
+
}
|
|
167
|
+
.org-row:last-child{border-bottom:none;}
|
|
168
|
+
.org-icon{
|
|
169
|
+
width:22px;height:22px;border-radius:4px;background:#0a3300;
|
|
170
|
+
display:flex;align-items:center;justify-content:center;
|
|
171
|
+
font-size:.625rem;font-weight:700;color:#116600;flex-shrink:0;
|
|
172
|
+
font-family:'Geist Pixel',monospace;
|
|
173
|
+
}
|
|
174
|
+
.org-name{font-weight:600;color:#33ff00;}
|
|
175
|
+
.badge{font-size:.6875rem;padding:1px 7px;border-radius:999px;font-weight:500;}
|
|
176
|
+
.badge-granted{background:#0a3300;color:#33ff00;}
|
|
177
|
+
.badge-denied{background:#1a0a0a;color:#ff4444;}
|
|
178
|
+
.badge-requested{background:#0a3300;color:#1a8c00;}
|
|
179
|
+
.btn-revoke{
|
|
180
|
+
display:inline-block;padding:5px 14px;border-radius:6px;
|
|
181
|
+
border:1px solid #0a3300;background:transparent;color:#ff4444;
|
|
182
|
+
font-size:.75rem;font-weight:600;cursor:pointer;transition:border-color .15s;
|
|
183
|
+
}
|
|
184
|
+
.btn-revoke:hover{border-color:#ff4444;}
|
|
185
|
+
.info-text{color:#1a8c00;font-size:.75rem;line-height:1.5;margin-top:10px;}
|
|
186
|
+
.info-text a,.section-heading a{color:#1a8c00;text-decoration:none;transition:color .15s;}
|
|
187
|
+
.info-text a:hover,.section-heading a:hover{color:#33ff00;}
|
|
188
|
+
code{font-family:'Geist Mono','SF Mono',ui-monospace,monospace;font-size:.8125rem;color:#33ff00;word-break:break-all;}
|
|
189
|
+
.code-block{
|
|
190
|
+
background:#020;border:1px solid #0a3300;border-radius:6px;padding:10px 12px;
|
|
191
|
+
margin:8px 0 12px;overflow-x:auto;
|
|
192
|
+
}
|
|
193
|
+
.code-block code{white-space:pre;word-break:normal;display:block;line-height:1.5;}
|
|
194
|
+
.app-link{
|
|
195
|
+
display:flex;align-items:center;gap:12px;padding:12px;
|
|
196
|
+
border:1px solid #0a3300;border-radius:8px;background:#000;
|
|
197
|
+
text-decoration:none;color:inherit;margin-bottom:8px;transition:border-color .15s;
|
|
198
|
+
}
|
|
199
|
+
.app-link:hover{border-color:#33ff00;}
|
|
200
|
+
.app-link-name{font-weight:600;font-size:.875rem;color:#33ff00;}
|
|
201
|
+
.app-link-scopes{font-size:.6875rem;color:#1a8c00;margin-top:1px;}
|
|
202
|
+
.empty{color:#1a8c00;text-align:center;padding:28px 0;font-size:.875rem;}
|
|
203
|
+
|
|
204
|
+
.inspector-layout{max-width:960px;margin:0 auto;padding:28px 20px;}
|
|
205
|
+
.inspector-tabs{display:flex;gap:4px;margin-bottom:20px;}
|
|
206
|
+
.inspector-tabs a{
|
|
207
|
+
padding:7px 16px;border-radius:6px;text-decoration:none;
|
|
208
|
+
font-size:.8125rem;color:#1a8c00;border:1px solid transparent;
|
|
209
|
+
transition:color .15s,border-color .15s;
|
|
210
|
+
}
|
|
211
|
+
.inspector-tabs a:hover{color:#33ff00;}
|
|
212
|
+
.inspector-tabs a.active{color:#33ff00;font-weight:600;border-color:#0a3300;background:#0a3300;}
|
|
213
|
+
.inspector-section{margin-bottom:24px;}
|
|
214
|
+
.inspector-section h2{
|
|
215
|
+
font-family:'Geist Pixel',monospace;
|
|
216
|
+
font-size:1rem;font-weight:600;color:#33ff00;margin-bottom:10px;
|
|
217
|
+
}
|
|
218
|
+
.inspector-section h3{
|
|
219
|
+
font-family:'Geist Pixel',monospace;
|
|
220
|
+
font-size:.875rem;font-weight:600;color:#1a8c00;margin:16px 0 8px;
|
|
221
|
+
}
|
|
222
|
+
.inspector-table{width:100%;border-collapse:collapse;margin-bottom:12px;}
|
|
223
|
+
.inspector-table th,.inspector-table td{
|
|
224
|
+
text-align:left;padding:8px 12px;border-bottom:1px solid #0a3300;
|
|
225
|
+
font-size:.8125rem;
|
|
226
|
+
}
|
|
227
|
+
.inspector-table th{color:#1a8c00;font-weight:600;font-size:.75rem;text-transform:uppercase;letter-spacing:.04em;}
|
|
228
|
+
.inspector-table td{color:#33ff00;}
|
|
229
|
+
.inspector-table tbody tr{transition:background .1s;}
|
|
230
|
+
.inspector-table tbody tr:hover{background:#0a3300;}
|
|
231
|
+
.inspector-empty{color:#1a8c00;text-align:center;padding:20px 0;font-size:.8125rem;}
|
|
232
|
+
|
|
233
|
+
.checkout-layout{
|
|
234
|
+
display:flex;min-height:calc(100vh - 42px);
|
|
235
|
+
}
|
|
236
|
+
.checkout-summary{
|
|
237
|
+
flex:1;background:#020;padding:48px 40px 48px 10%;
|
|
238
|
+
display:flex;flex-direction:column;justify-content:center;
|
|
239
|
+
border-right:1px solid #0a3300;
|
|
240
|
+
}
|
|
241
|
+
.checkout-form-side{
|
|
242
|
+
flex:1;background:#000;padding:48px 10% 48px 40px;
|
|
243
|
+
display:flex;flex-direction:column;justify-content:center;
|
|
244
|
+
}
|
|
245
|
+
.checkout-merchant{
|
|
246
|
+
display:flex;align-items:center;gap:10px;margin-bottom:6px;
|
|
247
|
+
}
|
|
248
|
+
.checkout-merchant-name{
|
|
249
|
+
font-family:'Geist Pixel',monospace;
|
|
250
|
+
font-size:.9375rem;font-weight:600;color:#33ff00;
|
|
251
|
+
}
|
|
252
|
+
.checkout-test-badge{
|
|
253
|
+
font-size:.625rem;font-weight:700;letter-spacing:.04em;text-transform:uppercase;
|
|
254
|
+
background:#0a3300;color:#1a8c00;padding:2px 8px;border-radius:4px;
|
|
255
|
+
}
|
|
256
|
+
.checkout-total{
|
|
257
|
+
font-family:'Geist Pixel',monospace;
|
|
258
|
+
font-size:2rem;font-weight:700;color:#33ff00;margin:8px 0 28px;
|
|
259
|
+
}
|
|
260
|
+
.checkout-line-item{
|
|
261
|
+
display:flex;align-items:center;gap:14px;padding:14px 0;
|
|
262
|
+
border-bottom:1px solid #0a3300;
|
|
263
|
+
}
|
|
264
|
+
.checkout-line-item:first-child{border-top:1px solid #0a3300;}
|
|
265
|
+
.checkout-item-icon{
|
|
266
|
+
width:42px;height:42px;border-radius:6px;background:#0a3300;
|
|
267
|
+
display:flex;align-items:center;justify-content:center;flex-shrink:0;
|
|
268
|
+
font-family:'Geist Pixel',monospace;font-size:.875rem;font-weight:700;color:#116600;
|
|
269
|
+
}
|
|
270
|
+
.checkout-item-details{flex:1;min-width:0;}
|
|
271
|
+
.checkout-item-name{font-size:.875rem;font-weight:600;color:#33ff00;}
|
|
272
|
+
.checkout-item-qty{font-size:.75rem;color:#1a8c00;margin-top:2px;}
|
|
273
|
+
.checkout-item-price{
|
|
274
|
+
font-size:.875rem;font-weight:600;color:#33ff00;text-align:right;white-space:nowrap;
|
|
275
|
+
}
|
|
276
|
+
.checkout-item-unit{font-size:.6875rem;color:#1a8c00;text-align:right;margin-top:2px;}
|
|
277
|
+
.checkout-totals{margin-top:20px;}
|
|
278
|
+
.checkout-totals-row{
|
|
279
|
+
display:flex;justify-content:space-between;padding:6px 0;
|
|
280
|
+
font-size:.8125rem;color:#1a8c00;
|
|
281
|
+
}
|
|
282
|
+
.checkout-totals-row.total{
|
|
283
|
+
border-top:1px solid #0a3300;margin-top:8px;padding-top:14px;
|
|
284
|
+
font-size:.9375rem;font-weight:600;color:#33ff00;
|
|
285
|
+
}
|
|
286
|
+
.checkout-form-section{margin-bottom:24px;}
|
|
287
|
+
.checkout-form-label{
|
|
288
|
+
font-size:.8125rem;font-weight:600;color:#33ff00;margin-bottom:8px;display:block;
|
|
289
|
+
}
|
|
290
|
+
.checkout-input{
|
|
291
|
+
width:100%;padding:10px 12px;border:1px solid #0a3300;border-radius:6px;
|
|
292
|
+
background:#020;color:#33ff00;font:inherit;font-size:.875rem;
|
|
293
|
+
transition:border-color .15s;outline:none;
|
|
294
|
+
}
|
|
295
|
+
.checkout-input:focus{border-color:#33ff00;}
|
|
296
|
+
.checkout-input::placeholder{color:#116600;}
|
|
297
|
+
.checkout-card-box{
|
|
298
|
+
border:1px solid #0a3300;border-radius:6px;padding:14px;
|
|
299
|
+
background:#020;
|
|
300
|
+
}
|
|
301
|
+
.checkout-card-row{
|
|
302
|
+
display:flex;gap:12px;margin-top:10px;
|
|
303
|
+
}
|
|
304
|
+
.checkout-card-row .checkout-input{flex:1;}
|
|
305
|
+
.checkout-sim-note{
|
|
306
|
+
font-size:.6875rem;color:#1a8c00;margin-top:10px;text-align:center;
|
|
307
|
+
font-style:italic;
|
|
308
|
+
}
|
|
309
|
+
.checkout-pay-btn{
|
|
310
|
+
width:100%;padding:14px;border:none;border-radius:8px;
|
|
311
|
+
background:#33ff00;color:#000;font:inherit;font-size:.9375rem;font-weight:700;
|
|
312
|
+
cursor:pointer;transition:background .15s;
|
|
313
|
+
font-family:'Geist Pixel',monospace;
|
|
314
|
+
}
|
|
315
|
+
.checkout-pay-btn:hover{background:#44ff22;}
|
|
316
|
+
.checkout-cancel{
|
|
317
|
+
text-align:center;margin-top:14px;
|
|
318
|
+
}
|
|
319
|
+
.checkout-cancel a{
|
|
320
|
+
color:#1a8c00;text-decoration:none;font-size:.8125rem;
|
|
321
|
+
transition:color .15s;
|
|
322
|
+
}
|
|
323
|
+
.checkout-cancel a:hover{color:#33ff00;}
|
|
324
|
+
@media(max-width:768px){
|
|
325
|
+
.checkout-layout{flex-direction:column;}
|
|
326
|
+
.checkout-summary{padding:32px 20px;border-right:none;border-bottom:1px solid #0a3300;}
|
|
327
|
+
.checkout-form-side{padding:32px 20px;}
|
|
328
|
+
}
|
|
329
|
+
`;
|
|
330
|
+
var POWERED_BY = `<div class="powered-by">Powered by <a href="https://emulate.dev" target="_blank" rel="noopener">emulate</a></div>`;
|
|
331
|
+
function emuBar(service) {
|
|
332
|
+
const title = service ? `${escapeHtml(service)} Emulator` : "Emulator";
|
|
333
|
+
return `<div class="emu-bar">
|
|
334
|
+
<span class="emu-bar-title">${title}</span>
|
|
335
|
+
<nav class="emu-bar-links">
|
|
336
|
+
<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>
|
|
337
|
+
<a href="https://github.com/vercel-labs/emulate" target="_blank" rel="noopener"><span class="full">Source Code</span><span class="short">Source</span></a>
|
|
338
|
+
<a href="https://emulate.dev" target="_blank" rel="noopener"><span class="full">Learn More</span><span class="short">Learn</span></a>
|
|
339
|
+
</nav>
|
|
340
|
+
</div>`;
|
|
341
|
+
}
|
|
342
|
+
function head(title) {
|
|
343
|
+
return `<!DOCTYPE html>
|
|
344
|
+
<html lang="en">
|
|
345
|
+
<head>
|
|
346
|
+
<meta charset="utf-8"/>
|
|
347
|
+
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
|
348
|
+
<link rel="icon" href="/_emulate/favicon.ico"/>
|
|
349
|
+
<title>${escapeHtml(title)} | emulate</title>
|
|
350
|
+
<style>${CSS}</style>
|
|
351
|
+
</head>`;
|
|
352
|
+
}
|
|
353
|
+
function renderCardPage(title, subtitle, body, service) {
|
|
354
|
+
return `${head(title)}
|
|
355
|
+
<body>
|
|
356
|
+
${emuBar(service)}
|
|
357
|
+
<div class="content">
|
|
358
|
+
<div class="content-inner">
|
|
359
|
+
<div class="card-title">${escapeHtml(title)}</div>
|
|
360
|
+
<div class="card-subtitle">${subtitle}</div>
|
|
361
|
+
${body}
|
|
362
|
+
</div>
|
|
363
|
+
</div>
|
|
364
|
+
${POWERED_BY}
|
|
365
|
+
</body></html>`;
|
|
366
|
+
}
|
|
367
|
+
function renderUserButton(opts) {
|
|
368
|
+
const hiddens = Object.entries(opts.hiddenFields).map(([k, v]) => `<input type="hidden" name="${escapeAttr(k)}" value="${escapeAttr(v)}"/>`).join("");
|
|
369
|
+
const nameLine = opts.name ? `<div class="user-meta">${escapeHtml(opts.name)}</div>` : "";
|
|
370
|
+
const emailLine = opts.email ? `<div class="user-email">${escapeHtml(opts.email)}</div>` : "";
|
|
371
|
+
return `<form class="user-form" method="post" action="${escapeAttr(opts.formAction)}">
|
|
372
|
+
${hiddens}
|
|
373
|
+
<button type="submit" class="user-btn">
|
|
374
|
+
<span class="avatar">${escapeHtml(opts.letter)}</span>
|
|
375
|
+
<span class="user-text">
|
|
376
|
+
<span class="user-login">${escapeHtml(opts.login)}</span>
|
|
377
|
+
${nameLine}${emailLine}
|
|
378
|
+
</span>
|
|
379
|
+
</button>
|
|
380
|
+
</form>`;
|
|
381
|
+
}
|
|
382
|
+
var keyPairPromise = generateKeyPair("RS256", { extractable: true });
|
|
383
|
+
var KID = "emulate-workos-1";
|
|
384
|
+
async function jwksResponse() {
|
|
385
|
+
const { publicKey } = await keyPairPromise;
|
|
386
|
+
const jwk = await exportJWK(publicKey);
|
|
387
|
+
return { keys: [{ ...jwk, kid: KID, use: "sig", alg: "RS256" }] };
|
|
388
|
+
}
|
|
389
|
+
async function signAccessToken(claims, options) {
|
|
390
|
+
const { privateKey } = await keyPairPromise;
|
|
391
|
+
let jwt = new SignJWT(claims).setProtectedHeader({ alg: "RS256", kid: KID, typ: "JWT" }).setIssuer(options.issuer).setIssuedAt().setJti(`jti_${Math.random().toString(36).slice(2)}`).setExpirationTime(options.expiresIn ?? "1h");
|
|
392
|
+
if (options.audience) jwt = jwt.setAudience(options.audience);
|
|
393
|
+
return jwt.sign(privateKey);
|
|
394
|
+
}
|
|
395
|
+
var counter = 0;
|
|
396
|
+
function workosId(prefix) {
|
|
397
|
+
counter += 1;
|
|
398
|
+
return `${prefix}_${String(counter).padStart(4, "0")}${Math.random().toString(36).slice(2, 8).toUpperCase()}`;
|
|
399
|
+
}
|
|
400
|
+
function randomToken(prefix) {
|
|
401
|
+
return `${prefix}_${Math.random().toString(36).slice(2)}${Math.random().toString(36).slice(2)}`;
|
|
402
|
+
}
|
|
403
|
+
function workosError(c, status, code, message) {
|
|
404
|
+
return c.json({ code, message, error: code, error_description: message }, status);
|
|
405
|
+
}
|
|
406
|
+
function listEnvelope(data) {
|
|
407
|
+
return {
|
|
408
|
+
object: "list",
|
|
409
|
+
data,
|
|
410
|
+
list_metadata: { before: null, after: null },
|
|
411
|
+
// Some SDK paths read the camelCase variant on raw responses.
|
|
412
|
+
listMetadata: { before: null, after: null }
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
function serializeUser(user) {
|
|
416
|
+
return {
|
|
417
|
+
object: "user",
|
|
418
|
+
id: user.workos_id,
|
|
419
|
+
email: user.email,
|
|
420
|
+
email_verified: user.email_verified,
|
|
421
|
+
first_name: user.first_name,
|
|
422
|
+
last_name: user.last_name,
|
|
423
|
+
profile_picture_url: user.profile_picture_url,
|
|
424
|
+
last_sign_in_at: user.updated_at,
|
|
425
|
+
external_id: null,
|
|
426
|
+
metadata: {},
|
|
427
|
+
created_at: user.created_at,
|
|
428
|
+
updated_at: user.updated_at
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
function serializeOrganization(org) {
|
|
432
|
+
return {
|
|
433
|
+
object: "organization",
|
|
434
|
+
id: org.workos_id,
|
|
435
|
+
name: org.name,
|
|
436
|
+
allow_profiles_outside_organization: false,
|
|
437
|
+
domains: [],
|
|
438
|
+
stripe_customer_id: null,
|
|
439
|
+
external_id: org.external_id,
|
|
440
|
+
metadata: {},
|
|
441
|
+
created_at: org.created_at,
|
|
442
|
+
updated_at: org.updated_at
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
function serializeMembership(membership, organizationName) {
|
|
446
|
+
return {
|
|
447
|
+
object: "organization_membership",
|
|
448
|
+
id: membership.workos_id,
|
|
449
|
+
user_id: membership.user_id,
|
|
450
|
+
organization_id: membership.organization_id,
|
|
451
|
+
organization_name: organizationName,
|
|
452
|
+
status: membership.status,
|
|
453
|
+
role: { slug: membership.role_slug },
|
|
454
|
+
created_at: membership.created_at,
|
|
455
|
+
updated_at: membership.updated_at
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
function serializeInvitation(invitation) {
|
|
459
|
+
return {
|
|
460
|
+
object: "invitation",
|
|
461
|
+
id: invitation.workos_id,
|
|
462
|
+
email: invitation.email,
|
|
463
|
+
state: invitation.state,
|
|
464
|
+
organization_id: invitation.organization_id,
|
|
465
|
+
inviter_user_id: invitation.inviter_user_id,
|
|
466
|
+
accepted_user_id: null,
|
|
467
|
+
token: invitation.token,
|
|
468
|
+
accept_invitation_url: `https://example.invalid/invite/${invitation.token}`,
|
|
469
|
+
expires_at: invitation.expires_at,
|
|
470
|
+
created_at: invitation.created_at,
|
|
471
|
+
updated_at: invitation.updated_at
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
function serializeApiKey(key, options = {}) {
|
|
475
|
+
return {
|
|
476
|
+
object: "api_key",
|
|
477
|
+
id: key.workos_id,
|
|
478
|
+
name: key.name,
|
|
479
|
+
obfuscated_value: `${key.value.slice(0, 7)}\u2026${key.value.slice(-4)}`,
|
|
480
|
+
owner: { type: "user", id: key.user_id, organization_id: key.organization_id },
|
|
481
|
+
last_used_at: key.last_used_at,
|
|
482
|
+
created_at: key.created_at,
|
|
483
|
+
updated_at: key.updated_at,
|
|
484
|
+
...options.includeValue ? { value: key.value } : {}
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
function serializeVaultMetadata(object) {
|
|
488
|
+
return {
|
|
489
|
+
id: object.workos_id,
|
|
490
|
+
context: object.key_context,
|
|
491
|
+
environment_id: "environment_emulate",
|
|
492
|
+
key_id: "key_emulate",
|
|
493
|
+
updated_at: object.updated_at,
|
|
494
|
+
updated_by: { id: "key_emulate", name: "emulate" },
|
|
495
|
+
version_id: object.version_id
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
function serializeVaultObject(object) {
|
|
499
|
+
return {
|
|
500
|
+
id: object.workos_id,
|
|
501
|
+
name: object.name,
|
|
502
|
+
value: object.value,
|
|
503
|
+
metadata: serializeVaultMetadata(object)
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
function parseStatuses(c) {
|
|
507
|
+
const repeated = c.req.queries("statuses") ?? [];
|
|
508
|
+
const bracketed = c.req.queries("statuses[]") ?? [];
|
|
509
|
+
return [...repeated, ...bracketed].flatMap((value) => value.split(",")).filter(Boolean);
|
|
510
|
+
}
|
|
511
|
+
function ensureUserByEmail(ws, email) {
|
|
512
|
+
const existing = ws.users.findOneBy("email", email);
|
|
513
|
+
if (existing) return existing;
|
|
514
|
+
return ws.users.insert({
|
|
515
|
+
workos_id: workosId("user"),
|
|
516
|
+
email,
|
|
517
|
+
first_name: "Test",
|
|
518
|
+
last_name: "User",
|
|
519
|
+
email_verified: true,
|
|
520
|
+
profile_picture_url: null
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
async function authenticationResponse(c, ws, baseUrl, user, organizationId, clientId) {
|
|
524
|
+
const session = ws.sessions.insert({
|
|
525
|
+
workos_id: workosId("session"),
|
|
526
|
+
refresh_token: randomToken("rt"),
|
|
527
|
+
user_id: user.workos_id,
|
|
528
|
+
organization_id: organizationId,
|
|
529
|
+
client_id: clientId,
|
|
530
|
+
revoked: false
|
|
531
|
+
});
|
|
532
|
+
const membership = organizationId ? ws.memberships.findBy("user_id", user.workos_id).find((m) => m.organization_id === organizationId) : void 0;
|
|
533
|
+
const accessToken = await signAccessToken(
|
|
534
|
+
{
|
|
535
|
+
sub: user.workos_id,
|
|
536
|
+
sid: session.workos_id,
|
|
537
|
+
...organizationId ? { org_id: organizationId } : {},
|
|
538
|
+
...membership ? { role: membership.role_slug } : {},
|
|
539
|
+
permissions: []
|
|
540
|
+
},
|
|
541
|
+
{ issuer: baseUrl, audience: clientId }
|
|
542
|
+
);
|
|
543
|
+
return c.json({
|
|
544
|
+
user: serializeUser(user),
|
|
545
|
+
organization_id: organizationId,
|
|
546
|
+
access_token: accessToken,
|
|
547
|
+
refresh_token: session.refresh_token,
|
|
548
|
+
authentication_method: "SSO"
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
function userManagementRoutes(ctx) {
|
|
552
|
+
const { app, store, baseUrl } = ctx;
|
|
553
|
+
const ws = () => getWorkosStore(store);
|
|
554
|
+
app.get("/user_management/authorize", (c) => {
|
|
555
|
+
const clientId = c.req.query("client_id") ?? "";
|
|
556
|
+
const redirectUri = c.req.query("redirect_uri") ?? "";
|
|
557
|
+
const state = c.req.query("state") ?? "";
|
|
558
|
+
const loginHint = c.req.query("login_hint");
|
|
559
|
+
if (loginHint) {
|
|
560
|
+
return issueCodeRedirect(c, ws(), loginHint, clientId, redirectUri, state);
|
|
561
|
+
}
|
|
562
|
+
const users = ws().users.all();
|
|
563
|
+
const buttons = users.map(
|
|
564
|
+
(user) => renderUserButton({
|
|
565
|
+
letter: (user.email[0] ?? "?").toUpperCase(),
|
|
566
|
+
login: user.email,
|
|
567
|
+
name: `${user.first_name ?? ""} ${user.last_name ?? ""}`.trim() || user.email,
|
|
568
|
+
email: user.email,
|
|
569
|
+
formAction: `${baseUrl}/user_management/authorize/submit`,
|
|
570
|
+
hiddenFields: { email: user.email, client_id: clientId, redirect_uri: redirectUri, state }
|
|
571
|
+
})
|
|
572
|
+
).join("\n");
|
|
573
|
+
const newUserForm = `
|
|
574
|
+
<form method="post" action="${baseUrl}/user_management/authorize/submit" class="new-user">
|
|
575
|
+
<input type="hidden" name="client_id" value="${escapeHtml(clientId)}" />
|
|
576
|
+
<input type="hidden" name="redirect_uri" value="${escapeHtml(redirectUri)}" />
|
|
577
|
+
<input type="hidden" name="state" value="${escapeHtml(state)}" />
|
|
578
|
+
<input type="email" name="email" class="checkout-input" placeholder="new-user@example.com" required />
|
|
579
|
+
<button type="submit" class="checkout-pay-btn">Continue as new user</button>
|
|
580
|
+
</form>`;
|
|
581
|
+
return c.html(
|
|
582
|
+
renderCardPage(
|
|
583
|
+
"Sign in with AuthKit",
|
|
584
|
+
"Pick an existing emulator user or continue as a new one.",
|
|
585
|
+
`${buttons}${newUserForm}`
|
|
586
|
+
),
|
|
587
|
+
200
|
|
588
|
+
);
|
|
589
|
+
});
|
|
590
|
+
app.post("/user_management/authorize/submit", async (c) => {
|
|
591
|
+
const form = await c.req.parseBody();
|
|
592
|
+
const email = String(form.email ?? "");
|
|
593
|
+
if (!email) return workosError(c, 422, "invalid_request", "email is required");
|
|
594
|
+
return issueCodeRedirect(
|
|
595
|
+
c,
|
|
596
|
+
ws(),
|
|
597
|
+
email,
|
|
598
|
+
String(form.client_id ?? ""),
|
|
599
|
+
String(form.redirect_uri ?? ""),
|
|
600
|
+
String(form.state ?? "")
|
|
601
|
+
);
|
|
602
|
+
});
|
|
603
|
+
function issueCodeRedirect(c, storeRef, email, clientId, redirectUri, state) {
|
|
604
|
+
if (!redirectUri) return workosError(c, 422, "invalid_request", "redirect_uri is required");
|
|
605
|
+
const user = ensureUserByEmail(storeRef, email);
|
|
606
|
+
const code = randomToken("code");
|
|
607
|
+
const activeMembership = storeRef.memberships.findBy("user_id", user.workos_id).find((m) => m.status === "active");
|
|
608
|
+
storeRef.authCodes.insert({
|
|
609
|
+
code,
|
|
610
|
+
user_id: user.workos_id,
|
|
611
|
+
organization_id: activeMembership?.organization_id ?? null,
|
|
612
|
+
client_id: clientId,
|
|
613
|
+
redirect_uri: redirectUri,
|
|
614
|
+
used: false
|
|
615
|
+
});
|
|
616
|
+
const target = new URL(redirectUri);
|
|
617
|
+
target.searchParams.set("code", code);
|
|
618
|
+
if (state) target.searchParams.set("state", state);
|
|
619
|
+
return c.redirect(target.toString(), 302);
|
|
620
|
+
}
|
|
621
|
+
app.post("/user_management/authenticate", async (c) => {
|
|
622
|
+
const body = await c.req.json().catch(() => ({}));
|
|
623
|
+
const grantType = String(body.grant_type ?? "");
|
|
624
|
+
const clientId = String(body.client_id ?? "");
|
|
625
|
+
if (grantType === "authorization_code") {
|
|
626
|
+
const code = String(body.code ?? "");
|
|
627
|
+
const authCode = ws().authCodes.findOneBy("code", code);
|
|
628
|
+
if (!authCode || authCode.used) {
|
|
629
|
+
return workosError(c, 400, "invalid_grant", "The code is invalid or has been used.");
|
|
630
|
+
}
|
|
631
|
+
ws().authCodes.update(authCode.id, { used: true });
|
|
632
|
+
const user = ws().users.findOneBy("workos_id", authCode.user_id);
|
|
633
|
+
if (!user) return workosError(c, 400, "invalid_grant", "Unknown user for code.");
|
|
634
|
+
return authenticationResponse(c, ws(), baseUrl, user, authCode.organization_id, clientId);
|
|
635
|
+
}
|
|
636
|
+
if (grantType === "refresh_token") {
|
|
637
|
+
const refreshToken = String(body.refresh_token ?? "");
|
|
638
|
+
const session = ws().sessions.findOneBy("refresh_token", refreshToken);
|
|
639
|
+
if (!session || session.revoked) {
|
|
640
|
+
return workosError(c, 400, "invalid_grant", "Refresh token is invalid.");
|
|
641
|
+
}
|
|
642
|
+
const user = ws().users.findOneBy("workos_id", session.user_id);
|
|
643
|
+
if (!user) return workosError(c, 400, "invalid_grant", "Unknown user for session.");
|
|
644
|
+
const requestedOrg = typeof body.organization_id === "string" && body.organization_id.length > 0 ? body.organization_id : session.organization_id;
|
|
645
|
+
if (requestedOrg) {
|
|
646
|
+
const membership = ws().memberships.findBy("user_id", user.workos_id).find((m) => m.organization_id === requestedOrg && m.status === "active");
|
|
647
|
+
if (!membership) {
|
|
648
|
+
return workosError(
|
|
649
|
+
c,
|
|
650
|
+
403,
|
|
651
|
+
"sso_required",
|
|
652
|
+
"User does not have an active membership in the requested organization."
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
const organization = ws().organizations.findOneBy("workos_id", requestedOrg);
|
|
656
|
+
if (!organization) return workosError(c, 404, "not_found", "Organization not found.");
|
|
657
|
+
}
|
|
658
|
+
ws().sessions.update(session.id, { revoked: true });
|
|
659
|
+
return authenticationResponse(c, ws(), baseUrl, user, requestedOrg ?? null, clientId);
|
|
660
|
+
}
|
|
661
|
+
return workosError(c, 400, "unsupported_grant_type", `Unsupported grant_type: ${grantType}`);
|
|
662
|
+
});
|
|
663
|
+
app.get("/user_management/users/:id", (c) => {
|
|
664
|
+
const user = ws().users.findOneBy("workos_id", c.req.param("id"));
|
|
665
|
+
if (!user) return workosError(c, 404, "entity_not_found", "User not found.");
|
|
666
|
+
return c.json(serializeUser(user));
|
|
667
|
+
});
|
|
668
|
+
app.get("/user_management/organization_memberships", (c) => {
|
|
669
|
+
const userId = c.req.query("user_id");
|
|
670
|
+
const organizationId = c.req.query("organization_id");
|
|
671
|
+
const statuses = parseStatuses(c);
|
|
672
|
+
let memberships = ws().memberships.all();
|
|
673
|
+
if (userId) memberships = memberships.filter((m) => m.user_id === userId);
|
|
674
|
+
if (organizationId) {
|
|
675
|
+
memberships = memberships.filter((m) => m.organization_id === organizationId);
|
|
676
|
+
}
|
|
677
|
+
if (statuses.length > 0) memberships = memberships.filter((m) => statuses.includes(m.status));
|
|
678
|
+
return c.json(
|
|
679
|
+
listEnvelope(
|
|
680
|
+
memberships.map(
|
|
681
|
+
(m) => serializeMembership(
|
|
682
|
+
m,
|
|
683
|
+
ws().organizations.findOneBy("workos_id", m.organization_id)?.name ?? m.organization_id
|
|
684
|
+
)
|
|
685
|
+
)
|
|
686
|
+
)
|
|
687
|
+
);
|
|
688
|
+
});
|
|
689
|
+
app.post("/user_management/organization_memberships", async (c) => {
|
|
690
|
+
const body = await c.req.json().catch(() => ({}));
|
|
691
|
+
const userId = String(body.user_id ?? "");
|
|
692
|
+
const organizationId = String(body.organization_id ?? "");
|
|
693
|
+
const user = ws().users.findOneBy("workos_id", userId);
|
|
694
|
+
const organization = ws().organizations.findOneBy("workos_id", organizationId);
|
|
695
|
+
if (!user) return workosError(c, 404, "entity_not_found", "User not found.");
|
|
696
|
+
if (!organization) return workosError(c, 404, "entity_not_found", "Organization not found.");
|
|
697
|
+
const existing = ws().memberships.findBy("user_id", userId).find((m) => m.organization_id === organizationId);
|
|
698
|
+
if (existing) {
|
|
699
|
+
return workosError(c, 409, "organization_membership_already_exists", "Already a member.");
|
|
700
|
+
}
|
|
701
|
+
const membership = ws().memberships.insert({
|
|
702
|
+
workos_id: workosId("om"),
|
|
703
|
+
user_id: userId,
|
|
704
|
+
organization_id: organizationId,
|
|
705
|
+
status: "active",
|
|
706
|
+
role_slug: typeof body.role_slug === "string" && body.role_slug ? body.role_slug : "member"
|
|
707
|
+
});
|
|
708
|
+
return c.json(serializeMembership(membership, organization.name), 201);
|
|
709
|
+
});
|
|
710
|
+
app.get("/user_management/organization_memberships/:id", (c) => {
|
|
711
|
+
const membership = ws().memberships.findOneBy("workos_id", c.req.param("id"));
|
|
712
|
+
if (!membership) return workosError(c, 404, "entity_not_found", "Membership not found.");
|
|
713
|
+
const organizationName = ws().organizations.findOneBy("workos_id", membership.organization_id)?.name ?? membership.organization_id;
|
|
714
|
+
return c.json(serializeMembership(membership, organizationName));
|
|
715
|
+
});
|
|
716
|
+
app.put("/user_management/organization_memberships/:id", async (c) => {
|
|
717
|
+
const membership = ws().memberships.findOneBy("workos_id", c.req.param("id"));
|
|
718
|
+
if (!membership) return workosError(c, 404, "entity_not_found", "Membership not found.");
|
|
719
|
+
const body = await c.req.json().catch(() => ({}));
|
|
720
|
+
const updated = ws().memberships.update(membership.id, {
|
|
721
|
+
role_slug: typeof body.role_slug === "string" && body.role_slug ? body.role_slug : membership.role_slug
|
|
722
|
+
});
|
|
723
|
+
const organizationName = ws().organizations.findOneBy("workos_id", updated.organization_id)?.name ?? updated.organization_id;
|
|
724
|
+
return c.json(serializeMembership(updated, organizationName));
|
|
725
|
+
});
|
|
726
|
+
app.delete("/user_management/organization_memberships/:id", (c) => {
|
|
727
|
+
const membership = ws().memberships.findOneBy("workos_id", c.req.param("id"));
|
|
728
|
+
if (!membership) return workosError(c, 404, "entity_not_found", "Membership not found.");
|
|
729
|
+
ws().memberships.delete(membership.id);
|
|
730
|
+
return c.body(null, 204);
|
|
731
|
+
});
|
|
732
|
+
app.get("/user_management/invitations", (c) => {
|
|
733
|
+
const email = c.req.query("email");
|
|
734
|
+
const organizationId = c.req.query("organization_id");
|
|
735
|
+
let invitations = ws().invitations.all();
|
|
736
|
+
if (email) invitations = invitations.filter((i) => i.email === email);
|
|
737
|
+
if (organizationId) {
|
|
738
|
+
invitations = invitations.filter((i) => i.organization_id === organizationId);
|
|
739
|
+
}
|
|
740
|
+
return c.json(listEnvelope(invitations.map(serializeInvitation)));
|
|
741
|
+
});
|
|
742
|
+
app.post("/user_management/invitations", async (c) => {
|
|
743
|
+
const body = await c.req.json().catch(() => ({}));
|
|
744
|
+
const email = String(body.email ?? "");
|
|
745
|
+
const organizationId = String(body.organization_id ?? "");
|
|
746
|
+
if (!email) return workosError(c, 422, "invalid_request", "email is required");
|
|
747
|
+
if (!ws().organizations.findOneBy("workos_id", organizationId)) {
|
|
748
|
+
return workosError(c, 404, "entity_not_found", "Organization not found.");
|
|
749
|
+
}
|
|
750
|
+
const invitation = ws().invitations.insert({
|
|
751
|
+
workos_id: workosId("invitation"),
|
|
752
|
+
email,
|
|
753
|
+
organization_id: organizationId,
|
|
754
|
+
inviter_user_id: typeof body.inviter_user_id === "string" ? body.inviter_user_id : null,
|
|
755
|
+
role_slug: typeof body.role_slug === "string" ? body.role_slug : null,
|
|
756
|
+
state: "pending",
|
|
757
|
+
token: randomToken("invite"),
|
|
758
|
+
expires_at: new Date(Date.now() + 7 * 24 * 3600 * 1e3).toISOString()
|
|
759
|
+
});
|
|
760
|
+
return c.json(serializeInvitation(invitation), 201);
|
|
761
|
+
});
|
|
762
|
+
app.post("/user_management/invitations/:id/accept", (c) => {
|
|
763
|
+
const invitation = ws().invitations.findOneBy("workos_id", c.req.param("id"));
|
|
764
|
+
if (!invitation) return workosError(c, 404, "entity_not_found", "Invitation not found.");
|
|
765
|
+
if (invitation.state !== "pending") {
|
|
766
|
+
return workosError(c, 400, "invalid_request", "Invitation is not pending.");
|
|
767
|
+
}
|
|
768
|
+
const updated = ws().invitations.update(invitation.id, { state: "accepted" });
|
|
769
|
+
const user = ws().users.findOneBy("email", invitation.email);
|
|
770
|
+
if (user) {
|
|
771
|
+
const already = ws().memberships.findBy("user_id", user.workos_id).find((m) => m.organization_id === invitation.organization_id);
|
|
772
|
+
if (!already) {
|
|
773
|
+
ws().memberships.insert({
|
|
774
|
+
workos_id: workosId("om"),
|
|
775
|
+
user_id: user.workos_id,
|
|
776
|
+
organization_id: invitation.organization_id,
|
|
777
|
+
status: "active",
|
|
778
|
+
role_slug: invitation.role_slug ?? "member"
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
return c.json(serializeInvitation(updated));
|
|
783
|
+
});
|
|
784
|
+
app.get("/user_management/users/:id/api_keys", (c) => {
|
|
785
|
+
const userId = c.req.param("id");
|
|
786
|
+
const organizationId = c.req.query("organization_id");
|
|
787
|
+
let keys = ws().apiKeys.findBy("user_id", userId);
|
|
788
|
+
if (organizationId) keys = keys.filter((k) => k.organization_id === organizationId);
|
|
789
|
+
return c.json(listEnvelope(keys.map((k) => serializeApiKey(k))));
|
|
790
|
+
});
|
|
791
|
+
app.post("/user_management/users/:id/api_keys", async (c) => {
|
|
792
|
+
const userId = c.req.param("id");
|
|
793
|
+
const body = await c.req.json().catch(() => ({}));
|
|
794
|
+
const user = ws().users.findOneBy("workos_id", userId);
|
|
795
|
+
if (!user) return workosError(c, 404, "entity_not_found", "User not found.");
|
|
796
|
+
const key = ws().apiKeys.insert({
|
|
797
|
+
workos_id: workosId("key"),
|
|
798
|
+
name: String(body.name ?? "api key"),
|
|
799
|
+
value: randomToken("sk_emulate"),
|
|
800
|
+
user_id: userId,
|
|
801
|
+
organization_id: String(body.organization_id ?? ""),
|
|
802
|
+
last_used_at: null
|
|
803
|
+
});
|
|
804
|
+
return c.json(serializeApiKey(key, { includeValue: true }), 201);
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
function organizationRoutes(ctx) {
|
|
808
|
+
const { app, store, baseUrl } = ctx;
|
|
809
|
+
const ws = () => getWorkosStore(store);
|
|
810
|
+
app.post("/organizations", async (c) => {
|
|
811
|
+
const body = await c.req.json().catch(() => ({}));
|
|
812
|
+
const name = String(body.name ?? "");
|
|
813
|
+
if (!name) return workosError(c, 422, "invalid_request", "name is required");
|
|
814
|
+
const organization = ws().organizations.insert({
|
|
815
|
+
workos_id: workosId("org"),
|
|
816
|
+
name,
|
|
817
|
+
external_id: typeof body.external_id === "string" ? body.external_id : null
|
|
818
|
+
});
|
|
819
|
+
return c.json(serializeOrganization(organization), 201);
|
|
820
|
+
});
|
|
821
|
+
app.get("/organizations/:id", (c) => {
|
|
822
|
+
const organization = ws().organizations.findOneBy("workos_id", c.req.param("id"));
|
|
823
|
+
if (!organization) return workosError(c, 404, "entity_not_found", "Organization not found.");
|
|
824
|
+
return c.json(serializeOrganization(organization));
|
|
825
|
+
});
|
|
826
|
+
app.put("/organizations/:id", async (c) => {
|
|
827
|
+
const organization = ws().organizations.findOneBy("workos_id", c.req.param("id"));
|
|
828
|
+
if (!organization) return workosError(c, 404, "entity_not_found", "Organization not found.");
|
|
829
|
+
const body = await c.req.json().catch(() => ({}));
|
|
830
|
+
const updated = ws().organizations.update(organization.id, {
|
|
831
|
+
name: typeof body.name === "string" && body.name ? body.name : organization.name
|
|
832
|
+
});
|
|
833
|
+
return c.json(serializeOrganization(updated));
|
|
834
|
+
});
|
|
835
|
+
app.get("/organizations/:id/roles", (c) => {
|
|
836
|
+
const organization = ws().organizations.findOneBy("workos_id", c.req.param("id"));
|
|
837
|
+
if (!organization) return workosError(c, 404, "entity_not_found", "Organization not found.");
|
|
838
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
839
|
+
const role = (slug, name) => ({
|
|
840
|
+
object: "role",
|
|
841
|
+
id: `role_${slug}`,
|
|
842
|
+
name,
|
|
843
|
+
slug,
|
|
844
|
+
description: null,
|
|
845
|
+
permissions: [],
|
|
846
|
+
resource_type_slug: "organization",
|
|
847
|
+
type: "OrganizationRole",
|
|
848
|
+
created_at: now,
|
|
849
|
+
updated_at: now
|
|
850
|
+
});
|
|
851
|
+
return c.json(listEnvelope([role("admin", "Admin"), role("member", "Member")]));
|
|
852
|
+
});
|
|
853
|
+
app.post("/portal/generate_link", async (c) => {
|
|
854
|
+
const body = await c.req.json().catch(() => ({}));
|
|
855
|
+
const organization = String(body.organization ?? "");
|
|
856
|
+
return c.json({ link: `${baseUrl}/_portal/${organization}?intent=${body.intent ?? ""}` });
|
|
857
|
+
});
|
|
858
|
+
app.get(
|
|
859
|
+
"/organization_domains/:id",
|
|
860
|
+
(c) => c.json({
|
|
861
|
+
object: "organization_domain",
|
|
862
|
+
id: c.req.param("id"),
|
|
863
|
+
organization_id: "org_unknown",
|
|
864
|
+
domain: "example.com",
|
|
865
|
+
state: "verified",
|
|
866
|
+
verification_strategy: "dns",
|
|
867
|
+
verification_token: "token"
|
|
868
|
+
})
|
|
869
|
+
);
|
|
870
|
+
app.delete("/organization_domains/:id", (c) => c.body(null, 204));
|
|
871
|
+
}
|
|
872
|
+
function apiKeyRoutes(ctx) {
|
|
873
|
+
const { app, store } = ctx;
|
|
874
|
+
const ws = () => getWorkosStore(store);
|
|
875
|
+
app.post("/api_keys/validations", async (c) => {
|
|
876
|
+
const body = await c.req.json().catch(() => ({}));
|
|
877
|
+
const value = String(body.value ?? "");
|
|
878
|
+
const key = ws().apiKeys.findOneBy("value", value);
|
|
879
|
+
if (!key) return workosError(c, 404, "invalid_api_key", "API key is invalid.");
|
|
880
|
+
ws().apiKeys.update(key.id, { last_used_at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
881
|
+
return c.json({ api_key: serializeApiKey(key) });
|
|
882
|
+
});
|
|
883
|
+
app.delete("/api_keys/:id", (c) => {
|
|
884
|
+
const key = ws().apiKeys.findOneBy("workos_id", c.req.param("id"));
|
|
885
|
+
if (!key) return workosError(c, 404, "entity_not_found", "API key not found.");
|
|
886
|
+
ws().apiKeys.delete(key.id);
|
|
887
|
+
return c.body(null, 204);
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
function vaultRoutes(ctx) {
|
|
891
|
+
const { app, store } = ctx;
|
|
892
|
+
const ws = () => getWorkosStore(store);
|
|
893
|
+
app.post("/vault/v1/kv", async (c) => {
|
|
894
|
+
const body = await c.req.json().catch(() => ({}));
|
|
895
|
+
const name = String(body.name ?? "");
|
|
896
|
+
if (!name) return workosError(c, 400, "invalid_request", "name is required");
|
|
897
|
+
if (ws().vaultObjects.findOneBy("name", name)) {
|
|
898
|
+
return workosError(c, 409, "conflict", `An object named '${name}' already exists.`);
|
|
899
|
+
}
|
|
900
|
+
const object = ws().vaultObjects.insert({
|
|
901
|
+
workos_id: workosId("kv"),
|
|
902
|
+
name,
|
|
903
|
+
value: String(body.value ?? ""),
|
|
904
|
+
key_context: body.key_context ?? {},
|
|
905
|
+
version_id: randomToken("version")
|
|
906
|
+
});
|
|
907
|
+
return c.json(serializeVaultMetadata(object), 201);
|
|
908
|
+
});
|
|
909
|
+
app.get(
|
|
910
|
+
"/vault/v1/kv",
|
|
911
|
+
(c) => c.json(
|
|
912
|
+
listEnvelope(
|
|
913
|
+
ws().vaultObjects.all().map((object) => ({ id: object.workos_id, name: object.name }))
|
|
914
|
+
)
|
|
915
|
+
)
|
|
916
|
+
);
|
|
917
|
+
app.get("/vault/v1/kv/name/:name", (c) => {
|
|
918
|
+
const object = ws().vaultObjects.findOneBy("name", c.req.param("name"));
|
|
919
|
+
if (!object) return workosError(c, 404, "not_found", "Object not found.");
|
|
920
|
+
return c.json(serializeVaultObject(object));
|
|
921
|
+
});
|
|
922
|
+
app.get("/vault/v1/kv/:id", (c) => {
|
|
923
|
+
const object = ws().vaultObjects.findOneBy("workos_id", c.req.param("id"));
|
|
924
|
+
if (!object) return workosError(c, 404, "not_found", "Object not found.");
|
|
925
|
+
return c.json(serializeVaultObject(object));
|
|
926
|
+
});
|
|
927
|
+
app.put("/vault/v1/kv/:id", async (c) => {
|
|
928
|
+
const object = ws().vaultObjects.findOneBy("workos_id", c.req.param("id"));
|
|
929
|
+
if (!object) return workosError(c, 404, "not_found", "Object not found.");
|
|
930
|
+
const body = await c.req.json().catch(() => ({}));
|
|
931
|
+
const versionCheck = body.version_check;
|
|
932
|
+
if (typeof versionCheck === "string" && versionCheck !== object.version_id) {
|
|
933
|
+
return workosError(c, 409, "conflict", "Version check failed.");
|
|
934
|
+
}
|
|
935
|
+
const updated = ws().vaultObjects.update(object.id, {
|
|
936
|
+
value: String(body.value ?? object.value),
|
|
937
|
+
version_id: randomToken("version")
|
|
938
|
+
});
|
|
939
|
+
return c.json(serializeVaultObject(updated));
|
|
940
|
+
});
|
|
941
|
+
app.delete("/vault/v1/kv/:id", (c) => {
|
|
942
|
+
const object = ws().vaultObjects.findOneBy("workos_id", c.req.param("id"));
|
|
943
|
+
if (!object) return workosError(c, 404, "not_found", "Object not found.");
|
|
944
|
+
ws().vaultObjects.delete(object.id);
|
|
945
|
+
return c.body(null, 204);
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
function oauthRoutes(ctx) {
|
|
949
|
+
const { app, store, baseUrl } = ctx;
|
|
950
|
+
const ws = () => getWorkosStore(store);
|
|
951
|
+
app.get("/sso/jwks/:clientId", async (c) => c.json(await jwksResponse()));
|
|
952
|
+
app.get("/oauth2/jwks", async (c) => c.json(await jwksResponse()));
|
|
953
|
+
app.get(
|
|
954
|
+
"/.well-known/oauth-authorization-server",
|
|
955
|
+
(c) => c.json({
|
|
956
|
+
issuer: baseUrl,
|
|
957
|
+
authorization_endpoint: `${baseUrl}/oauth2/authorize`,
|
|
958
|
+
token_endpoint: `${baseUrl}/oauth2/token`,
|
|
959
|
+
registration_endpoint: `${baseUrl}/oauth2/register`,
|
|
960
|
+
jwks_uri: `${baseUrl}/oauth2/jwks`,
|
|
961
|
+
response_types_supported: ["code"],
|
|
962
|
+
grant_types_supported: ["authorization_code", "refresh_token"],
|
|
963
|
+
code_challenge_methods_supported: ["S256"],
|
|
964
|
+
token_endpoint_auth_methods_supported: ["client_secret_post", "none"]
|
|
965
|
+
})
|
|
966
|
+
);
|
|
967
|
+
app.post("/oauth2/register", async (c) => {
|
|
968
|
+
const body = await c.req.json().catch(() => ({}));
|
|
969
|
+
const client = ws().oauthClients.insert({
|
|
970
|
+
client_id: workosId("client"),
|
|
971
|
+
client_secret: null,
|
|
972
|
+
redirect_uris: Array.isArray(body.redirect_uris) ? body.redirect_uris : [],
|
|
973
|
+
name: typeof body.client_name === "string" ? body.client_name : null
|
|
974
|
+
});
|
|
975
|
+
return c.json(
|
|
976
|
+
{
|
|
977
|
+
client_id: client.client_id,
|
|
978
|
+
client_id_issued_at: Math.floor(Date.now() / 1e3),
|
|
979
|
+
redirect_uris: client.redirect_uris,
|
|
980
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
981
|
+
response_types: ["code"],
|
|
982
|
+
token_endpoint_auth_method: "none"
|
|
983
|
+
},
|
|
984
|
+
201
|
|
985
|
+
);
|
|
986
|
+
});
|
|
987
|
+
app.get("/oauth2/authorize", (c) => {
|
|
988
|
+
const clientId = c.req.query("client_id") ?? "";
|
|
989
|
+
const redirectUri = c.req.query("redirect_uri") ?? "";
|
|
990
|
+
const state = c.req.query("state") ?? "";
|
|
991
|
+
const codeChallenge = c.req.query("code_challenge") ?? "";
|
|
992
|
+
const loginHint = c.req.query("login_hint");
|
|
993
|
+
if (!redirectUri) return workosError(c, 422, "invalid_request", "redirect_uri is required");
|
|
994
|
+
const issue = (email) => {
|
|
995
|
+
const user = ensureUserByEmail(ws(), email);
|
|
996
|
+
const activeMembership = ws().memberships.findBy("user_id", user.workos_id).find((m) => m.status === "active");
|
|
997
|
+
const code = randomToken("mcpcode");
|
|
998
|
+
ws().oauthCodes.insert({
|
|
999
|
+
code,
|
|
1000
|
+
user_id: user.workos_id,
|
|
1001
|
+
organization_id: activeMembership?.organization_id ?? null,
|
|
1002
|
+
client_id: clientId,
|
|
1003
|
+
redirect_uri: redirectUri,
|
|
1004
|
+
code_challenge: codeChallenge || null,
|
|
1005
|
+
used: false
|
|
1006
|
+
});
|
|
1007
|
+
const target = new URL(redirectUri);
|
|
1008
|
+
target.searchParams.set("code", code);
|
|
1009
|
+
if (state) target.searchParams.set("state", state);
|
|
1010
|
+
return c.redirect(target.toString(), 302);
|
|
1011
|
+
};
|
|
1012
|
+
if (loginHint) return issue(loginHint);
|
|
1013
|
+
const users = ws().users.all();
|
|
1014
|
+
const buttons = users.map(
|
|
1015
|
+
(user) => renderUserButton({
|
|
1016
|
+
letter: (user.email[0] ?? "?").toUpperCase(),
|
|
1017
|
+
login: user.email,
|
|
1018
|
+
name: `${user.first_name ?? ""} ${user.last_name ?? ""}`.trim() || user.email,
|
|
1019
|
+
email: user.email,
|
|
1020
|
+
formAction: `${baseUrl}/oauth2/authorize/submit`,
|
|
1021
|
+
hiddenFields: {
|
|
1022
|
+
email: user.email,
|
|
1023
|
+
client_id: clientId,
|
|
1024
|
+
redirect_uri: redirectUri,
|
|
1025
|
+
state,
|
|
1026
|
+
code_challenge: codeChallenge
|
|
1027
|
+
}
|
|
1028
|
+
})
|
|
1029
|
+
).join("\n");
|
|
1030
|
+
const newUserForm = `
|
|
1031
|
+
<form method="post" action="${baseUrl}/oauth2/authorize/submit" class="new-user">
|
|
1032
|
+
<input type="hidden" name="client_id" value="${escapeHtml(clientId)}" />
|
|
1033
|
+
<input type="hidden" name="redirect_uri" value="${escapeHtml(redirectUri)}" />
|
|
1034
|
+
<input type="hidden" name="state" value="${escapeHtml(state)}" />
|
|
1035
|
+
<input type="hidden" name="code_challenge" value="${escapeHtml(codeChallenge)}" />
|
|
1036
|
+
<input type="email" name="email" class="checkout-input" placeholder="new-user@example.com" required />
|
|
1037
|
+
<button type="submit" class="checkout-pay-btn">Continue as new user</button>
|
|
1038
|
+
</form>`;
|
|
1039
|
+
return c.html(
|
|
1040
|
+
renderCardPage("Authorize MCP client", "Sign in to connect this MCP client.", `${buttons}${newUserForm}`),
|
|
1041
|
+
200
|
|
1042
|
+
);
|
|
1043
|
+
});
|
|
1044
|
+
app.post("/oauth2/authorize/submit", async (c) => {
|
|
1045
|
+
const form = await c.req.parseBody();
|
|
1046
|
+
const email = String(form.email ?? "");
|
|
1047
|
+
const redirectUri = String(form.redirect_uri ?? "");
|
|
1048
|
+
if (!email || !redirectUri) {
|
|
1049
|
+
return workosError(c, 422, "invalid_request", "email and redirect_uri are required");
|
|
1050
|
+
}
|
|
1051
|
+
const user = ensureUserByEmail(ws(), email);
|
|
1052
|
+
const activeMembership = ws().memberships.findBy("user_id", user.workos_id).find((m) => m.status === "active");
|
|
1053
|
+
const code = randomToken("mcpcode");
|
|
1054
|
+
ws().oauthCodes.insert({
|
|
1055
|
+
code,
|
|
1056
|
+
user_id: user.workos_id,
|
|
1057
|
+
organization_id: activeMembership?.organization_id ?? null,
|
|
1058
|
+
client_id: String(form.client_id ?? ""),
|
|
1059
|
+
redirect_uri: redirectUri,
|
|
1060
|
+
code_challenge: String(form.code_challenge ?? "") || null,
|
|
1061
|
+
used: false
|
|
1062
|
+
});
|
|
1063
|
+
const target = new URL(redirectUri);
|
|
1064
|
+
target.searchParams.set("code", code);
|
|
1065
|
+
const state = String(form.state ?? "");
|
|
1066
|
+
if (state) target.searchParams.set("state", state);
|
|
1067
|
+
return c.redirect(target.toString(), 302);
|
|
1068
|
+
});
|
|
1069
|
+
app.post("/oauth2/token", async (c) => {
|
|
1070
|
+
const contentType = c.req.header("content-type") ?? "";
|
|
1071
|
+
const body = contentType.includes("json") ? await c.req.json().catch(() => ({})) : await c.req.parseBody();
|
|
1072
|
+
const grantType = String(body.grant_type ?? "");
|
|
1073
|
+
if (grantType === "refresh_token") {
|
|
1074
|
+
const refreshToken = String(body.refresh_token ?? "");
|
|
1075
|
+
const session2 = ws().sessions.findOneBy("refresh_token", refreshToken);
|
|
1076
|
+
if (!session2 || session2.revoked) {
|
|
1077
|
+
return workosError(c, 400, "invalid_grant", "Refresh token is invalid.");
|
|
1078
|
+
}
|
|
1079
|
+
ws().sessions.update(session2.id, { revoked: true });
|
|
1080
|
+
const rotated = ws().sessions.insert({
|
|
1081
|
+
workos_id: workosId("session"),
|
|
1082
|
+
refresh_token: randomToken("rt"),
|
|
1083
|
+
user_id: session2.user_id,
|
|
1084
|
+
organization_id: session2.organization_id,
|
|
1085
|
+
client_id: session2.client_id,
|
|
1086
|
+
revoked: false
|
|
1087
|
+
});
|
|
1088
|
+
const audience2 = process.env.EMULATE_WORKOS_AUDIENCE ?? session2.client_id;
|
|
1089
|
+
const accessToken2 = await signAccessToken(
|
|
1090
|
+
{
|
|
1091
|
+
sub: session2.user_id,
|
|
1092
|
+
sid: rotated.workos_id,
|
|
1093
|
+
...session2.organization_id ? { org_id: session2.organization_id } : {},
|
|
1094
|
+
permissions: []
|
|
1095
|
+
},
|
|
1096
|
+
{ issuer: baseUrl, audience: audience2 }
|
|
1097
|
+
);
|
|
1098
|
+
return c.json({
|
|
1099
|
+
access_token: accessToken2,
|
|
1100
|
+
token_type: "Bearer",
|
|
1101
|
+
expires_in: 3600,
|
|
1102
|
+
refresh_token: rotated.refresh_token
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
if (grantType !== "authorization_code") {
|
|
1106
|
+
return workosError(c, 400, "unsupported_grant_type", `Unsupported grant_type: ${grantType}`);
|
|
1107
|
+
}
|
|
1108
|
+
const code = String(body.code ?? "");
|
|
1109
|
+
const oauthCode = ws().oauthCodes.findOneBy("code", code);
|
|
1110
|
+
if (!oauthCode || oauthCode.used) {
|
|
1111
|
+
return workosError(c, 400, "invalid_grant", "The code is invalid or has been used.");
|
|
1112
|
+
}
|
|
1113
|
+
ws().oauthCodes.update(oauthCode.id, { used: true });
|
|
1114
|
+
const audience = process.env.EMULATE_WORKOS_AUDIENCE ?? oauthCode.client_id;
|
|
1115
|
+
const session = ws().sessions.insert({
|
|
1116
|
+
workos_id: workosId("session"),
|
|
1117
|
+
refresh_token: randomToken("rt"),
|
|
1118
|
+
user_id: oauthCode.user_id,
|
|
1119
|
+
organization_id: oauthCode.organization_id,
|
|
1120
|
+
client_id: oauthCode.client_id,
|
|
1121
|
+
revoked: false
|
|
1122
|
+
});
|
|
1123
|
+
const accessToken = await signAccessToken(
|
|
1124
|
+
{
|
|
1125
|
+
sub: oauthCode.user_id,
|
|
1126
|
+
sid: session.workos_id,
|
|
1127
|
+
...oauthCode.organization_id ? { org_id: oauthCode.organization_id } : {},
|
|
1128
|
+
permissions: []
|
|
1129
|
+
},
|
|
1130
|
+
{ issuer: baseUrl, audience }
|
|
1131
|
+
);
|
|
1132
|
+
return c.json({
|
|
1133
|
+
access_token: accessToken,
|
|
1134
|
+
token_type: "Bearer",
|
|
1135
|
+
expires_in: 3600,
|
|
1136
|
+
refresh_token: session.refresh_token
|
|
1137
|
+
});
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
var manifest = {
|
|
1141
|
+
id: "workos",
|
|
1142
|
+
name: "WorkOS",
|
|
1143
|
+
description: "Stateful WorkOS emulator: AuthKit user management (hosted login, code + refresh grants, sealed-session JWKS), organizations, memberships, invitations, API keys, Vault KV, and an OAuth authorization server for MCP clients.",
|
|
1144
|
+
docsUrl: "https://docs.emulators.dev/workos",
|
|
1145
|
+
surfaces: [
|
|
1146
|
+
{ id: "rest", kind: "rest", title: "WorkOS REST API", status: "partial", basePath: "/" },
|
|
1147
|
+
{ id: "authkit", kind: "ui", title: "Hosted AuthKit login", status: "supported", basePath: "/user_management/authorize" },
|
|
1148
|
+
{ id: "oauth", kind: "rest", title: "OAuth authorization server (MCP)", status: "supported", basePath: "/oauth2" },
|
|
1149
|
+
{ id: "vault", kind: "rest", title: "Vault KV", status: "supported", basePath: "/vault/v1/kv" }
|
|
1150
|
+
],
|
|
1151
|
+
auth: [{ id: "api-key", title: "WorkOS secret key", type: "api-key", status: "supported" }],
|
|
1152
|
+
specs: [
|
|
1153
|
+
{
|
|
1154
|
+
kind: "manual",
|
|
1155
|
+
title: "WorkOS User Management + Organizations subset",
|
|
1156
|
+
coverage: "hand-authored",
|
|
1157
|
+
operations: [
|
|
1158
|
+
{ operationId: "userManagement.authorize", method: "GET", path: "/user_management/authorize", status: "hand-authored" },
|
|
1159
|
+
{ operationId: "userManagement.authenticate", method: "POST", path: "/user_management/authenticate", status: "hand-authored" },
|
|
1160
|
+
{ operationId: "userManagement.getUser", method: "GET", path: "/user_management/users/:id", status: "hand-authored" },
|
|
1161
|
+
{ operationId: "memberships.list", method: "GET", path: "/user_management/organization_memberships", status: "hand-authored" },
|
|
1162
|
+
{ operationId: "memberships.create", method: "POST", path: "/user_management/organization_memberships", status: "hand-authored" },
|
|
1163
|
+
{ operationId: "memberships.get", method: "GET", path: "/user_management/organization_memberships/:id", status: "hand-authored" },
|
|
1164
|
+
{ operationId: "memberships.update", method: "PUT", path: "/user_management/organization_memberships/:id", status: "hand-authored" },
|
|
1165
|
+
{ operationId: "memberships.delete", method: "DELETE", path: "/user_management/organization_memberships/:id", status: "hand-authored" },
|
|
1166
|
+
{ operationId: "invitations.list", method: "GET", path: "/user_management/invitations", status: "hand-authored" },
|
|
1167
|
+
{ operationId: "invitations.send", method: "POST", path: "/user_management/invitations", status: "hand-authored" },
|
|
1168
|
+
{ operationId: "invitations.accept", method: "POST", path: "/user_management/invitations/:id/accept", status: "hand-authored" },
|
|
1169
|
+
{ operationId: "userApiKeys.list", method: "GET", path: "/user_management/users/:id/api_keys", status: "hand-authored" },
|
|
1170
|
+
{ operationId: "userApiKeys.create", method: "POST", path: "/user_management/users/:id/api_keys", status: "hand-authored" },
|
|
1171
|
+
{ operationId: "apiKeys.validate", method: "POST", path: "/api_keys/validations", status: "hand-authored" },
|
|
1172
|
+
{ operationId: "apiKeys.delete", method: "DELETE", path: "/api_keys/:id", status: "hand-authored" },
|
|
1173
|
+
{ operationId: "organizations.create", method: "POST", path: "/organizations", status: "hand-authored" },
|
|
1174
|
+
{ operationId: "organizations.get", method: "GET", path: "/organizations/:id", status: "hand-authored" },
|
|
1175
|
+
{ operationId: "organizations.update", method: "PUT", path: "/organizations/:id", status: "hand-authored" },
|
|
1176
|
+
{ operationId: "organizations.roles", method: "GET", path: "/organizations/:id/roles", status: "hand-authored" },
|
|
1177
|
+
{ operationId: "sso.jwks", method: "GET", path: "/sso/jwks/:clientId", status: "hand-authored" },
|
|
1178
|
+
{ operationId: "oauth.metadata", method: "GET", path: "/.well-known/oauth-authorization-server", status: "hand-authored" },
|
|
1179
|
+
{ operationId: "oauth.register", method: "POST", path: "/oauth2/register", status: "hand-authored" },
|
|
1180
|
+
{ operationId: "oauth.authorize", method: "GET", path: "/oauth2/authorize", status: "hand-authored" },
|
|
1181
|
+
{ operationId: "oauth.token", method: "POST", path: "/oauth2/token", status: "hand-authored" },
|
|
1182
|
+
{ operationId: "vault.create", method: "POST", path: "/vault/v1/kv", status: "hand-authored" },
|
|
1183
|
+
{ operationId: "vault.readByName", method: "GET", path: "/vault/v1/kv/name/:name", status: "hand-authored" },
|
|
1184
|
+
{ operationId: "vault.update", method: "PUT", path: "/vault/v1/kv/:id", status: "hand-authored" },
|
|
1185
|
+
{ operationId: "vault.delete", method: "DELETE", path: "/vault/v1/kv/:id", status: "hand-authored" }
|
|
1186
|
+
]
|
|
1187
|
+
}
|
|
1188
|
+
],
|
|
1189
|
+
seedSchema: {
|
|
1190
|
+
description: "Seed users, organizations, and memberships.",
|
|
1191
|
+
fields: [
|
|
1192
|
+
{
|
|
1193
|
+
key: "users",
|
|
1194
|
+
title: "Users",
|
|
1195
|
+
description: "AuthKit users (email is the identity; sign-in creates missing users on the fly).",
|
|
1196
|
+
example: [{ email: "admin@example.com", first_name: "Admin", last_name: "User" }]
|
|
1197
|
+
},
|
|
1198
|
+
{
|
|
1199
|
+
key: "organizations",
|
|
1200
|
+
title: "Organizations",
|
|
1201
|
+
description: "Organizations with optional member emails (memberships are created as active).",
|
|
1202
|
+
example: [{ name: "Acme", members: ["admin@example.com"] }]
|
|
1203
|
+
}
|
|
1204
|
+
],
|
|
1205
|
+
example: {
|
|
1206
|
+
users: [{ email: "admin@example.com", first_name: "Admin", last_name: "User" }],
|
|
1207
|
+
organizations: [{ name: "Acme", members: ["admin@example.com"] }]
|
|
1208
|
+
}
|
|
1209
|
+
},
|
|
1210
|
+
stateModel: {
|
|
1211
|
+
description: "Entities mutated by WorkOS provider calls.",
|
|
1212
|
+
collections: [
|
|
1213
|
+
{ name: "workos.users" },
|
|
1214
|
+
{ name: "workos.organizations" },
|
|
1215
|
+
{ name: "workos.memberships" },
|
|
1216
|
+
{ name: "workos.invitations" },
|
|
1217
|
+
{ name: "workos.api_keys" },
|
|
1218
|
+
{ name: "workos.sessions" },
|
|
1219
|
+
{ name: "workos.vault_objects" },
|
|
1220
|
+
{ name: "workos.oauth_clients" }
|
|
1221
|
+
]
|
|
1222
|
+
},
|
|
1223
|
+
connections: [
|
|
1224
|
+
{
|
|
1225
|
+
id: "workos-node",
|
|
1226
|
+
title: "WorkOS Node SDK",
|
|
1227
|
+
kind: "sdk",
|
|
1228
|
+
language: "typescript",
|
|
1229
|
+
description: "Point @workos-inc/node at the emulator via apiHostname \u2014 sealed sessions, JWKS, everything follows.",
|
|
1230
|
+
template: 'import { WorkOS } from "@workos-inc/node";\n\nconst url = new URL("{{baseUrl}}");\nconst workos = new WorkOS("{{token}}", {\n clientId: "client_emulate",\n apiHostname: url.hostname,\n port: Number(url.port),\n https: url.protocol === "https:",\n});'
|
|
1231
|
+
}
|
|
1232
|
+
]
|
|
1233
|
+
};
|
|
1234
|
+
function seedFromConfig(store, _baseUrl, config) {
|
|
1235
|
+
const ws = getWorkosStore(store);
|
|
1236
|
+
for (const user of config.users ?? []) {
|
|
1237
|
+
const created = ensureUserByEmail(ws, user.email);
|
|
1238
|
+
if (user.first_name || user.last_name) {
|
|
1239
|
+
ws.users.update(created.id, {
|
|
1240
|
+
first_name: user.first_name ?? created.first_name,
|
|
1241
|
+
last_name: user.last_name ?? created.last_name
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
for (const organization of config.organizations ?? []) {
|
|
1246
|
+
const org = ws.organizations.insert({
|
|
1247
|
+
workos_id: workosId("org"),
|
|
1248
|
+
name: organization.name,
|
|
1249
|
+
external_id: null
|
|
1250
|
+
});
|
|
1251
|
+
for (const email of organization.members ?? []) {
|
|
1252
|
+
const member = ensureUserByEmail(ws, email);
|
|
1253
|
+
ws.memberships.insert({
|
|
1254
|
+
workos_id: workosId("om"),
|
|
1255
|
+
user_id: member.workos_id,
|
|
1256
|
+
organization_id: org.workos_id,
|
|
1257
|
+
status: "active",
|
|
1258
|
+
role_slug: "admin"
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
var workosPlugin = {
|
|
1264
|
+
name: "workos",
|
|
1265
|
+
register(app, store, webhooks, baseUrl, tokenMap) {
|
|
1266
|
+
const ctx = { app, store, webhooks, baseUrl, tokenMap };
|
|
1267
|
+
oauthRoutes(ctx);
|
|
1268
|
+
userManagementRoutes(ctx);
|
|
1269
|
+
organizationRoutes(ctx);
|
|
1270
|
+
apiKeyRoutes(ctx);
|
|
1271
|
+
vaultRoutes(ctx);
|
|
1272
|
+
},
|
|
1273
|
+
seed(_store, _baseUrl) {
|
|
1274
|
+
}
|
|
1275
|
+
};
|
|
1276
|
+
var index_default = workosPlugin;
|
|
1277
|
+
export {
|
|
1278
|
+
index_default as default,
|
|
1279
|
+
getWorkosStore,
|
|
1280
|
+
manifest,
|
|
1281
|
+
seedFromConfig,
|
|
1282
|
+
workosPlugin
|
|
1283
|
+
};
|
|
1284
|
+
//# sourceMappingURL=dist-IYZPDKJW.js.map
|