@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,2334 @@
1
+ import {
2
+ SignJWT,
3
+ exportJWK,
4
+ generateKeyPair
5
+ } from "./chunk-D6EKRYGP.js";
6
+
7
+ // ../@emulators/okta/dist/index.js
8
+ import { randomUUID } from "crypto";
9
+ import { timingSafeEqual } from "crypto";
10
+ import { createHash, randomBytes } from "crypto";
11
+ var ORG_AUTH_SERVER_ID = "org";
12
+ var DEFAULT_AUTH_SERVER_ID = "default";
13
+ var DEFAULT_AUDIENCE = "api://default";
14
+ var DEFAULT_EVERYONE_GROUP_NAME = "Everyone";
15
+ var DEFAULT_EVERYONE_GROUP_ID = "00g_everyone";
16
+ function nowIso() {
17
+ return (/* @__PURE__ */ new Date()).toISOString();
18
+ }
19
+ function generateOktaId(prefix) {
20
+ const compact = randomUUID().replace(/-/g, "");
21
+ return `${prefix}${compact.slice(0, 17)}`;
22
+ }
23
+ function normalizeStatus(status, fallback) {
24
+ if (status === "STAGED" || status === "PROVISIONED" || status === "ACTIVE" || status === "SUSPENDED" || status === "DEPROVISIONED") {
25
+ return status;
26
+ }
27
+ return fallback;
28
+ }
29
+ function normalizeAppStatus(status, fallback) {
30
+ if (status === "ACTIVE" || status === "INACTIVE") return status;
31
+ return fallback;
32
+ }
33
+ function normalizeAuthServerStatus(status, fallback) {
34
+ if (status === "ACTIVE" || status === "INACTIVE") return status;
35
+ return fallback;
36
+ }
37
+ function normalizeGroupType(type, fallback) {
38
+ if (type === "OKTA_GROUP" || type === "BUILT_IN") return type;
39
+ return fallback;
40
+ }
41
+ function boolFromQuery(value, fallback) {
42
+ if (value == null) return fallback;
43
+ const lowered = value.toLowerCase();
44
+ if (lowered === "true" || lowered === "1") return true;
45
+ if (lowered === "false" || lowered === "0") return false;
46
+ return fallback;
47
+ }
48
+ function resolveOktaIssuer(baseUrl, authServerId) {
49
+ if (authServerId === ORG_AUTH_SERVER_ID) return baseUrl;
50
+ return `${baseUrl}/oauth2/${authServerId}`;
51
+ }
52
+ function userDisplayName(user) {
53
+ if (user.display_name) return user.display_name;
54
+ const combined = `${user.first_name} ${user.last_name}`.trim();
55
+ return combined || user.login;
56
+ }
57
+ function createDefaultUser() {
58
+ const now = nowIso();
59
+ return {
60
+ okta_id: generateOktaId("00u"),
61
+ status: "ACTIVE",
62
+ activated_at: now,
63
+ status_changed_at: now,
64
+ last_login_at: null,
65
+ password_changed_at: null,
66
+ transitioning_to_status: null,
67
+ login: "testuser@okta.local",
68
+ email: "testuser@okta.local",
69
+ first_name: "Test",
70
+ last_name: "User",
71
+ display_name: "Test User",
72
+ locale: "en-US",
73
+ time_zone: "UTC"
74
+ };
75
+ }
76
+ function createDefaultGroup() {
77
+ return {
78
+ okta_id: DEFAULT_EVERYONE_GROUP_ID,
79
+ type: "BUILT_IN",
80
+ name: DEFAULT_EVERYONE_GROUP_NAME,
81
+ description: "All users in the organization"
82
+ };
83
+ }
84
+ function createDefaultAuthorizationServer() {
85
+ return {
86
+ server_id: DEFAULT_AUTH_SERVER_ID,
87
+ name: "default",
88
+ description: "Default custom authorization server",
89
+ audiences: [DEFAULT_AUDIENCE],
90
+ status: "ACTIVE"
91
+ };
92
+ }
93
+ function createDefaultApp() {
94
+ return {
95
+ okta_id: generateOktaId("0oa"),
96
+ name: "oidc_client",
97
+ label: "Sample OIDC App",
98
+ status: "ACTIVE",
99
+ sign_on_mode: "OPENID_CONNECT",
100
+ settings: {
101
+ oauthClient: {
102
+ redirect_uris: ["http://localhost:3000/callback"]
103
+ }
104
+ },
105
+ credentials: {}
106
+ };
107
+ }
108
+ function createErrorHandler(documentationUrl) {
109
+ return async (c, next) => {
110
+ if (documentationUrl) {
111
+ c.set("docsUrl", documentationUrl);
112
+ }
113
+ await next();
114
+ };
115
+ }
116
+ var errorHandler = createErrorHandler();
117
+ var isDebug = typeof process !== "undefined" && (process.env.DEBUG === "1" || process.env.DEBUG === "true" || process.env.EMULATE_DEBUG === "1");
118
+ function debug(label, ...args) {
119
+ if (isDebug) {
120
+ console.log(`[${label}]`, ...args);
121
+ }
122
+ }
123
+ function escapeHtml(s) {
124
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
125
+ }
126
+ function escapeAttr(s) {
127
+ return escapeHtml(s).replace(/'/g, "&#39;");
128
+ }
129
+ var CSS = `
130
+ @font-face{
131
+ font-family:'Geist';font-style:normal;font-weight:100 900;font-display:swap;
132
+ src:url('/_emulate/fonts/geist-sans.woff2') format('woff2');
133
+ }
134
+ @font-face{
135
+ font-family:'Geist Pixel';font-style:normal;font-weight:400;font-display:swap;
136
+ src:url('/_emulate/fonts/GeistPixel-Square.woff2') format('woff2');
137
+ }
138
+ *{box-sizing:border-box;margin:0;padding:0}
139
+ body{
140
+ font-family:'Geist',-apple-system,BlinkMacSystemFont,sans-serif;
141
+ background:#000;color:#33ff00;min-height:100vh;
142
+ -webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;
143
+ }
144
+ .emu-bar{
145
+ border-bottom:1px solid #0a3300;padding:10px 20px;
146
+ display:flex;align-items:center;gap:10px;font-size:.8125rem;color:#1a8c00;
147
+ }
148
+ .emu-bar-title{font-weight:600;color:#33ff00;font-family:'Geist Pixel',monospace;}
149
+ .emu-bar-links{margin-left:auto;display:flex;gap:16px;}
150
+ .emu-bar-links a{
151
+ color:#1a8c00;font-size:.75rem;text-decoration:none;transition:color .15s;
152
+ }
153
+ .emu-bar-links a:hover{color:#33ff00;}
154
+ .emu-bar-links a .full{display:inline;}
155
+ .emu-bar-links a .short{display:none;}
156
+ @media(max-width:600px){
157
+ .emu-bar-links a .full{display:none;}
158
+ .emu-bar-links a .short{display:inline;}
159
+ }
160
+
161
+ .content{
162
+ display:flex;align-items:center;justify-content:center;
163
+ min-height:calc(100vh - 42px);padding:24px 16px;
164
+ }
165
+ .content-inner{width:100%;max-width:420px;}
166
+ .card-title{
167
+ font-family:'Geist Pixel',monospace;
168
+ font-size:1.125rem;font-weight:600;margin-bottom:4px;color:#33ff00;
169
+ }
170
+ .card-subtitle{color:#1a8c00;font-size:.8125rem;margin-bottom:18px;line-height:1.45;}
171
+ .powered-by{
172
+ position:fixed;bottom:0;left:0;right:0;
173
+ text-align:center;padding:12px;font-size:.6875rem;color:#0a3300;
174
+ font-family:'Geist Pixel',monospace;
175
+ }
176
+ .powered-by a{color:#1a8c00;text-decoration:none;transition:color .15s;}
177
+ .powered-by a:hover{color:#33ff00;}
178
+
179
+ .error-title{
180
+ font-family:'Geist Pixel',monospace;
181
+ color:#ff4444;font-size:1.125rem;font-weight:600;margin-bottom:8px;
182
+ }
183
+ .error-msg{color:#1a8c00;font-size:.875rem;line-height:1.5;}
184
+ .error-card{text-align:center;}
185
+
186
+ .user-form{margin-bottom:8px;}
187
+ .user-form:last-of-type{margin-bottom:0;}
188
+ .user-btn{
189
+ width:100%;display:flex;align-items:center;gap:12px;
190
+ padding:10px 12px;border:1px solid #0a3300;border-radius:8px;
191
+ background:#000;color:inherit;cursor:pointer;text-align:left;
192
+ font:inherit;transition:border-color .15s;
193
+ }
194
+ .user-btn:hover{border-color:#33ff00;}
195
+ .avatar{
196
+ width:36px;height:36px;border-radius:50%;
197
+ background:#0a3300;color:#33ff00;font-weight:600;font-size:.875rem;
198
+ display:flex;align-items:center;justify-content:center;flex-shrink:0;
199
+ font-family:'Geist Pixel',monospace;
200
+ }
201
+ .user-text{min-width:0;}
202
+ .user-login{font-weight:600;font-size:.875rem;display:block;color:#33ff00;}
203
+ .user-meta{color:#1a8c00;font-size:.75rem;margin-top:1px;}
204
+ .user-email{font-size:.6875rem;color:#116600;word-break:break-all;margin-top:1px;}
205
+
206
+ .settings-layout{
207
+ max-width:920px;margin:0 auto;padding:28px 20px;
208
+ display:flex;gap:28px;
209
+ }
210
+ .settings-sidebar{width:200px;flex-shrink:0;}
211
+ .settings-sidebar a{
212
+ display:block;padding:6px 10px;border-radius:6px;color:#1a8c00;
213
+ text-decoration:none;font-size:.8125rem;transition:color .15s;
214
+ }
215
+ .settings-sidebar a:hover{color:#33ff00;}
216
+ .settings-sidebar a.active{color:#33ff00;font-weight:600;}
217
+ .settings-main{flex:1;min-width:0;}
218
+
219
+ .s-card{
220
+ padding:18px 0;margin-bottom:14px;border-bottom:1px solid #0a3300;
221
+ }
222
+ .s-card:last-child{border-bottom:none;}
223
+ .s-card-header{display:flex;align-items:center;gap:14px;margin-bottom:14px;}
224
+ .s-icon{
225
+ width:42px;height:42px;border-radius:8px;
226
+ background:#0a3300;display:flex;align-items:center;justify-content:center;
227
+ font-size:1.125rem;font-weight:700;color:#116600;flex-shrink:0;
228
+ font-family:'Geist Pixel',monospace;
229
+ }
230
+ .s-title{
231
+ font-family:'Geist Pixel',monospace;
232
+ font-size:1.25rem;font-weight:600;color:#33ff00;
233
+ }
234
+ .s-subtitle{font-size:.75rem;color:#1a8c00;margin-top:2px;}
235
+ .section-heading{
236
+ font-size:.9375rem;font-weight:600;margin-bottom:10px;color:#33ff00;
237
+ display:flex;align-items:center;justify-content:space-between;
238
+ }
239
+ .perm-list{list-style:none;}
240
+ .perm-list li{padding:5px 0;font-size:.8125rem;display:flex;align-items:center;gap:6px;color:#1a8c00;}
241
+ .check{color:#33ff00;}
242
+ .org-row{
243
+ display:flex;align-items:center;gap:8px;padding:7px 0;
244
+ border-bottom:1px solid #0a3300;font-size:.8125rem;
245
+ }
246
+ .org-row:last-child{border-bottom:none;}
247
+ .org-icon{
248
+ width:22px;height:22px;border-radius:4px;background:#0a3300;
249
+ display:flex;align-items:center;justify-content:center;
250
+ font-size:.625rem;font-weight:700;color:#116600;flex-shrink:0;
251
+ font-family:'Geist Pixel',monospace;
252
+ }
253
+ .org-name{font-weight:600;color:#33ff00;}
254
+ .badge{font-size:.6875rem;padding:1px 7px;border-radius:999px;font-weight:500;}
255
+ .badge-granted{background:#0a3300;color:#33ff00;}
256
+ .badge-denied{background:#1a0a0a;color:#ff4444;}
257
+ .badge-requested{background:#0a3300;color:#1a8c00;}
258
+ .btn-revoke{
259
+ display:inline-block;padding:5px 14px;border-radius:6px;
260
+ border:1px solid #0a3300;background:transparent;color:#ff4444;
261
+ font-size:.75rem;font-weight:600;cursor:pointer;transition:border-color .15s;
262
+ }
263
+ .btn-revoke:hover{border-color:#ff4444;}
264
+ .info-text{color:#1a8c00;font-size:.75rem;line-height:1.5;margin-top:10px;}
265
+ .info-text a,.section-heading a{color:#1a8c00;text-decoration:none;transition:color .15s;}
266
+ .info-text a:hover,.section-heading a:hover{color:#33ff00;}
267
+ code{font-family:'Geist Mono','SF Mono',ui-monospace,monospace;font-size:.8125rem;color:#33ff00;word-break:break-all;}
268
+ .code-block{
269
+ background:#020;border:1px solid #0a3300;border-radius:6px;padding:10px 12px;
270
+ margin:8px 0 12px;overflow-x:auto;
271
+ }
272
+ .code-block code{white-space:pre;word-break:normal;display:block;line-height:1.5;}
273
+ .app-link{
274
+ display:flex;align-items:center;gap:12px;padding:12px;
275
+ border:1px solid #0a3300;border-radius:8px;background:#000;
276
+ text-decoration:none;color:inherit;margin-bottom:8px;transition:border-color .15s;
277
+ }
278
+ .app-link:hover{border-color:#33ff00;}
279
+ .app-link-name{font-weight:600;font-size:.875rem;color:#33ff00;}
280
+ .app-link-scopes{font-size:.6875rem;color:#1a8c00;margin-top:1px;}
281
+ .empty{color:#1a8c00;text-align:center;padding:28px 0;font-size:.875rem;}
282
+
283
+ .inspector-layout{max-width:960px;margin:0 auto;padding:28px 20px;}
284
+ .inspector-tabs{display:flex;gap:4px;margin-bottom:20px;}
285
+ .inspector-tabs a{
286
+ padding:7px 16px;border-radius:6px;text-decoration:none;
287
+ font-size:.8125rem;color:#1a8c00;border:1px solid transparent;
288
+ transition:color .15s,border-color .15s;
289
+ }
290
+ .inspector-tabs a:hover{color:#33ff00;}
291
+ .inspector-tabs a.active{color:#33ff00;font-weight:600;border-color:#0a3300;background:#0a3300;}
292
+ .inspector-section{margin-bottom:24px;}
293
+ .inspector-section h2{
294
+ font-family:'Geist Pixel',monospace;
295
+ font-size:1rem;font-weight:600;color:#33ff00;margin-bottom:10px;
296
+ }
297
+ .inspector-section h3{
298
+ font-family:'Geist Pixel',monospace;
299
+ font-size:.875rem;font-weight:600;color:#1a8c00;margin:16px 0 8px;
300
+ }
301
+ .inspector-table{width:100%;border-collapse:collapse;margin-bottom:12px;}
302
+ .inspector-table th,.inspector-table td{
303
+ text-align:left;padding:8px 12px;border-bottom:1px solid #0a3300;
304
+ font-size:.8125rem;
305
+ }
306
+ .inspector-table th{color:#1a8c00;font-weight:600;font-size:.75rem;text-transform:uppercase;letter-spacing:.04em;}
307
+ .inspector-table td{color:#33ff00;}
308
+ .inspector-table tbody tr{transition:background .1s;}
309
+ .inspector-table tbody tr:hover{background:#0a3300;}
310
+ .inspector-empty{color:#1a8c00;text-align:center;padding:20px 0;font-size:.8125rem;}
311
+
312
+ .checkout-layout{
313
+ display:flex;min-height:calc(100vh - 42px);
314
+ }
315
+ .checkout-summary{
316
+ flex:1;background:#020;padding:48px 40px 48px 10%;
317
+ display:flex;flex-direction:column;justify-content:center;
318
+ border-right:1px solid #0a3300;
319
+ }
320
+ .checkout-form-side{
321
+ flex:1;background:#000;padding:48px 10% 48px 40px;
322
+ display:flex;flex-direction:column;justify-content:center;
323
+ }
324
+ .checkout-merchant{
325
+ display:flex;align-items:center;gap:10px;margin-bottom:6px;
326
+ }
327
+ .checkout-merchant-name{
328
+ font-family:'Geist Pixel',monospace;
329
+ font-size:.9375rem;font-weight:600;color:#33ff00;
330
+ }
331
+ .checkout-test-badge{
332
+ font-size:.625rem;font-weight:700;letter-spacing:.04em;text-transform:uppercase;
333
+ background:#0a3300;color:#1a8c00;padding:2px 8px;border-radius:4px;
334
+ }
335
+ .checkout-total{
336
+ font-family:'Geist Pixel',monospace;
337
+ font-size:2rem;font-weight:700;color:#33ff00;margin:8px 0 28px;
338
+ }
339
+ .checkout-line-item{
340
+ display:flex;align-items:center;gap:14px;padding:14px 0;
341
+ border-bottom:1px solid #0a3300;
342
+ }
343
+ .checkout-line-item:first-child{border-top:1px solid #0a3300;}
344
+ .checkout-item-icon{
345
+ width:42px;height:42px;border-radius:6px;background:#0a3300;
346
+ display:flex;align-items:center;justify-content:center;flex-shrink:0;
347
+ font-family:'Geist Pixel',monospace;font-size:.875rem;font-weight:700;color:#116600;
348
+ }
349
+ .checkout-item-details{flex:1;min-width:0;}
350
+ .checkout-item-name{font-size:.875rem;font-weight:600;color:#33ff00;}
351
+ .checkout-item-qty{font-size:.75rem;color:#1a8c00;margin-top:2px;}
352
+ .checkout-item-price{
353
+ font-size:.875rem;font-weight:600;color:#33ff00;text-align:right;white-space:nowrap;
354
+ }
355
+ .checkout-item-unit{font-size:.6875rem;color:#1a8c00;text-align:right;margin-top:2px;}
356
+ .checkout-totals{margin-top:20px;}
357
+ .checkout-totals-row{
358
+ display:flex;justify-content:space-between;padding:6px 0;
359
+ font-size:.8125rem;color:#1a8c00;
360
+ }
361
+ .checkout-totals-row.total{
362
+ border-top:1px solid #0a3300;margin-top:8px;padding-top:14px;
363
+ font-size:.9375rem;font-weight:600;color:#33ff00;
364
+ }
365
+ .checkout-form-section{margin-bottom:24px;}
366
+ .checkout-form-label{
367
+ font-size:.8125rem;font-weight:600;color:#33ff00;margin-bottom:8px;display:block;
368
+ }
369
+ .checkout-input{
370
+ width:100%;padding:10px 12px;border:1px solid #0a3300;border-radius:6px;
371
+ background:#020;color:#33ff00;font:inherit;font-size:.875rem;
372
+ transition:border-color .15s;outline:none;
373
+ }
374
+ .checkout-input:focus{border-color:#33ff00;}
375
+ .checkout-input::placeholder{color:#116600;}
376
+ .checkout-card-box{
377
+ border:1px solid #0a3300;border-radius:6px;padding:14px;
378
+ background:#020;
379
+ }
380
+ .checkout-card-row{
381
+ display:flex;gap:12px;margin-top:10px;
382
+ }
383
+ .checkout-card-row .checkout-input{flex:1;}
384
+ .checkout-sim-note{
385
+ font-size:.6875rem;color:#1a8c00;margin-top:10px;text-align:center;
386
+ font-style:italic;
387
+ }
388
+ .checkout-pay-btn{
389
+ width:100%;padding:14px;border:none;border-radius:8px;
390
+ background:#33ff00;color:#000;font:inherit;font-size:.9375rem;font-weight:700;
391
+ cursor:pointer;transition:background .15s;
392
+ font-family:'Geist Pixel',monospace;
393
+ }
394
+ .checkout-pay-btn:hover{background:#44ff22;}
395
+ .checkout-cancel{
396
+ text-align:center;margin-top:14px;
397
+ }
398
+ .checkout-cancel a{
399
+ color:#1a8c00;text-decoration:none;font-size:.8125rem;
400
+ transition:color .15s;
401
+ }
402
+ .checkout-cancel a:hover{color:#33ff00;}
403
+ @media(max-width:768px){
404
+ .checkout-layout{flex-direction:column;}
405
+ .checkout-summary{padding:32px 20px;border-right:none;border-bottom:1px solid #0a3300;}
406
+ .checkout-form-side{padding:32px 20px;}
407
+ }
408
+ `;
409
+ var POWERED_BY = `<div class="powered-by">Powered by <a href="https://emulate.dev" target="_blank" rel="noopener">emulate</a></div>`;
410
+ function emuBar(service) {
411
+ const title = service ? `${escapeHtml(service)} Emulator` : "Emulator";
412
+ return `<div class="emu-bar">
413
+ <span class="emu-bar-title">${title}</span>
414
+ <nav class="emu-bar-links">
415
+ <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>
416
+ <a href="https://github.com/vercel-labs/emulate" target="_blank" rel="noopener"><span class="full">Source Code</span><span class="short">Source</span></a>
417
+ <a href="https://emulate.dev" target="_blank" rel="noopener"><span class="full">Learn More</span><span class="short">Learn</span></a>
418
+ </nav>
419
+ </div>`;
420
+ }
421
+ function head(title) {
422
+ return `<!DOCTYPE html>
423
+ <html lang="en">
424
+ <head>
425
+ <meta charset="utf-8"/>
426
+ <meta name="viewport" content="width=device-width,initial-scale=1"/>
427
+ <link rel="icon" href="/_emulate/favicon.ico"/>
428
+ <title>${escapeHtml(title)} | emulate</title>
429
+ <style>${CSS}</style>
430
+ </head>`;
431
+ }
432
+ function renderCardPage(title, subtitle, body, service) {
433
+ return `${head(title)}
434
+ <body>
435
+ ${emuBar(service)}
436
+ <div class="content">
437
+ <div class="content-inner">
438
+ <div class="card-title">${escapeHtml(title)}</div>
439
+ <div class="card-subtitle">${subtitle}</div>
440
+ ${body}
441
+ </div>
442
+ </div>
443
+ ${POWERED_BY}
444
+ </body></html>`;
445
+ }
446
+ function renderErrorPage(title, message, service) {
447
+ return `${head(title)}
448
+ <body>
449
+ ${emuBar(service)}
450
+ <div class="content">
451
+ <div class="content-inner error-card">
452
+ <div class="error-title">${escapeHtml(title)}</div>
453
+ <div class="error-msg">${escapeHtml(message)}</div>
454
+ </div>
455
+ </div>
456
+ ${POWERED_BY}
457
+ </body></html>`;
458
+ }
459
+ function renderFormPostPage(action, fields, service) {
460
+ const hiddens = Object.entries(fields).filter(([, v]) => v != null).map(([k, v]) => `<input type="hidden" name="${escapeAttr(k)}" value="${escapeAttr(v)}"/>`).join("\n");
461
+ return `${head("Redirecting")}
462
+ <body onload="document.forms[0].submit()">
463
+ ${emuBar(service)}
464
+ <div class="content">
465
+ <div class="content-inner" style="text-align:center">
466
+ <div class="card-subtitle">Redirecting&hellip;</div>
467
+ <form method="POST" action="${escapeAttr(action)}">
468
+ ${hiddens}
469
+ <noscript><button type="submit" class="user-btn" style="margin-top:12px;justify-content:center">
470
+ <span class="user-login">Continue</span>
471
+ </button></noscript>
472
+ </form>
473
+ </div>
474
+ </div>
475
+ ${POWERED_BY}
476
+ </body></html>`;
477
+ }
478
+ function renderUserButton(opts) {
479
+ const hiddens = Object.entries(opts.hiddenFields).map(([k, v]) => `<input type="hidden" name="${escapeAttr(k)}" value="${escapeAttr(v)}"/>`).join("");
480
+ const nameLine = opts.name ? `<div class="user-meta">${escapeHtml(opts.name)}</div>` : "";
481
+ const emailLine = opts.email ? `<div class="user-email">${escapeHtml(opts.email)}</div>` : "";
482
+ return `<form class="user-form" method="post" action="${escapeAttr(opts.formAction)}">
483
+ ${hiddens}
484
+ <button type="submit" class="user-btn">
485
+ <span class="avatar">${escapeHtml(opts.letter)}</span>
486
+ <span class="user-text">
487
+ <span class="user-login">${escapeHtml(opts.login)}</span>
488
+ ${nameLine}${emailLine}
489
+ </span>
490
+ </button>
491
+ </form>`;
492
+ }
493
+ function parsePagination(c) {
494
+ const page = Math.max(1, parseInt(c.req.query("page") ?? "1", 10) || 1);
495
+ const per_page = Math.min(100, Math.max(1, parseInt(c.req.query("per_page") ?? "30", 10) || 30));
496
+ return { page, per_page };
497
+ }
498
+ function setLinkHeader(c, totalCount, page, perPage) {
499
+ const lastPage = Math.max(1, Math.ceil(totalCount / perPage));
500
+ const baseUrl = new URL(c.req.url);
501
+ const links = [];
502
+ const makeLink = (p, rel) => {
503
+ baseUrl.searchParams.set("page", String(p));
504
+ baseUrl.searchParams.set("per_page", String(perPage));
505
+ return `<${baseUrl.toString()}>; rel="${rel}"`;
506
+ };
507
+ if (page < lastPage) {
508
+ links.push(makeLink(page + 1, "next"));
509
+ links.push(makeLink(lastPage, "last"));
510
+ }
511
+ if (page > 1) {
512
+ links.push(makeLink(1, "first"));
513
+ links.push(makeLink(page - 1, "prev"));
514
+ }
515
+ if (links.length > 0) {
516
+ c.header("Link", links.join(", "));
517
+ }
518
+ }
519
+ function normalizeUri(uri) {
520
+ try {
521
+ const u = new URL(uri);
522
+ return `${u.origin}${u.pathname.replace(/\/+$/, "")}`;
523
+ } catch {
524
+ return uri.replace(/\/+$/, "").split("?")[0];
525
+ }
526
+ }
527
+ function matchesRedirectUri(incoming, registered) {
528
+ const normalized = normalizeUri(incoming);
529
+ return registered.some((r) => normalizeUri(r) === normalized);
530
+ }
531
+ function constantTimeSecretEqual(a, b) {
532
+ const bufA = Buffer.from(a, "utf-8");
533
+ const bufB = Buffer.from(b, "utf-8");
534
+ if (bufA.length !== bufB.length) return false;
535
+ return timingSafeEqual(bufA, bufB);
536
+ }
537
+ function bodyStr(v) {
538
+ if (typeof v === "string") return v;
539
+ if (Array.isArray(v) && typeof v[0] === "string") return v[0];
540
+ return "";
541
+ }
542
+ function createErrorBody(status, errorCode, errorSummary, errorCauses = []) {
543
+ return {
544
+ errorCode,
545
+ errorSummary,
546
+ errorLink: errorCode,
547
+ errorId: `${errorCode}-${Date.now()}`,
548
+ errorCauses,
549
+ status
550
+ };
551
+ }
552
+ function oktaError(c, status, errorCode, errorSummary, errorCauses = []) {
553
+ const body = createErrorBody(status, errorCode, errorSummary, errorCauses);
554
+ return c.json(body, status);
555
+ }
556
+ async function readJsonObject(c) {
557
+ try {
558
+ const body = await c.req.json();
559
+ if (body && typeof body === "object") {
560
+ return body;
561
+ }
562
+ return {};
563
+ } catch {
564
+ return {};
565
+ }
566
+ }
567
+ function requireManagementAuth(c, tokenMap) {
568
+ const existing = c.get("authUser");
569
+ if (existing) return existing;
570
+ const authHeader = c.req.header("Authorization") ?? "";
571
+ if (authHeader.toLowerCase().startsWith("ssws ")) {
572
+ const token = authHeader.slice(5).trim();
573
+ const mapped = tokenMap?.get(token);
574
+ if (mapped) {
575
+ c.set("authUser", mapped);
576
+ c.set("authToken", token);
577
+ c.set("authScopes", mapped.scopes);
578
+ return mapped;
579
+ }
580
+ }
581
+ return oktaError(c, 401, "E0000004", "Authentication failed");
582
+ }
583
+ function findUserByRef(os, userRef) {
584
+ const decoded = decodeURIComponent(userRef);
585
+ return os.users.findOneBy("okta_id", decoded) ?? os.users.findOneBy("login", decoded) ?? os.users.findOneBy("email", decoded);
586
+ }
587
+ function findGroupByRef(os, groupRef) {
588
+ const decoded = decodeURIComponent(groupRef);
589
+ return os.groups.findOneBy("okta_id", decoded);
590
+ }
591
+ function findAppByRef(os, appRef) {
592
+ const decoded = decodeURIComponent(appRef);
593
+ return os.apps.findOneBy("okta_id", decoded);
594
+ }
595
+ function findAuthorizationServerByRef(os, serverRef) {
596
+ const decoded = decodeURIComponent(serverRef);
597
+ return os.authorizationServers.findOneBy("server_id", decoded);
598
+ }
599
+ function userResponse(baseUrl, user) {
600
+ return {
601
+ id: user.okta_id,
602
+ status: user.status,
603
+ created: user.created_at,
604
+ activated: user.activated_at,
605
+ statusChanged: user.status_changed_at,
606
+ lastLogin: user.last_login_at,
607
+ lastUpdated: user.updated_at,
608
+ passwordChanged: user.password_changed_at,
609
+ profile: {
610
+ login: user.login,
611
+ email: user.email,
612
+ firstName: user.first_name,
613
+ lastName: user.last_name,
614
+ displayName: userDisplayName(user),
615
+ locale: user.locale,
616
+ timeZone: user.time_zone
617
+ },
618
+ _links: {
619
+ self: {
620
+ href: `${baseUrl}/api/v1/users/${encodeURIComponent(user.okta_id)}`
621
+ }
622
+ }
623
+ };
624
+ }
625
+ function groupResponse(baseUrl, group) {
626
+ return {
627
+ id: group.okta_id,
628
+ created: group.created_at,
629
+ lastUpdated: group.updated_at,
630
+ lastMembershipUpdated: group.updated_at,
631
+ objectClass: ["okta:user_group"],
632
+ type: group.type,
633
+ profile: {
634
+ name: group.name,
635
+ description: group.description
636
+ },
637
+ _links: {
638
+ self: {
639
+ href: `${baseUrl}/api/v1/groups/${encodeURIComponent(group.okta_id)}`
640
+ }
641
+ }
642
+ };
643
+ }
644
+ function appResponse(baseUrl, app) {
645
+ return {
646
+ id: app.okta_id,
647
+ name: app.name,
648
+ label: app.label,
649
+ status: app.status,
650
+ created: app.created_at,
651
+ lastUpdated: app.updated_at,
652
+ signOnMode: app.sign_on_mode,
653
+ credentials: app.credentials,
654
+ settings: app.settings,
655
+ _links: {
656
+ self: {
657
+ href: `${baseUrl}/api/v1/apps/${encodeURIComponent(app.okta_id)}`
658
+ }
659
+ }
660
+ };
661
+ }
662
+ function authorizationServerResponse(baseUrl, server) {
663
+ return {
664
+ id: server.server_id,
665
+ name: server.name,
666
+ description: server.description,
667
+ audiences: server.audiences,
668
+ issuer: resolveOktaIssuer(baseUrl, server.server_id),
669
+ status: server.status,
670
+ created: server.created_at,
671
+ lastUpdated: server.updated_at,
672
+ _links: {
673
+ self: {
674
+ href: `${baseUrl}/api/v1/authorizationServers/${encodeURIComponent(server.server_id)}`
675
+ }
676
+ }
677
+ };
678
+ }
679
+ function getOktaStore(store) {
680
+ return {
681
+ users: store.collection("okta.users", ["okta_id", "login", "email"]),
682
+ groups: store.collection("okta.groups", ["okta_id", "name"]),
683
+ apps: store.collection("okta.apps", ["okta_id", "name"]),
684
+ oauthClients: store.collection("okta.oauth_clients", ["client_id", "auth_server_id"]),
685
+ authorizationServers: store.collection("okta.auth_servers", ["server_id"]),
686
+ groupMemberships: store.collection("okta.group_memberships", [
687
+ "group_okta_id",
688
+ "user_okta_id"
689
+ ]),
690
+ appAssignments: store.collection("okta.app_assignments", ["app_okta_id", "user_okta_id"])
691
+ };
692
+ }
693
+ function appRoutes({ app, store, baseUrl, tokenMap }) {
694
+ const oktaStore = getOktaStore(store);
695
+ app.get("/api/v1/apps", (c) => {
696
+ const auth = requireManagementAuth(c, tokenMap);
697
+ if (auth instanceof Response) return auth;
698
+ const q = (c.req.query("q") ?? "").toLowerCase();
699
+ let apps = oktaStore.apps.all();
700
+ if (q) {
701
+ apps = apps.filter((entry) => `${entry.name} ${entry.label}`.toLowerCase().includes(q));
702
+ }
703
+ const { page, per_page } = parsePagination(c);
704
+ const total = apps.length;
705
+ const start = (page - 1) * per_page;
706
+ const paged = apps.slice(start, start + per_page);
707
+ setLinkHeader(c, total, page, per_page);
708
+ c.header("X-Total-Count", String(total));
709
+ return c.json(paged.map((entry) => appResponse(baseUrl, entry)));
710
+ });
711
+ app.post("/api/v1/apps", async (c) => {
712
+ const auth = requireManagementAuth(c, tokenMap);
713
+ if (auth instanceof Response) return auth;
714
+ const body = await readJsonObject(c);
715
+ const name = typeof body.name === "string" ? body.name : "oidc_client";
716
+ const label = typeof body.label === "string" ? body.label : "Okta App";
717
+ const signOnMode = typeof body.signOnMode === "string" ? body.signOnMode : "OPENID_CONNECT";
718
+ const settings = body.settings && typeof body.settings === "object" ? body.settings : {};
719
+ const credentials = body.credentials && typeof body.credentials === "object" ? body.credentials : {};
720
+ const created = oktaStore.apps.insert({
721
+ okta_id: generateOktaId("0oa"),
722
+ name,
723
+ label,
724
+ status: normalizeAppStatus(typeof body.status === "string" ? body.status : void 0, "ACTIVE"),
725
+ sign_on_mode: signOnMode,
726
+ settings,
727
+ credentials
728
+ });
729
+ return c.json(appResponse(baseUrl, created), 201);
730
+ });
731
+ app.get("/api/v1/apps/:appId/users", (c) => {
732
+ const auth = requireManagementAuth(c, tokenMap);
733
+ if (auth instanceof Response) return auth;
734
+ const appEntity = findAppByRef(oktaStore, c.req.param("appId"));
735
+ if (!appEntity) return oktaError(c, 404, "E0000007", "Not found: app");
736
+ const assignments = oktaStore.appAssignments.findBy("app_okta_id", appEntity.okta_id);
737
+ const users = assignments.map((assignment) => oktaStore.users.findOneBy("okta_id", assignment.user_okta_id)).filter((user) => Boolean(user));
738
+ return c.json(
739
+ users.map((user) => ({
740
+ id: user.okta_id,
741
+ scope: "USER",
742
+ credentials: { userName: user.login },
743
+ profile: userResponse(baseUrl, user).profile
744
+ }))
745
+ );
746
+ });
747
+ app.put("/api/v1/apps/:appId/users/:userId", (c) => {
748
+ const auth = requireManagementAuth(c, tokenMap);
749
+ if (auth instanceof Response) return auth;
750
+ const appEntity = findAppByRef(oktaStore, c.req.param("appId"));
751
+ if (!appEntity) return oktaError(c, 404, "E0000007", "Not found: app");
752
+ const user = findUserByRef(oktaStore, c.req.param("userId"));
753
+ if (!user) return oktaError(c, 404, "E0000007", "Not found: user");
754
+ const existing = oktaStore.appAssignments.findBy("app_okta_id", appEntity.okta_id).find((assignment) => assignment.user_okta_id === user.okta_id);
755
+ if (!existing) {
756
+ oktaStore.appAssignments.insert({
757
+ app_okta_id: appEntity.okta_id,
758
+ user_okta_id: user.okta_id
759
+ });
760
+ }
761
+ return new Response(null, { status: 204 });
762
+ });
763
+ app.delete("/api/v1/apps/:appId/users/:userId", (c) => {
764
+ const auth = requireManagementAuth(c, tokenMap);
765
+ if (auth instanceof Response) return auth;
766
+ const appEntity = findAppByRef(oktaStore, c.req.param("appId"));
767
+ if (!appEntity) return oktaError(c, 404, "E0000007", "Not found: app");
768
+ const user = findUserByRef(oktaStore, c.req.param("userId"));
769
+ if (!user) return oktaError(c, 404, "E0000007", "Not found: user");
770
+ const existing = oktaStore.appAssignments.findBy("app_okta_id", appEntity.okta_id).find((assignment) => assignment.user_okta_id === user.okta_id);
771
+ if (existing) oktaStore.appAssignments.delete(existing.id);
772
+ return new Response(null, { status: 204 });
773
+ });
774
+ app.post("/api/v1/apps/:appId/lifecycle/activate", (c) => {
775
+ const auth = requireManagementAuth(c, tokenMap);
776
+ if (auth instanceof Response) return auth;
777
+ const appEntity = findAppByRef(oktaStore, c.req.param("appId"));
778
+ if (!appEntity) return oktaError(c, 404, "E0000007", "Not found: app");
779
+ const updated = oktaStore.apps.update(appEntity.id, { status: "ACTIVE" });
780
+ return c.json(appResponse(baseUrl, updated ?? appEntity));
781
+ });
782
+ app.post("/api/v1/apps/:appId/lifecycle/deactivate", (c) => {
783
+ const auth = requireManagementAuth(c, tokenMap);
784
+ if (auth instanceof Response) return auth;
785
+ const appEntity = findAppByRef(oktaStore, c.req.param("appId"));
786
+ if (!appEntity) return oktaError(c, 404, "E0000007", "Not found: app");
787
+ const updated = oktaStore.apps.update(appEntity.id, { status: "INACTIVE" });
788
+ return c.json(appResponse(baseUrl, updated ?? appEntity));
789
+ });
790
+ app.get("/api/v1/apps/:appId", (c) => {
791
+ const auth = requireManagementAuth(c, tokenMap);
792
+ if (auth instanceof Response) return auth;
793
+ const appEntity = findAppByRef(oktaStore, c.req.param("appId"));
794
+ if (!appEntity) return oktaError(c, 404, "E0000007", "Not found: app");
795
+ return c.json(appResponse(baseUrl, appEntity));
796
+ });
797
+ app.put("/api/v1/apps/:appId", async (c) => {
798
+ const auth = requireManagementAuth(c, tokenMap);
799
+ if (auth instanceof Response) return auth;
800
+ const appEntity = findAppByRef(oktaStore, c.req.param("appId"));
801
+ if (!appEntity) return oktaError(c, 404, "E0000007", "Not found: app");
802
+ const body = await readJsonObject(c);
803
+ const updated = oktaStore.apps.update(appEntity.id, {
804
+ name: typeof body.name === "string" ? body.name : appEntity.name,
805
+ label: typeof body.label === "string" ? body.label : appEntity.label,
806
+ status: normalizeAppStatus(typeof body.status === "string" ? body.status : void 0, appEntity.status),
807
+ sign_on_mode: typeof body.signOnMode === "string" ? body.signOnMode : appEntity.sign_on_mode,
808
+ settings: body.settings && typeof body.settings === "object" ? body.settings : appEntity.settings,
809
+ credentials: body.credentials && typeof body.credentials === "object" ? body.credentials : appEntity.credentials
810
+ });
811
+ return c.json(appResponse(baseUrl, updated ?? appEntity));
812
+ });
813
+ app.delete("/api/v1/apps/:appId", (c) => {
814
+ const auth = requireManagementAuth(c, tokenMap);
815
+ if (auth instanceof Response) return auth;
816
+ const appEntity = findAppByRef(oktaStore, c.req.param("appId"));
817
+ if (!appEntity) return oktaError(c, 404, "E0000007", "Not found: app");
818
+ if (appEntity.status !== "INACTIVE") {
819
+ return oktaError(c, 400, "E0000001", "App must be INACTIVE before deletion");
820
+ }
821
+ for (const assignment of oktaStore.appAssignments.findBy("app_okta_id", appEntity.okta_id)) {
822
+ oktaStore.appAssignments.delete(assignment.id);
823
+ }
824
+ oktaStore.apps.delete(appEntity.id);
825
+ return new Response(null, { status: 204 });
826
+ });
827
+ }
828
+ function normalizeServerId(name) {
829
+ const compact = name.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-");
830
+ if (compact.length > 0) return compact;
831
+ return generateOktaId("as");
832
+ }
833
+ function authorizationServerRoutes({ app, store, baseUrl, tokenMap }) {
834
+ const oktaStore = getOktaStore(store);
835
+ app.get("/api/v1/authorizationServers", (c) => {
836
+ const auth = requireManagementAuth(c, tokenMap);
837
+ if (auth instanceof Response) return auth;
838
+ const servers = oktaStore.authorizationServers.all();
839
+ const { page, per_page } = parsePagination(c);
840
+ const total = servers.length;
841
+ const start = (page - 1) * per_page;
842
+ const paged = servers.slice(start, start + per_page);
843
+ setLinkHeader(c, total, page, per_page);
844
+ c.header("X-Total-Count", String(total));
845
+ return c.json(paged.map((server) => authorizationServerResponse(baseUrl, server)));
846
+ });
847
+ app.post("/api/v1/authorizationServers", async (c) => {
848
+ const auth = requireManagementAuth(c, tokenMap);
849
+ if (auth instanceof Response) return auth;
850
+ const body = await readJsonObject(c);
851
+ const name = typeof body.name === "string" ? body.name.trim() : "";
852
+ if (!name) return oktaError(c, 400, "E0000001", "name is required");
853
+ const serverId = typeof body.id === "string" ? body.id : normalizeServerId(name);
854
+ if (oktaStore.authorizationServers.findOneBy("server_id", serverId)) {
855
+ return oktaError(c, 400, "E0000001", `Authorization server '${serverId}' already exists`);
856
+ }
857
+ const audiences = Array.isArray(body.audiences) ? body.audiences.filter((entry) => typeof entry === "string") : [DEFAULT_AUDIENCE];
858
+ const created = oktaStore.authorizationServers.insert({
859
+ server_id: serverId,
860
+ name,
861
+ description: typeof body.description === "string" ? body.description : "",
862
+ audiences: audiences.length > 0 ? audiences : [DEFAULT_AUDIENCE],
863
+ status: normalizeAuthServerStatus(typeof body.status === "string" ? body.status : void 0, "ACTIVE")
864
+ });
865
+ return c.json(authorizationServerResponse(baseUrl, created), 201);
866
+ });
867
+ app.post("/api/v1/authorizationServers/:authServerId/lifecycle/activate", (c) => {
868
+ const auth = requireManagementAuth(c, tokenMap);
869
+ if (auth instanceof Response) return auth;
870
+ const server = findAuthorizationServerByRef(oktaStore, c.req.param("authServerId"));
871
+ if (!server) return oktaError(c, 404, "E0000007", "Not found: authorization server");
872
+ const updated = oktaStore.authorizationServers.update(server.id, { status: "ACTIVE" });
873
+ return c.json(authorizationServerResponse(baseUrl, updated ?? server));
874
+ });
875
+ app.post("/api/v1/authorizationServers/:authServerId/lifecycle/deactivate", (c) => {
876
+ const auth = requireManagementAuth(c, tokenMap);
877
+ if (auth instanceof Response) return auth;
878
+ const server = findAuthorizationServerByRef(oktaStore, c.req.param("authServerId"));
879
+ if (!server) return oktaError(c, 404, "E0000007", "Not found: authorization server");
880
+ const updated = oktaStore.authorizationServers.update(server.id, { status: "INACTIVE" });
881
+ return c.json(authorizationServerResponse(baseUrl, updated ?? server));
882
+ });
883
+ app.get("/api/v1/authorizationServers/:authServerId", (c) => {
884
+ const auth = requireManagementAuth(c, tokenMap);
885
+ if (auth instanceof Response) return auth;
886
+ const server = findAuthorizationServerByRef(oktaStore, c.req.param("authServerId"));
887
+ if (!server) return oktaError(c, 404, "E0000007", "Not found: authorization server");
888
+ return c.json(authorizationServerResponse(baseUrl, server));
889
+ });
890
+ app.put("/api/v1/authorizationServers/:authServerId", async (c) => {
891
+ const auth = requireManagementAuth(c, tokenMap);
892
+ if (auth instanceof Response) return auth;
893
+ const server = findAuthorizationServerByRef(oktaStore, c.req.param("authServerId"));
894
+ if (!server) return oktaError(c, 404, "E0000007", "Not found: authorization server");
895
+ const body = await readJsonObject(c);
896
+ const audiences = Array.isArray(body.audiences) ? body.audiences.filter((entry) => typeof entry === "string") : server.audiences;
897
+ const updated = oktaStore.authorizationServers.update(server.id, {
898
+ name: typeof body.name === "string" ? body.name : server.name,
899
+ description: typeof body.description === "string" ? body.description : server.description,
900
+ audiences: audiences.length > 0 ? audiences : server.audiences,
901
+ status: normalizeAuthServerStatus(typeof body.status === "string" ? body.status : void 0, server.status)
902
+ });
903
+ return c.json(authorizationServerResponse(baseUrl, updated ?? server));
904
+ });
905
+ app.delete("/api/v1/authorizationServers/:authServerId", (c) => {
906
+ const auth = requireManagementAuth(c, tokenMap);
907
+ if (auth instanceof Response) return auth;
908
+ const server = findAuthorizationServerByRef(oktaStore, c.req.param("authServerId"));
909
+ if (!server) return oktaError(c, 404, "E0000007", "Not found: authorization server");
910
+ for (const client of oktaStore.oauthClients.findBy("auth_server_id", server.server_id)) {
911
+ oktaStore.oauthClients.delete(client.id);
912
+ }
913
+ oktaStore.authorizationServers.delete(server.id);
914
+ return new Response(null, { status: 204 });
915
+ });
916
+ }
917
+ function groupRoutes({ app, store, baseUrl, tokenMap }) {
918
+ const oktaStore = getOktaStore(store);
919
+ app.get("/api/v1/groups", (c) => {
920
+ const auth = requireManagementAuth(c, tokenMap);
921
+ if (auth instanceof Response) return auth;
922
+ const q = (c.req.query("q") ?? "").toLowerCase();
923
+ let groups = oktaStore.groups.all();
924
+ if (q) {
925
+ groups = groups.filter((group) => `${group.name} ${group.description ?? ""}`.toLowerCase().includes(q));
926
+ }
927
+ const { page, per_page } = parsePagination(c);
928
+ const total = groups.length;
929
+ const start = (page - 1) * per_page;
930
+ const paged = groups.slice(start, start + per_page);
931
+ setLinkHeader(c, total, page, per_page);
932
+ c.header("X-Total-Count", String(total));
933
+ return c.json(paged.map((group) => groupResponse(baseUrl, group)));
934
+ });
935
+ app.post("/api/v1/groups", async (c) => {
936
+ const auth = requireManagementAuth(c, tokenMap);
937
+ if (auth instanceof Response) return auth;
938
+ const body = await readJsonObject(c);
939
+ const profile = body.profile && typeof body.profile === "object" ? body.profile : {};
940
+ const name = typeof profile.name === "string" ? profile.name.trim() : "";
941
+ if (!name) {
942
+ return oktaError(c, 400, "E0000001", "profile.name is required");
943
+ }
944
+ if (oktaStore.groups.findOneBy("name", name)) {
945
+ return oktaError(c, 400, "E0000001", "A group with the same name already exists");
946
+ }
947
+ const created = oktaStore.groups.insert({
948
+ okta_id: generateOktaId("00g"),
949
+ type: normalizeGroupType(typeof body.type === "string" ? body.type : void 0, "OKTA_GROUP"),
950
+ name,
951
+ description: typeof profile.description === "string" ? profile.description : null
952
+ });
953
+ return c.json(groupResponse(baseUrl, created), 201);
954
+ });
955
+ app.get("/api/v1/groups/:groupId/users", (c) => {
956
+ const auth = requireManagementAuth(c, tokenMap);
957
+ if (auth instanceof Response) return auth;
958
+ const group = findGroupByRef(oktaStore, c.req.param("groupId"));
959
+ if (!group) return oktaError(c, 404, "E0000007", "Not found: group");
960
+ const memberships = oktaStore.groupMemberships.findBy("group_okta_id", group.okta_id);
961
+ const users = memberships.map((membership) => oktaStore.users.findOneBy("okta_id", membership.user_okta_id)).filter((user) => Boolean(user));
962
+ return c.json(users.map((user) => userResponse(baseUrl, user)));
963
+ });
964
+ app.put("/api/v1/groups/:groupId/users/:userId", (c) => {
965
+ const auth = requireManagementAuth(c, tokenMap);
966
+ if (auth instanceof Response) return auth;
967
+ const group = findGroupByRef(oktaStore, c.req.param("groupId"));
968
+ if (!group) return oktaError(c, 404, "E0000007", "Not found: group");
969
+ const user = findUserByRef(oktaStore, c.req.param("userId"));
970
+ if (!user) return oktaError(c, 404, "E0000007", "Not found: user");
971
+ const existing = oktaStore.groupMemberships.findBy("group_okta_id", group.okta_id).find((membership) => membership.user_okta_id === user.okta_id);
972
+ if (!existing) {
973
+ oktaStore.groupMemberships.insert({
974
+ group_okta_id: group.okta_id,
975
+ user_okta_id: user.okta_id
976
+ });
977
+ }
978
+ return new Response(null, { status: 204 });
979
+ });
980
+ app.delete("/api/v1/groups/:groupId/users/:userId", (c) => {
981
+ const auth = requireManagementAuth(c, tokenMap);
982
+ if (auth instanceof Response) return auth;
983
+ const group = findGroupByRef(oktaStore, c.req.param("groupId"));
984
+ if (!group) return oktaError(c, 404, "E0000007", "Not found: group");
985
+ const user = findUserByRef(oktaStore, c.req.param("userId"));
986
+ if (!user) return oktaError(c, 404, "E0000007", "Not found: user");
987
+ const existing = oktaStore.groupMemberships.findBy("group_okta_id", group.okta_id).find((membership) => membership.user_okta_id === user.okta_id);
988
+ if (existing) {
989
+ oktaStore.groupMemberships.delete(existing.id);
990
+ }
991
+ return new Response(null, { status: 204 });
992
+ });
993
+ app.get("/api/v1/groups/:groupId", (c) => {
994
+ const auth = requireManagementAuth(c, tokenMap);
995
+ if (auth instanceof Response) return auth;
996
+ const group = findGroupByRef(oktaStore, c.req.param("groupId"));
997
+ if (!group) return oktaError(c, 404, "E0000007", "Not found: group");
998
+ return c.json(groupResponse(baseUrl, group));
999
+ });
1000
+ app.put("/api/v1/groups/:groupId", async (c) => {
1001
+ const auth = requireManagementAuth(c, tokenMap);
1002
+ if (auth instanceof Response) return auth;
1003
+ const group = findGroupByRef(oktaStore, c.req.param("groupId"));
1004
+ if (!group) return oktaError(c, 404, "E0000007", "Not found: group");
1005
+ const body = await readJsonObject(c);
1006
+ const profile = body.profile && typeof body.profile === "object" ? body.profile : {};
1007
+ const nextName = typeof profile.name === "string" ? profile.name.trim() : group.name;
1008
+ if (nextName !== group.name) {
1009
+ const existing = oktaStore.groups.findOneBy("name", nextName);
1010
+ if (existing && existing.okta_id !== group.okta_id) {
1011
+ return oktaError(c, 400, "E0000001", "A group with the same name already exists");
1012
+ }
1013
+ }
1014
+ const updated = oktaStore.groups.update(group.id, {
1015
+ name: nextName,
1016
+ description: typeof profile.description === "string" ? profile.description : group.description,
1017
+ type: normalizeGroupType(typeof body.type === "string" ? body.type : void 0, group.type)
1018
+ });
1019
+ return c.json(groupResponse(baseUrl, updated ?? group));
1020
+ });
1021
+ app.delete("/api/v1/groups/:groupId", (c) => {
1022
+ const auth = requireManagementAuth(c, tokenMap);
1023
+ if (auth instanceof Response) return auth;
1024
+ const group = findGroupByRef(oktaStore, c.req.param("groupId"));
1025
+ if (!group) return oktaError(c, 404, "E0000007", "Not found: group");
1026
+ for (const membership of oktaStore.groupMemberships.findBy("group_okta_id", group.okta_id)) {
1027
+ oktaStore.groupMemberships.delete(membership.id);
1028
+ }
1029
+ oktaStore.groups.delete(group.id);
1030
+ return new Response(null, { status: 204 });
1031
+ });
1032
+ }
1033
+ var keyPairPromise = generateKeyPair("RS256");
1034
+ var KID = "emulate-okta-1";
1035
+ var CODE_TTL_MS = 10 * 60 * 1e3;
1036
+ function getPendingCodes(store) {
1037
+ let map = store.getData("okta.oauth.pendingCodes");
1038
+ if (!map) {
1039
+ map = /* @__PURE__ */ new Map();
1040
+ store.setData("okta.oauth.pendingCodes", map);
1041
+ }
1042
+ return map;
1043
+ }
1044
+ function getAccessTokens(store) {
1045
+ let map = store.getData("okta.oauth.accessTokens");
1046
+ if (!map) {
1047
+ map = /* @__PURE__ */ new Map();
1048
+ store.setData("okta.oauth.accessTokens", map);
1049
+ }
1050
+ return map;
1051
+ }
1052
+ function getRefreshTokens(store) {
1053
+ let map = store.getData("okta.oauth.refreshTokens");
1054
+ if (!map) {
1055
+ map = /* @__PURE__ */ new Map();
1056
+ store.setData("okta.oauth.refreshTokens", map);
1057
+ }
1058
+ return map;
1059
+ }
1060
+ function isCodeExpired(code) {
1061
+ return Date.now() - code.createdAt > CODE_TTL_MS;
1062
+ }
1063
+ function buildOAuthBasePath(authServerId) {
1064
+ if (authServerId === ORG_AUTH_SERVER_ID) return "/oauth2/v1";
1065
+ return `/oauth2/${encodeURIComponent(authServerId)}/v1`;
1066
+ }
1067
+ function getClientsForServer(clients, authServerId) {
1068
+ return clients.filter((client) => client.auth_server_id === authServerId);
1069
+ }
1070
+ function resolveServer(authServerId, baseUrl, store) {
1071
+ if (authServerId === ORG_AUTH_SERVER_ID) {
1072
+ return {
1073
+ authServerId,
1074
+ issuer: baseUrl,
1075
+ audiences: [DEFAULT_AUDIENCE]
1076
+ };
1077
+ }
1078
+ const server = store.authorizationServers.findOneBy("server_id", authServerId);
1079
+ if (!server) return null;
1080
+ return {
1081
+ authServerId,
1082
+ issuer: resolveOktaIssuer(baseUrl, authServerId),
1083
+ audiences: server.audiences.length > 0 ? server.audiences : [DEFAULT_AUDIENCE]
1084
+ };
1085
+ }
1086
+ function buildOidcConfiguration(baseUrl, server) {
1087
+ const oauthBase = buildOAuthBasePath(server.authServerId);
1088
+ const oauthUrlBase = `${baseUrl}${oauthBase}`;
1089
+ const tokenEndpointAuthMethods = ["client_secret_post", "client_secret_basic", "none"];
1090
+ return {
1091
+ issuer: server.issuer,
1092
+ authorization_endpoint: `${oauthUrlBase}/authorize`,
1093
+ token_endpoint: `${oauthUrlBase}/token`,
1094
+ userinfo_endpoint: `${oauthUrlBase}/userinfo`,
1095
+ jwks_uri: `${oauthUrlBase}/keys`,
1096
+ end_session_endpoint: `${oauthUrlBase}/logout`,
1097
+ revocation_endpoint: `${oauthUrlBase}/revoke`,
1098
+ introspection_endpoint: `${oauthUrlBase}/introspect`,
1099
+ registration_endpoint: `${oauthUrlBase}/clients`,
1100
+ response_types_supported: ["code"],
1101
+ response_modes_supported: ["query", "fragment", "form_post"],
1102
+ grant_types_supported: ["authorization_code", "refresh_token", "client_credentials"],
1103
+ subject_types_supported: ["public"],
1104
+ id_token_signing_alg_values_supported: ["RS256"],
1105
+ scopes_supported: ["openid", "profile", "email", "offline_access", "groups"],
1106
+ token_endpoint_auth_methods_supported: tokenEndpointAuthMethods,
1107
+ revocation_endpoint_auth_methods_supported: tokenEndpointAuthMethods,
1108
+ introspection_endpoint_auth_methods_supported: tokenEndpointAuthMethods,
1109
+ request_parameter_supported: false,
1110
+ request_uri_parameter_supported: false,
1111
+ claims_parameter_supported: false,
1112
+ request_object_signing_alg_values_supported: ["RS256"],
1113
+ claims_supported: [
1114
+ "sub",
1115
+ "iss",
1116
+ "aud",
1117
+ "exp",
1118
+ "iat",
1119
+ "auth_time",
1120
+ "nonce",
1121
+ "name",
1122
+ "preferred_username",
1123
+ "email",
1124
+ "email_verified",
1125
+ "locale",
1126
+ "zoneinfo",
1127
+ "groups"
1128
+ ],
1129
+ code_challenge_methods_supported: ["plain", "S256"]
1130
+ };
1131
+ }
1132
+ async function parseTokenLikeBody(c) {
1133
+ const contentType = c.req.header("Content-Type") ?? "";
1134
+ const raw = await c.req.text();
1135
+ if (contentType.includes("application/json")) {
1136
+ try {
1137
+ const parsed = JSON.parse(raw);
1138
+ const out = {};
1139
+ for (const [key, value] of Object.entries(parsed)) {
1140
+ if (typeof value === "string") out[key] = value;
1141
+ }
1142
+ return out;
1143
+ } catch {
1144
+ return {};
1145
+ }
1146
+ }
1147
+ return Object.fromEntries(new URLSearchParams(raw));
1148
+ }
1149
+ function parseClientCredentials(c, body) {
1150
+ let clientId = body.client_id ?? "";
1151
+ let clientSecret = body.client_secret ?? "";
1152
+ const authHeader = c.req.header("Authorization") ?? "";
1153
+ if (authHeader.startsWith("Basic ")) {
1154
+ const decoded = Buffer.from(authHeader.slice(6), "base64").toString("utf8");
1155
+ const sep = decoded.indexOf(":");
1156
+ if (sep !== -1) {
1157
+ const headerId = decodeURIComponent(decoded.slice(0, sep));
1158
+ const headerSecret = decodeURIComponent(decoded.slice(sep + 1));
1159
+ if (!clientId) clientId = headerId;
1160
+ if (!clientSecret) clientSecret = headerSecret;
1161
+ }
1162
+ }
1163
+ return { clientId, clientSecret };
1164
+ }
1165
+ function validateClient(clients, authServerId, clientId, clientSecret) {
1166
+ const scopedClients = getClientsForServer(clients, authServerId);
1167
+ if (scopedClients.length === 0) {
1168
+ return { client: null, error: null };
1169
+ }
1170
+ const client = scopedClients.find((entry) => entry.client_id === clientId);
1171
+ if (!client) {
1172
+ return {
1173
+ client: null,
1174
+ error: {
1175
+ body: { error: "invalid_client", error_description: "Unknown client." },
1176
+ status: 401
1177
+ }
1178
+ };
1179
+ }
1180
+ if (client.token_endpoint_auth_method === "none") {
1181
+ return { client, error: null };
1182
+ }
1183
+ if (!constantTimeSecretEqual(client.client_secret ?? "", clientSecret)) {
1184
+ return {
1185
+ client: null,
1186
+ error: {
1187
+ body: { error: "invalid_client", error_description: "Invalid client credentials." },
1188
+ status: 401
1189
+ }
1190
+ };
1191
+ }
1192
+ return { client, error: null };
1193
+ }
1194
+ function parseScope(scope) {
1195
+ return scope.split(/\s+/).map((part) => part.trim()).filter(Boolean);
1196
+ }
1197
+ function collectUserGroups(oktaStore, user) {
1198
+ const memberships = oktaStore.groupMemberships.findBy("user_okta_id", user.okta_id);
1199
+ const names = [];
1200
+ for (const membership of memberships) {
1201
+ const group = oktaStore.groups.findOneBy("okta_id", membership.group_okta_id);
1202
+ if (group) names.push(group.name);
1203
+ }
1204
+ return names;
1205
+ }
1206
+ async function createIdToken(oktaStore, user, clientId, nonce, issuer, scope) {
1207
+ const { privateKey } = await keyPairPromise;
1208
+ const now = Math.floor(Date.now() / 1e3);
1209
+ const scopes = parseScope(scope);
1210
+ const claims = {
1211
+ sub: user.okta_id,
1212
+ name: userDisplayName(user),
1213
+ preferred_username: user.login,
1214
+ email: user.email,
1215
+ email_verified: true,
1216
+ locale: user.locale,
1217
+ zoneinfo: user.time_zone,
1218
+ auth_time: now
1219
+ };
1220
+ if (nonce) claims.nonce = nonce;
1221
+ if (scopes.includes("groups")) {
1222
+ claims.groups = collectUserGroups(oktaStore, user);
1223
+ }
1224
+ return new SignJWT(claims).setProtectedHeader({ alg: "RS256", kid: KID, typ: "JWT" }).setIssuer(issuer).setAudience(clientId).setIssuedAt(now).setExpirationTime("1h").sign(privateKey);
1225
+ }
1226
+ function unauthorizedOAuthError() {
1227
+ return new Response(JSON.stringify({ error: "invalid_token", error_description: "The access token is invalid." }), {
1228
+ status: 401,
1229
+ headers: { "Content-Type": "application/json" }
1230
+ });
1231
+ }
1232
+ function oauthRoutes({ app, store, baseUrl, tokenMap }) {
1233
+ const oktaStore = getOktaStore(store);
1234
+ const SERVICE_LABEL = "Okta";
1235
+ app.get("/.well-known/openid-configuration", (c) => {
1236
+ const server = resolveServer(ORG_AUTH_SERVER_ID, baseUrl, oktaStore);
1237
+ if (!server) return oktaError(c, 404, "E0000007", "Not found: org authorization server");
1238
+ return c.json(buildOidcConfiguration(baseUrl, server));
1239
+ });
1240
+ app.get("/oauth2/:authServerId/.well-known/openid-configuration", (c) => {
1241
+ const authServerId = c.req.param("authServerId");
1242
+ const server = resolveServer(authServerId, baseUrl, oktaStore);
1243
+ if (!server) return oktaError(c, 404, "E0000007", `Not found: authorization server '${authServerId}'`);
1244
+ return c.json(buildOidcConfiguration(baseUrl, server));
1245
+ });
1246
+ app.get("/oauth2/v1/keys", async (c) => {
1247
+ const { publicKey } = await keyPairPromise;
1248
+ const jwk = await exportJWK(publicKey);
1249
+ return c.json({
1250
+ keys: [{ ...jwk, kid: KID, use: "sig", alg: "RS256" }]
1251
+ });
1252
+ });
1253
+ app.get("/oauth2/:authServerId/v1/keys", async (c) => {
1254
+ const authServerId = c.req.param("authServerId");
1255
+ const server = resolveServer(authServerId, baseUrl, oktaStore);
1256
+ if (!server) return oktaError(c, 404, "E0000007", `Not found: authorization server '${authServerId}'`);
1257
+ const { publicKey } = await keyPairPromise;
1258
+ const jwk = await exportJWK(publicKey);
1259
+ return c.json({
1260
+ keys: [{ ...jwk, kid: KID, use: "sig", alg: "RS256" }]
1261
+ });
1262
+ });
1263
+ const renderAuthorizePage = (c, authServerId) => {
1264
+ const server = resolveServer(authServerId, baseUrl, oktaStore);
1265
+ if (!server) return oktaError(c, 404, "E0000007", `Not found: authorization server '${authServerId}'`);
1266
+ const clientId = c.req.query("client_id") ?? "";
1267
+ const redirectUri = c.req.query("redirect_uri") ?? "";
1268
+ const scope = c.req.query("scope") ?? "openid profile email";
1269
+ const state = c.req.query("state") ?? "";
1270
+ const nonce = c.req.query("nonce") ?? "";
1271
+ const responseMode = c.req.query("response_mode") ?? "query";
1272
+ const responseType = c.req.query("response_type") ?? "code";
1273
+ const codeChallenge = c.req.query("code_challenge") ?? "";
1274
+ const codeChallengeMethod = c.req.query("code_challenge_method") ?? "";
1275
+ if (responseType !== "code") {
1276
+ return c.html(
1277
+ renderErrorPage("Unsupported response_type", "Only response_type=code is supported.", SERVICE_LABEL),
1278
+ 400
1279
+ );
1280
+ }
1281
+ if (!redirectUri) {
1282
+ return c.html(
1283
+ renderErrorPage("Missing redirect URI", "The redirect_uri parameter is required.", SERVICE_LABEL),
1284
+ 400
1285
+ );
1286
+ }
1287
+ const configuredClients = getClientsForServer(oktaStore.oauthClients.all(), authServerId);
1288
+ let clientName = "";
1289
+ if (configuredClients.length > 0) {
1290
+ const client = configuredClients.find((entry) => entry.client_id === clientId);
1291
+ if (!client) {
1292
+ return c.html(
1293
+ renderErrorPage("Application not found", `The client_id '${clientId}' is not registered.`, SERVICE_LABEL),
1294
+ 400
1295
+ );
1296
+ }
1297
+ if (!matchesRedirectUri(redirectUri, client.redirect_uris)) {
1298
+ return c.html(
1299
+ renderErrorPage(
1300
+ "Redirect URI mismatch",
1301
+ "The redirect_uri is not registered for this application.",
1302
+ SERVICE_LABEL
1303
+ ),
1304
+ 400
1305
+ );
1306
+ }
1307
+ clientName = client.name;
1308
+ }
1309
+ const users = oktaStore.users.all();
1310
+ const callbackPath = `${baseUrl}${buildOAuthBasePath(authServerId)}/authorize/callback`;
1311
+ const buttons = users.map(
1312
+ (user) => renderUserButton({
1313
+ letter: (user.login[0] ?? "?").toUpperCase(),
1314
+ login: user.login,
1315
+ name: userDisplayName(user),
1316
+ email: user.email,
1317
+ formAction: callbackPath,
1318
+ hiddenFields: {
1319
+ user_ref: user.okta_id,
1320
+ redirect_uri: redirectUri,
1321
+ scope,
1322
+ state,
1323
+ nonce,
1324
+ client_id: clientId,
1325
+ response_mode: responseMode,
1326
+ code_challenge: codeChallenge,
1327
+ code_challenge_method: codeChallengeMethod,
1328
+ auth_server_id: authServerId
1329
+ }
1330
+ })
1331
+ ).join("\n");
1332
+ const subtitle = clientName ? `Sign in to <strong>${escapeHtml(clientName)}</strong> with your Okta account.` : "Choose a seeded user to continue.";
1333
+ return c.html(
1334
+ renderCardPage(
1335
+ "Sign in with Okta",
1336
+ subtitle,
1337
+ users.length > 0 ? buttons : '<p class="empty">No users in the emulator store.</p>',
1338
+ SERVICE_LABEL
1339
+ )
1340
+ );
1341
+ };
1342
+ app.get("/oauth2/v1/authorize", (c) => renderAuthorizePage(c, ORG_AUTH_SERVER_ID));
1343
+ app.get("/oauth2/:authServerId/v1/authorize", (c) => renderAuthorizePage(c, c.req.param("authServerId")));
1344
+ const handleAuthorizeCallback = async (c, authServerId) => {
1345
+ const server = resolveServer(authServerId, baseUrl, oktaStore);
1346
+ if (!server) return oktaError(c, 404, "E0000007", `Not found: authorization server '${authServerId}'`);
1347
+ const body = await c.req.parseBody();
1348
+ const userRef = bodyStr(body.user_ref);
1349
+ const redirectUri = bodyStr(body.redirect_uri);
1350
+ const scope = bodyStr(body.scope) || "openid profile email";
1351
+ const state = bodyStr(body.state);
1352
+ const nonce = bodyStr(body.nonce);
1353
+ const clientId = bodyStr(body.client_id);
1354
+ const responseMode = bodyStr(body.response_mode) || "query";
1355
+ const codeChallenge = bodyStr(body.code_challenge);
1356
+ const codeChallengeMethod = bodyStr(body.code_challenge_method);
1357
+ if (!redirectUri) {
1358
+ return c.html(
1359
+ renderErrorPage("Missing redirect URI", "The redirect_uri parameter is required.", SERVICE_LABEL),
1360
+ 400
1361
+ );
1362
+ }
1363
+ const user = findUserByRef(oktaStore, userRef);
1364
+ if (!user) {
1365
+ return c.html(renderErrorPage("Unknown user", "The selected user is not available.", SERVICE_LABEL), 400);
1366
+ }
1367
+ const configuredClients = getClientsForServer(oktaStore.oauthClients.all(), authServerId);
1368
+ if (configuredClients.length > 0) {
1369
+ const client = configuredClients.find((entry) => entry.client_id === clientId);
1370
+ if (!client) {
1371
+ return c.html(
1372
+ renderErrorPage("Application not found", `The client_id '${clientId}' is not registered.`, SERVICE_LABEL),
1373
+ 400
1374
+ );
1375
+ }
1376
+ if (!matchesRedirectUri(redirectUri, client.redirect_uris)) {
1377
+ return c.html(
1378
+ renderErrorPage(
1379
+ "Redirect URI mismatch",
1380
+ "The redirect_uri is not registered for this application.",
1381
+ SERVICE_LABEL
1382
+ ),
1383
+ 400
1384
+ );
1385
+ }
1386
+ }
1387
+ const code = randomBytes(20).toString("hex");
1388
+ getPendingCodes(store).set(code, {
1389
+ userRef: user.okta_id,
1390
+ scope,
1391
+ redirectUri,
1392
+ clientId,
1393
+ nonce: nonce || null,
1394
+ codeChallenge: codeChallenge || null,
1395
+ codeChallengeMethod: codeChallengeMethod || null,
1396
+ authServerId,
1397
+ createdAt: Date.now()
1398
+ });
1399
+ debug("okta.oauth", `[callback] code=${code.slice(0, 8)}... user=${user.login} server=${authServerId}`);
1400
+ if (responseMode === "form_post") {
1401
+ return c.html(renderFormPostPage(redirectUri, { code, state }, SERVICE_LABEL));
1402
+ }
1403
+ const url = new URL(redirectUri);
1404
+ url.searchParams.set("code", code);
1405
+ if (state) url.searchParams.set("state", state);
1406
+ return c.redirect(url.toString(), 302);
1407
+ };
1408
+ app.post("/oauth2/v1/authorize/callback", (c) => handleAuthorizeCallback(c, ORG_AUTH_SERVER_ID));
1409
+ app.post(
1410
+ "/oauth2/:authServerId/v1/authorize/callback",
1411
+ (c) => handleAuthorizeCallback(c, c.req.param("authServerId"))
1412
+ );
1413
+ const handleToken = async (c, authServerId) => {
1414
+ const server = resolveServer(authServerId, baseUrl, oktaStore);
1415
+ if (!server) return oktaError(c, 404, "E0000007", `Not found: authorization server '${authServerId}'`);
1416
+ const body = await parseTokenLikeBody(c);
1417
+ const grantType = body.grant_type ?? "";
1418
+ const code = body.code ?? "";
1419
+ const redirectUri = body.redirect_uri ?? "";
1420
+ const codeVerifier = body.code_verifier;
1421
+ const refreshToken = body.refresh_token ?? "";
1422
+ const requestedScope = body.scope ?? "";
1423
+ const creds = parseClientCredentials(c, body);
1424
+ const validation = validateClient(oktaStore.oauthClients.all(), authServerId, creds.clientId, creds.clientSecret);
1425
+ if (validation.error) {
1426
+ return c.json(validation.error.body, validation.error.status);
1427
+ }
1428
+ const validatedClient = validation.client;
1429
+ if (grantType === "authorization_code") {
1430
+ const pending = getPendingCodes(store).get(code);
1431
+ if (!pending || isCodeExpired(pending)) {
1432
+ if (pending) getPendingCodes(store).delete(code);
1433
+ return c.json({ error: "invalid_grant", error_description: "Authorization code is invalid or expired." }, 400);
1434
+ }
1435
+ if (pending.authServerId !== authServerId) {
1436
+ return c.json({ error: "invalid_grant", error_description: "Authorization server mismatch." }, 400);
1437
+ }
1438
+ if (redirectUri && redirectUri !== pending.redirectUri) {
1439
+ return c.json({ error: "invalid_grant", error_description: "redirect_uri does not match." }, 400);
1440
+ }
1441
+ if (validatedClient && validatedClient.client_id !== pending.clientId) {
1442
+ return c.json(
1443
+ { error: "invalid_grant", error_description: "Authorization code was not issued to this client." },
1444
+ 400
1445
+ );
1446
+ }
1447
+ if (pending.codeChallenge !== null) {
1448
+ if (!codeVerifier) {
1449
+ return c.json({ error: "invalid_grant", error_description: "PKCE verification failed." }, 400);
1450
+ }
1451
+ const method = (pending.codeChallengeMethod ?? "plain").toLowerCase();
1452
+ if (method === "s256") {
1453
+ const expected = createHash("sha256").update(codeVerifier).digest("base64url");
1454
+ if (expected !== pending.codeChallenge) {
1455
+ return c.json({ error: "invalid_grant", error_description: "PKCE verification failed." }, 400);
1456
+ }
1457
+ } else if (method === "plain") {
1458
+ if (codeVerifier !== pending.codeChallenge) {
1459
+ return c.json({ error: "invalid_grant", error_description: "PKCE verification failed." }, 400);
1460
+ }
1461
+ } else {
1462
+ return c.json({ error: "invalid_grant", error_description: "PKCE verification failed." }, 400);
1463
+ }
1464
+ }
1465
+ const user = findUserByRef(oktaStore, pending.userRef);
1466
+ if (!user) return c.json({ error: "invalid_grant", error_description: "Unknown user." }, 400);
1467
+ getPendingCodes(store).delete(code);
1468
+ const now = Math.floor(Date.now() / 1e3);
1469
+ const audienceClient = pending.clientId || creds.clientId || "okta-client";
1470
+ const scope = pending.scope || "openid profile email";
1471
+ const accessToken = `okta_${randomBytes(20).toString("base64url")}`;
1472
+ const newRefreshToken = `r_okta_${randomBytes(20).toString("base64url")}`;
1473
+ getAccessTokens(store).set(accessToken, {
1474
+ authServerId,
1475
+ clientId: audienceClient,
1476
+ scope,
1477
+ issuedAt: now,
1478
+ expiresAt: now + 3600,
1479
+ userOktaId: user.okta_id,
1480
+ username: user.login
1481
+ });
1482
+ getRefreshTokens(store).set(newRefreshToken, {
1483
+ authServerId,
1484
+ clientId: audienceClient,
1485
+ scope,
1486
+ userOktaId: user.okta_id,
1487
+ username: user.login,
1488
+ nonce: pending.nonce
1489
+ });
1490
+ tokenMap?.set(accessToken, {
1491
+ login: user.login,
1492
+ id: user.id,
1493
+ scopes: parseScope(scope)
1494
+ });
1495
+ const idToken = await createIdToken(oktaStore, user, audienceClient, pending.nonce, server.issuer, scope);
1496
+ return c.json({
1497
+ token_type: "Bearer",
1498
+ expires_in: 3600,
1499
+ access_token: accessToken,
1500
+ refresh_token: newRefreshToken,
1501
+ id_token: idToken,
1502
+ scope
1503
+ });
1504
+ }
1505
+ if (grantType === "refresh_token") {
1506
+ const existing = getRefreshTokens(store).get(refreshToken);
1507
+ if (!existing) {
1508
+ return c.json({ error: "invalid_grant", error_description: "Invalid refresh token." }, 400);
1509
+ }
1510
+ if (existing.authServerId !== authServerId) {
1511
+ return c.json({ error: "invalid_grant", error_description: "Authorization server mismatch." }, 400);
1512
+ }
1513
+ if (validatedClient && validatedClient.client_id !== existing.clientId) {
1514
+ return c.json(
1515
+ { error: "invalid_grant", error_description: "Refresh token was not issued to this client." },
1516
+ 400
1517
+ );
1518
+ }
1519
+ const user = oktaStore.users.findOneBy("okta_id", existing.userOktaId);
1520
+ if (!user) return c.json({ error: "invalid_grant", error_description: "Unknown user." }, 400);
1521
+ getRefreshTokens(store).delete(refreshToken);
1522
+ const now = Math.floor(Date.now() / 1e3);
1523
+ const nextAccessToken = `okta_${randomBytes(20).toString("base64url")}`;
1524
+ const nextRefreshToken = `r_okta_${randomBytes(20).toString("base64url")}`;
1525
+ const scope = requestedScope || existing.scope;
1526
+ getAccessTokens(store).set(nextAccessToken, {
1527
+ authServerId,
1528
+ clientId: existing.clientId,
1529
+ scope,
1530
+ issuedAt: now,
1531
+ expiresAt: now + 3600,
1532
+ userOktaId: user.okta_id,
1533
+ username: user.login
1534
+ });
1535
+ getRefreshTokens(store).set(nextRefreshToken, {
1536
+ ...existing,
1537
+ scope
1538
+ });
1539
+ tokenMap?.set(nextAccessToken, {
1540
+ login: user.login,
1541
+ id: user.id,
1542
+ scopes: parseScope(scope)
1543
+ });
1544
+ const response = {
1545
+ token_type: "Bearer",
1546
+ expires_in: 3600,
1547
+ access_token: nextAccessToken,
1548
+ refresh_token: nextRefreshToken,
1549
+ scope
1550
+ };
1551
+ if (parseScope(scope).includes("openid")) {
1552
+ response.id_token = await createIdToken(
1553
+ oktaStore,
1554
+ user,
1555
+ existing.clientId,
1556
+ existing.nonce,
1557
+ server.issuer,
1558
+ scope
1559
+ );
1560
+ }
1561
+ return c.json(response);
1562
+ }
1563
+ if (grantType === "client_credentials") {
1564
+ if (oktaStore.oauthClients.all().length > 0 && !validatedClient) {
1565
+ return c.json({ error: "invalid_client", error_description: "Unknown client." }, 401);
1566
+ }
1567
+ const scope = requestedScope || ".default";
1568
+ const now = Math.floor(Date.now() / 1e3);
1569
+ const accessToken = `okta_${randomBytes(20).toString("base64url")}`;
1570
+ const clientId = validatedClient?.client_id ?? creds.clientId;
1571
+ if (!clientId) {
1572
+ return c.json({ error: "invalid_client", error_description: "client_id is required." }, 401);
1573
+ }
1574
+ getAccessTokens(store).set(accessToken, {
1575
+ authServerId,
1576
+ clientId,
1577
+ scope,
1578
+ issuedAt: now,
1579
+ expiresAt: now + 3600,
1580
+ userOktaId: null,
1581
+ username: null
1582
+ });
1583
+ tokenMap?.set(accessToken, {
1584
+ login: clientId,
1585
+ id: 0,
1586
+ scopes: parseScope(scope)
1587
+ });
1588
+ return c.json({
1589
+ token_type: "Bearer",
1590
+ expires_in: 3600,
1591
+ access_token: accessToken,
1592
+ scope
1593
+ });
1594
+ }
1595
+ return c.json({ error: "unsupported_grant_type" }, 400);
1596
+ };
1597
+ app.post("/oauth2/v1/token", (c) => handleToken(c, ORG_AUTH_SERVER_ID));
1598
+ app.post("/oauth2/:authServerId/v1/token", (c) => handleToken(c, c.req.param("authServerId")));
1599
+ const handleUserInfo = (c, authServerId) => {
1600
+ const server = resolveServer(authServerId, baseUrl, oktaStore);
1601
+ if (!server) return oktaError(c, 404, "E0000007", `Not found: authorization server '${authServerId}'`);
1602
+ const token = c.get("authToken") ?? "";
1603
+ const access = getAccessTokens(store).get(token);
1604
+ if (!access || access.authServerId !== authServerId || !access.userOktaId) {
1605
+ return unauthorizedOAuthError();
1606
+ }
1607
+ const user = oktaStore.users.findOneBy("okta_id", access.userOktaId);
1608
+ if (!user) return unauthorizedOAuthError();
1609
+ const claims = {
1610
+ sub: user.okta_id,
1611
+ name: userDisplayName(user),
1612
+ preferred_username: user.login,
1613
+ email: user.email,
1614
+ email_verified: true,
1615
+ locale: user.locale,
1616
+ zoneinfo: user.time_zone
1617
+ };
1618
+ if (parseScope(access.scope).includes("groups")) {
1619
+ claims.groups = collectUserGroups(oktaStore, user);
1620
+ }
1621
+ return c.json(claims);
1622
+ };
1623
+ app.get("/oauth2/v1/userinfo", (c) => handleUserInfo(c, ORG_AUTH_SERVER_ID));
1624
+ app.get("/oauth2/:authServerId/v1/userinfo", (c) => handleUserInfo(c, c.req.param("authServerId")));
1625
+ const handleRevoke = async (c, authServerId) => {
1626
+ const server = resolveServer(authServerId, baseUrl, oktaStore);
1627
+ if (!server) return oktaError(c, 404, "E0000007", `Not found: authorization server '${authServerId}'`);
1628
+ const body = await parseTokenLikeBody(c);
1629
+ const token = body.token ?? "";
1630
+ getAccessTokens(store).delete(token);
1631
+ getRefreshTokens(store).delete(token);
1632
+ tokenMap?.delete(token);
1633
+ return c.body("", 200);
1634
+ };
1635
+ app.post("/oauth2/v1/revoke", (c) => handleRevoke(c, ORG_AUTH_SERVER_ID));
1636
+ app.post("/oauth2/:authServerId/v1/revoke", (c) => handleRevoke(c, c.req.param("authServerId")));
1637
+ const handleIntrospect = async (c, authServerId) => {
1638
+ const server = resolveServer(authServerId, baseUrl, oktaStore);
1639
+ if (!server) return oktaError(c, 404, "E0000007", `Not found: authorization server '${authServerId}'`);
1640
+ const body = await parseTokenLikeBody(c);
1641
+ const token = body.token ?? "";
1642
+ const creds = parseClientCredentials(c, body);
1643
+ const validation = validateClient(oktaStore.oauthClients.all(), authServerId, creds.clientId, creds.clientSecret);
1644
+ if (validation.error) {
1645
+ return c.json(validation.error.body, validation.error.status);
1646
+ }
1647
+ const now = Math.floor(Date.now() / 1e3);
1648
+ const access = getAccessTokens(store).get(token);
1649
+ if (access && access.authServerId === authServerId && access.expiresAt > now) {
1650
+ return c.json({
1651
+ active: true,
1652
+ token_type: "Bearer",
1653
+ scope: access.scope,
1654
+ client_id: access.clientId,
1655
+ username: access.username,
1656
+ sub: access.userOktaId,
1657
+ aud: server.audiences,
1658
+ iss: server.issuer,
1659
+ exp: access.expiresAt,
1660
+ iat: access.issuedAt
1661
+ });
1662
+ }
1663
+ const refresh = getRefreshTokens(store).get(token);
1664
+ if (refresh && refresh.authServerId === authServerId) {
1665
+ return c.json({
1666
+ active: true,
1667
+ token_type: "refresh_token",
1668
+ scope: refresh.scope,
1669
+ client_id: refresh.clientId,
1670
+ username: refresh.username,
1671
+ sub: refresh.userOktaId,
1672
+ aud: server.audiences,
1673
+ iss: server.issuer
1674
+ });
1675
+ }
1676
+ return c.json({ active: false });
1677
+ };
1678
+ app.post("/oauth2/v1/introspect", (c) => handleIntrospect(c, ORG_AUTH_SERVER_ID));
1679
+ app.post("/oauth2/:authServerId/v1/introspect", (c) => handleIntrospect(c, c.req.param("authServerId")));
1680
+ const handleLogout = (c, authServerId) => {
1681
+ const server = resolveServer(authServerId, baseUrl, oktaStore);
1682
+ if (!server) return oktaError(c, 404, "E0000007", `Not found: authorization server '${authServerId}'`);
1683
+ const postLogoutRedirectUri = c.req.query("post_logout_redirect_uri");
1684
+ if (!postLogoutRedirectUri) return c.text("Logged out");
1685
+ const scopedClients = getClientsForServer(oktaStore.oauthClients.all(), authServerId);
1686
+ if (scopedClients.length > 0) {
1687
+ const isAllowed = scopedClients.some((client) => matchesRedirectUri(postLogoutRedirectUri, client.redirect_uris));
1688
+ if (!isAllowed) return c.text("Invalid post_logout_redirect_uri", 400);
1689
+ }
1690
+ return c.redirect(postLogoutRedirectUri, 302);
1691
+ };
1692
+ app.get("/oauth2/v1/logout", (c) => handleLogout(c, ORG_AUTH_SERVER_ID));
1693
+ app.get("/oauth2/:authServerId/v1/logout", (c) => handleLogout(c, c.req.param("authServerId")));
1694
+ }
1695
+ function updateUserProfile(user, profile) {
1696
+ const nextFirstName = typeof profile.firstName === "string" ? profile.firstName : user.first_name;
1697
+ const nextLastName = typeof profile.lastName === "string" ? profile.lastName : user.last_name;
1698
+ const nextDisplayName = typeof profile.displayName === "string" ? profile.displayName : typeof profile.nickName === "string" ? profile.nickName : user.display_name;
1699
+ return {
1700
+ login: typeof profile.login === "string" ? profile.login : user.login,
1701
+ email: typeof profile.email === "string" ? profile.email : user.email,
1702
+ first_name: nextFirstName,
1703
+ last_name: nextLastName,
1704
+ display_name: nextDisplayName || `${nextFirstName} ${nextLastName}`.trim(),
1705
+ locale: typeof profile.locale === "string" ? profile.locale : user.locale,
1706
+ time_zone: typeof profile.timeZone === "string" ? profile.timeZone : user.time_zone
1707
+ };
1708
+ }
1709
+ function setLifecycleStatus(user, target) {
1710
+ const now = nowIso();
1711
+ const activatedAt = target === "ACTIVE" ? user.activated_at ?? now : user.activated_at;
1712
+ return {
1713
+ status: target,
1714
+ transitioning_to_status: null,
1715
+ status_changed_at: now,
1716
+ activated_at: activatedAt
1717
+ };
1718
+ }
1719
+ function userRoutes({ app, store, baseUrl, tokenMap }) {
1720
+ const oktaStore = getOktaStore(store);
1721
+ app.get("/api/v1/users", (c) => {
1722
+ const auth = requireManagementAuth(c, tokenMap);
1723
+ if (auth instanceof Response) return auth;
1724
+ const q = (c.req.query("q") ?? "").toLowerCase();
1725
+ const search = (c.req.query("search") ?? "").toLowerCase();
1726
+ const filter = c.req.query("filter") ?? "";
1727
+ let users = oktaStore.users.all();
1728
+ if (q) {
1729
+ users = users.filter(
1730
+ (user) => [user.login, user.email, user.first_name, user.last_name, user.display_name].join(" ").toLowerCase().includes(q)
1731
+ );
1732
+ }
1733
+ if (search) {
1734
+ users = users.filter(
1735
+ (user) => [user.login, user.email, user.first_name, user.last_name, user.display_name].join(" ").toLowerCase().includes(search)
1736
+ );
1737
+ }
1738
+ if (filter) {
1739
+ const statusMatch = filter.match(/status\s+eq\s+"?([A-Z_]+)"?/i);
1740
+ if (statusMatch?.[1]) {
1741
+ users = users.filter((user) => user.status === statusMatch[1]);
1742
+ }
1743
+ }
1744
+ const { page, per_page } = parsePagination(c);
1745
+ const total = users.length;
1746
+ const start = (page - 1) * per_page;
1747
+ const paged = users.slice(start, start + per_page);
1748
+ setLinkHeader(c, total, page, per_page);
1749
+ c.header("X-Total-Count", String(total));
1750
+ return c.json(paged.map((user) => userResponse(baseUrl, user)));
1751
+ });
1752
+ app.post("/api/v1/users", async (c) => {
1753
+ const auth = requireManagementAuth(c, tokenMap);
1754
+ if (auth instanceof Response) return auth;
1755
+ const body = await readJsonObject(c);
1756
+ const profile = body.profile && typeof body.profile === "object" ? body.profile : {};
1757
+ const login = typeof profile.login === "string" ? profile.login.trim() : "";
1758
+ const email = typeof profile.email === "string" ? profile.email.trim() : login;
1759
+ if (!login || !email) {
1760
+ return oktaError(c, 400, "E0000001", "profile.login and profile.email are required");
1761
+ }
1762
+ if (oktaStore.users.findOneBy("login", login) || oktaStore.users.findOneBy("email", email)) {
1763
+ return oktaError(c, 400, "E0000001", "A user with the same login or email already exists");
1764
+ }
1765
+ const activate = boolFromQuery(c.req.query("activate"), true);
1766
+ const now = nowIso();
1767
+ const firstName = typeof profile.firstName === "string" ? profile.firstName : "Test";
1768
+ const lastName = typeof profile.lastName === "string" ? profile.lastName : "User";
1769
+ const displayName = typeof profile.displayName === "string" ? profile.displayName : `${firstName} ${lastName}`.trim() || login;
1770
+ const created = oktaStore.users.insert({
1771
+ okta_id: generateOktaId("00u"),
1772
+ status: activate ? "ACTIVE" : "STAGED",
1773
+ activated_at: activate ? now : null,
1774
+ status_changed_at: now,
1775
+ last_login_at: null,
1776
+ password_changed_at: null,
1777
+ transitioning_to_status: null,
1778
+ login,
1779
+ email,
1780
+ first_name: firstName,
1781
+ last_name: lastName,
1782
+ display_name: displayName,
1783
+ locale: typeof profile.locale === "string" ? profile.locale : "en-US",
1784
+ time_zone: typeof profile.timeZone === "string" ? profile.timeZone : "UTC"
1785
+ });
1786
+ return c.json(userResponse(baseUrl, created), 201);
1787
+ });
1788
+ app.get("/api/v1/users/me", (c) => {
1789
+ const auth = requireManagementAuth(c, tokenMap);
1790
+ if (auth instanceof Response) return auth;
1791
+ const user = oktaStore.users.findOneBy("login", auth.login) ?? oktaStore.users.all()[0];
1792
+ if (!user) return oktaError(c, 404, "E0000007", "Not found: user");
1793
+ const response = userResponse(baseUrl, user);
1794
+ return c.json({
1795
+ ...response,
1796
+ profile: {
1797
+ ...response.profile,
1798
+ displayName: userDisplayName(user)
1799
+ }
1800
+ });
1801
+ });
1802
+ app.get("/api/v1/users/:userId/groups", (c) => {
1803
+ const auth = requireManagementAuth(c, tokenMap);
1804
+ if (auth instanceof Response) return auth;
1805
+ const user = findUserByRef(oktaStore, c.req.param("userId"));
1806
+ if (!user) return oktaError(c, 404, "E0000007", "Not found: user");
1807
+ const memberships = oktaStore.groupMemberships.findBy("user_okta_id", user.okta_id);
1808
+ const groups = memberships.map((membership) => oktaStore.groups.findOneBy("okta_id", membership.group_okta_id)).filter((group) => Boolean(group));
1809
+ return c.json(
1810
+ groups.map((group) => ({
1811
+ id: group.okta_id,
1812
+ profile: {
1813
+ name: group.name,
1814
+ description: group.description
1815
+ },
1816
+ type: group.type
1817
+ }))
1818
+ );
1819
+ });
1820
+ app.post("/api/v1/users/:userId/lifecycle/activate", (c) => {
1821
+ const auth = requireManagementAuth(c, tokenMap);
1822
+ if (auth instanceof Response) return auth;
1823
+ const user = findUserByRef(oktaStore, c.req.param("userId"));
1824
+ if (!user) return oktaError(c, 404, "E0000007", "Not found: user");
1825
+ const updated = oktaStore.users.update(user.id, setLifecycleStatus(user, "ACTIVE"));
1826
+ return c.json(userResponse(baseUrl, updated ?? user));
1827
+ });
1828
+ app.post("/api/v1/users/:userId/lifecycle/deactivate", (c) => {
1829
+ const auth = requireManagementAuth(c, tokenMap);
1830
+ if (auth instanceof Response) return auth;
1831
+ const user = findUserByRef(oktaStore, c.req.param("userId"));
1832
+ if (!user) return oktaError(c, 404, "E0000007", "Not found: user");
1833
+ const updated = oktaStore.users.update(user.id, setLifecycleStatus(user, "DEPROVISIONED"));
1834
+ return c.json(userResponse(baseUrl, updated ?? user));
1835
+ });
1836
+ app.post("/api/v1/users/:userId/lifecycle/suspend", (c) => {
1837
+ const auth = requireManagementAuth(c, tokenMap);
1838
+ if (auth instanceof Response) return auth;
1839
+ const user = findUserByRef(oktaStore, c.req.param("userId"));
1840
+ if (!user) return oktaError(c, 404, "E0000007", "Not found: user");
1841
+ const updated = oktaStore.users.update(user.id, setLifecycleStatus(user, "SUSPENDED"));
1842
+ return c.json(userResponse(baseUrl, updated ?? user));
1843
+ });
1844
+ app.post("/api/v1/users/:userId/lifecycle/unsuspend", (c) => {
1845
+ const auth = requireManagementAuth(c, tokenMap);
1846
+ if (auth instanceof Response) return auth;
1847
+ const user = findUserByRef(oktaStore, c.req.param("userId"));
1848
+ if (!user) return oktaError(c, 404, "E0000007", "Not found: user");
1849
+ const updated = oktaStore.users.update(user.id, setLifecycleStatus(user, "ACTIVE"));
1850
+ return c.json(userResponse(baseUrl, updated ?? user));
1851
+ });
1852
+ app.get("/api/v1/users/:userId", (c) => {
1853
+ const auth = requireManagementAuth(c, tokenMap);
1854
+ if (auth instanceof Response) return auth;
1855
+ const user = findUserByRef(oktaStore, c.req.param("userId"));
1856
+ if (!user) return oktaError(c, 404, "E0000007", "Not found: user");
1857
+ return c.json(userResponse(baseUrl, user));
1858
+ });
1859
+ app.put("/api/v1/users/:userId", async (c) => {
1860
+ const auth = requireManagementAuth(c, tokenMap);
1861
+ if (auth instanceof Response) return auth;
1862
+ const user = findUserByRef(oktaStore, c.req.param("userId"));
1863
+ if (!user) return oktaError(c, 404, "E0000007", "Not found: user");
1864
+ const body = await readJsonObject(c);
1865
+ const profile = body.profile && typeof body.profile === "object" ? body.profile : {};
1866
+ const updates = updateUserProfile(user, profile);
1867
+ if (updates.login !== user.login && oktaStore.users.findOneBy("login", updates.login ?? "") || updates.email !== user.email && oktaStore.users.findOneBy("email", updates.email ?? "")) {
1868
+ return oktaError(c, 400, "E0000001", "A user with the same login or email already exists");
1869
+ }
1870
+ const updated = oktaStore.users.update(user.id, updates);
1871
+ return c.json(userResponse(baseUrl, updated ?? user));
1872
+ });
1873
+ app.post("/api/v1/users/:userId", async (c) => {
1874
+ const auth = requireManagementAuth(c, tokenMap);
1875
+ if (auth instanceof Response) return auth;
1876
+ const user = findUserByRef(oktaStore, c.req.param("userId"));
1877
+ if (!user) return oktaError(c, 404, "E0000007", "Not found: user");
1878
+ const body = await readJsonObject(c);
1879
+ const profile = body.profile && typeof body.profile === "object" ? body.profile : {};
1880
+ const updates = updateUserProfile(user, profile);
1881
+ const updated = oktaStore.users.update(user.id, updates);
1882
+ return c.json(userResponse(baseUrl, updated ?? user));
1883
+ });
1884
+ app.delete("/api/v1/users/:userId", (c) => {
1885
+ const auth = requireManagementAuth(c, tokenMap);
1886
+ if (auth instanceof Response) return auth;
1887
+ const user = findUserByRef(oktaStore, c.req.param("userId"));
1888
+ if (!user) return oktaError(c, 404, "E0000007", "Not found: user");
1889
+ if (user.status !== "DEPROVISIONED") {
1890
+ oktaStore.users.update(user.id, setLifecycleStatus(user, "DEPROVISIONED"));
1891
+ return new Response(null, { status: 204 });
1892
+ }
1893
+ for (const membership of oktaStore.groupMemberships.findBy("user_okta_id", user.okta_id)) {
1894
+ oktaStore.groupMemberships.delete(membership.id);
1895
+ }
1896
+ for (const assignment of oktaStore.appAssignments.findBy("user_okta_id", user.okta_id)) {
1897
+ oktaStore.appAssignments.delete(assignment.id);
1898
+ }
1899
+ oktaStore.users.delete(user.id);
1900
+ return new Response(null, { status: 204 });
1901
+ });
1902
+ app.post("/api/v1/users/:userId/lifecycle/reactivate", (c) => {
1903
+ const auth = requireManagementAuth(c, tokenMap);
1904
+ if (auth instanceof Response) return auth;
1905
+ const user = findUserByRef(oktaStore, c.req.param("userId"));
1906
+ if (!user) return oktaError(c, 404, "E0000007", "Not found: user");
1907
+ const updated = oktaStore.users.update(user.id, {
1908
+ status: "PROVISIONED",
1909
+ status_changed_at: nowIso(),
1910
+ transitioning_to_status: null
1911
+ });
1912
+ return c.json(userResponse(baseUrl, updated ?? user));
1913
+ });
1914
+ }
1915
+ var manifest = {
1916
+ id: "okta",
1917
+ name: "Okta",
1918
+ description: "Stateful Okta OAuth, OpenID Connect, users, groups, apps, and authorization server emulator.",
1919
+ docsUrl: "https://docs.emulators.dev/okta",
1920
+ surfaces: [
1921
+ { id: "rest", kind: "rest", title: "Management API", status: "partial", basePath: "/api/v1" },
1922
+ { id: "oauth", kind: "oauth", title: "Okta OAuth 2.0", status: "supported", basePath: "/oauth2" },
1923
+ { id: "oidc", kind: "oidc", title: "OpenID Connect", status: "supported", basePath: "/.well-known" }
1924
+ ],
1925
+ auth: [
1926
+ { id: "oauth-code", title: "OAuth authorization code", type: "oauth-authorization-code", status: "supported" },
1927
+ {
1928
+ id: "client-credentials",
1929
+ title: "OAuth client credentials",
1930
+ type: "oauth-client-credentials",
1931
+ status: "supported"
1932
+ },
1933
+ { id: "oidc", title: "OIDC identity tokens", type: "oidc", status: "supported" }
1934
+ ],
1935
+ specs: [
1936
+ {
1937
+ kind: "oauth-metadata",
1938
+ title: "Okta OIDC metadata",
1939
+ coverage: "hand-authored",
1940
+ operations: [
1941
+ {
1942
+ operationId: "oidc/discovery",
1943
+ method: "GET",
1944
+ path: "/.well-known/openid-configuration",
1945
+ status: "hand-authored"
1946
+ },
1947
+ {
1948
+ operationId: "oidc/authServerDiscovery",
1949
+ method: "GET",
1950
+ path: "/oauth2/:authServerId/.well-known/openid-configuration",
1951
+ status: "hand-authored"
1952
+ },
1953
+ { operationId: "oidc/jwks", method: "GET", path: "/oauth2/v1/keys", status: "hand-authored" },
1954
+ { operationId: "oauth/authorize", method: "GET", path: "/oauth2/v1/authorize", status: "hand-authored" },
1955
+ { operationId: "oauth/token", method: "POST", path: "/oauth2/v1/token", status: "hand-authored" },
1956
+ { operationId: "oauth/userinfo", method: "GET", path: "/oauth2/v1/userinfo", status: "hand-authored" },
1957
+ { operationId: "oauth/introspect", method: "POST", path: "/oauth2/v1/introspect", status: "hand-authored" },
1958
+ { operationId: "oauth/revoke", method: "POST", path: "/oauth2/v1/revoke", status: "hand-authored" },
1959
+ { operationId: "oauth/logout", method: "GET", path: "/oauth2/v1/logout", status: "hand-authored" }
1960
+ ]
1961
+ },
1962
+ {
1963
+ kind: "manual",
1964
+ title: "Okta management API behavior",
1965
+ coverage: "partial",
1966
+ operations: [
1967
+ { operationId: "users/list", method: "GET", path: "/api/v1/users", status: "hand-authored" },
1968
+ { operationId: "users/create", method: "POST", path: "/api/v1/users", status: "hand-authored" },
1969
+ { operationId: "users/getCurrent", method: "GET", path: "/api/v1/users/me", status: "hand-authored" },
1970
+ { operationId: "users/get", method: "GET", path: "/api/v1/users/:userId", status: "hand-authored" },
1971
+ { operationId: "users/update", method: "PUT", path: "/api/v1/users/:userId", status: "hand-authored" },
1972
+ { operationId: "users/delete", method: "DELETE", path: "/api/v1/users/:userId", status: "hand-authored" },
1973
+ {
1974
+ operationId: "users/listGroups",
1975
+ method: "GET",
1976
+ path: "/api/v1/users/:userId/groups",
1977
+ status: "hand-authored"
1978
+ },
1979
+ {
1980
+ operationId: "users/activate",
1981
+ method: "POST",
1982
+ path: "/api/v1/users/:userId/lifecycle/activate",
1983
+ status: "hand-authored"
1984
+ },
1985
+ {
1986
+ operationId: "users/deactivate",
1987
+ method: "POST",
1988
+ path: "/api/v1/users/:userId/lifecycle/deactivate",
1989
+ status: "hand-authored"
1990
+ },
1991
+ { operationId: "groups/list", method: "GET", path: "/api/v1/groups", status: "hand-authored" },
1992
+ { operationId: "groups/create", method: "POST", path: "/api/v1/groups", status: "hand-authored" },
1993
+ { operationId: "groups/get", method: "GET", path: "/api/v1/groups/:groupId", status: "hand-authored" },
1994
+ {
1995
+ operationId: "groups/listUsers",
1996
+ method: "GET",
1997
+ path: "/api/v1/groups/:groupId/users",
1998
+ status: "hand-authored"
1999
+ },
2000
+ {
2001
+ operationId: "groups/addUser",
2002
+ method: "PUT",
2003
+ path: "/api/v1/groups/:groupId/users/:userId",
2004
+ status: "hand-authored"
2005
+ },
2006
+ {
2007
+ operationId: "groups/removeUser",
2008
+ method: "DELETE",
2009
+ path: "/api/v1/groups/:groupId/users/:userId",
2010
+ status: "hand-authored"
2011
+ },
2012
+ { operationId: "apps/list", method: "GET", path: "/api/v1/apps", status: "hand-authored" },
2013
+ { operationId: "apps/create", method: "POST", path: "/api/v1/apps", status: "hand-authored" },
2014
+ { operationId: "apps/get", method: "GET", path: "/api/v1/apps/:appId", status: "hand-authored" },
2015
+ {
2016
+ operationId: "apps/assignUser",
2017
+ method: "PUT",
2018
+ path: "/api/v1/apps/:appId/users/:userId",
2019
+ status: "hand-authored"
2020
+ },
2021
+ {
2022
+ operationId: "authorizationServers/list",
2023
+ method: "GET",
2024
+ path: "/api/v1/authorizationServers",
2025
+ status: "hand-authored"
2026
+ },
2027
+ {
2028
+ operationId: "authorizationServers/create",
2029
+ method: "POST",
2030
+ path: "/api/v1/authorizationServers",
2031
+ status: "hand-authored"
2032
+ },
2033
+ {
2034
+ operationId: "authorizationServers/get",
2035
+ method: "GET",
2036
+ path: "/api/v1/authorizationServers/:authServerId",
2037
+ status: "hand-authored"
2038
+ }
2039
+ ]
2040
+ }
2041
+ ],
2042
+ seedSchema: {
2043
+ description: "Seed users, groups, apps, OAuth clients, and authorization servers.",
2044
+ fields: [
2045
+ {
2046
+ key: "users",
2047
+ title: "Users",
2048
+ description: "Directory users addressable by login.",
2049
+ example: [
2050
+ { login: "testuser@okta.local", email: "testuser@okta.local", first_name: "Test", last_name: "User" }
2051
+ ]
2052
+ },
2053
+ {
2054
+ key: "groups",
2055
+ title: "Groups",
2056
+ description: "Directory groups for membership assignment.",
2057
+ example: [{ name: "Everyone", description: "All users", type: "BUILT_IN", okta_id: "00g_everyone" }]
2058
+ },
2059
+ {
2060
+ key: "apps",
2061
+ title: "Applications",
2062
+ description: "Okta application integrations.",
2063
+ example: [{ name: "My App", label: "My App", sign_on_mode: "OPENID_CONNECT", status: "ACTIVE" }]
2064
+ },
2065
+ {
2066
+ key: "oauth_clients",
2067
+ title: "OAuth clients",
2068
+ description: "OIDC clients usable in OAuth flows.",
2069
+ example: [
2070
+ {
2071
+ client_id: "okta-test-client",
2072
+ client_secret: "okta-test-secret",
2073
+ name: "Sample OIDC Client",
2074
+ redirect_uris: ["http://localhost:3000/callback"],
2075
+ auth_server_id: "default"
2076
+ }
2077
+ ]
2078
+ },
2079
+ {
2080
+ key: "authorization_servers",
2081
+ title: "Authorization servers",
2082
+ description: "Custom authorization servers that issue access tokens.",
2083
+ example: [{ id: "default", name: "default", audiences: ["api://default"] }]
2084
+ },
2085
+ {
2086
+ key: "group_memberships",
2087
+ title: "Group memberships",
2088
+ description: "Links between users and groups by Okta id.",
2089
+ example: [{ group_okta_id: "00g_everyone", user_okta_id: "00u_example" }]
2090
+ },
2091
+ {
2092
+ key: "app_assignments",
2093
+ title: "App assignments",
2094
+ description: "Links between users and applications by Okta id.",
2095
+ example: [{ app_okta_id: "0oa_example", user_okta_id: "00u_example" }]
2096
+ }
2097
+ ],
2098
+ example: {
2099
+ users: [{ login: "testuser@okta.local", email: "testuser@okta.local", first_name: "Test", last_name: "User" }],
2100
+ groups: [{ name: "Everyone", description: "All users", type: "BUILT_IN", okta_id: "00g_everyone" }],
2101
+ authorization_servers: [{ id: "default", name: "default", audiences: ["api://default"] }],
2102
+ oauth_clients: [
2103
+ {
2104
+ client_id: "okta-test-client",
2105
+ client_secret: "okta-test-secret",
2106
+ name: "Sample OIDC Client",
2107
+ redirect_uris: ["http://localhost:3000/callback"],
2108
+ auth_server_id: "default"
2109
+ }
2110
+ ]
2111
+ }
2112
+ },
2113
+ stateModel: {
2114
+ description: "Entities mutated by Okta provider calls.",
2115
+ collections: [
2116
+ { name: "users" },
2117
+ { name: "groups" },
2118
+ { name: "apps" },
2119
+ { name: "oauth_clients" },
2120
+ { name: "auth_servers" },
2121
+ { name: "group_memberships" },
2122
+ { name: "app_assignments" }
2123
+ ]
2124
+ },
2125
+ connections: [
2126
+ {
2127
+ id: "okta-auth-js",
2128
+ title: "Okta Auth JS (TypeScript)",
2129
+ kind: "sdk",
2130
+ language: "typescript",
2131
+ description: "Point the Okta Auth JS SDK at the emulator instance.",
2132
+ template: 'import { OktaAuth } from "@okta/okta-auth-js";\n\nconst authClient = new OktaAuth({\n issuer: "{{baseUrl}}/oauth2/default",\n clientId: "{{clientId}}",\n redirectUri: "http://localhost:3000/callback",\n scopes: ["openid", "profile", "email"],\n});'
2133
+ },
2134
+ {
2135
+ id: "okta-env",
2136
+ title: "Okta issuer (env)",
2137
+ kind: "env",
2138
+ language: "bash",
2139
+ description: "Point Okta SDKs and the management API at the emulator.",
2140
+ template: "OKTA_ORG_URL={{baseUrl}}\nOKTA_ISSUER={{baseUrl}}/oauth2/default\nOKTA_CLIENT_ID={{clientId}}\nOKTA_CLIENT_SECRET={{clientSecret}}\nOKTA_API_TOKEN={{token}}"
2141
+ },
2142
+ {
2143
+ id: "curl",
2144
+ title: "curl",
2145
+ kind: "curl",
2146
+ language: "bash",
2147
+ description: "Read the OIDC discovery document.",
2148
+ template: "curl -s {{baseUrl}}/oauth2/default/.well-known/openid-configuration"
2149
+ }
2150
+ ]
2151
+ };
2152
+ function ensureMembership(store, groupOktaId, userOktaId) {
2153
+ const existing = store.groupMemberships.findBy("group_okta_id", groupOktaId).find((entry) => entry.user_okta_id === userOktaId);
2154
+ if (!existing) {
2155
+ store.groupMemberships.insert({
2156
+ group_okta_id: groupOktaId,
2157
+ user_okta_id: userOktaId
2158
+ });
2159
+ }
2160
+ }
2161
+ function ensureAppAssignment(store, appOktaId, userOktaId) {
2162
+ const existing = store.appAssignments.findBy("app_okta_id", appOktaId).find((entry) => entry.user_okta_id === userOktaId);
2163
+ if (!existing) {
2164
+ store.appAssignments.insert({
2165
+ app_okta_id: appOktaId,
2166
+ user_okta_id: userOktaId
2167
+ });
2168
+ }
2169
+ }
2170
+ function seedDefaults(store, _baseUrl) {
2171
+ const okta = getOktaStore(store);
2172
+ const defaultServer = okta.authorizationServers.findOneBy("server_id", DEFAULT_AUTH_SERVER_ID);
2173
+ if (!defaultServer) {
2174
+ okta.authorizationServers.insert(createDefaultAuthorizationServer());
2175
+ }
2176
+ let everyone = okta.groups.findOneBy("okta_id", DEFAULT_EVERYONE_GROUP_ID);
2177
+ if (!everyone) {
2178
+ everyone = okta.groups.insert(createDefaultGroup());
2179
+ }
2180
+ let user = okta.users.findOneBy("login", "testuser@okta.local");
2181
+ if (!user) {
2182
+ user = okta.users.insert(createDefaultUser());
2183
+ }
2184
+ if (!okta.oauthClients.findOneBy("client_id", "okta-test-client")) {
2185
+ okta.oauthClients.insert({
2186
+ client_id: "okta-test-client",
2187
+ client_secret: "okta-test-secret",
2188
+ name: "Sample OIDC Client",
2189
+ redirect_uris: ["http://localhost:3000/callback"],
2190
+ response_types: ["code"],
2191
+ grant_types: ["authorization_code", "refresh_token", "client_credentials"],
2192
+ token_endpoint_auth_method: "client_secret_post",
2193
+ auth_server_id: DEFAULT_AUTH_SERVER_ID
2194
+ });
2195
+ }
2196
+ if (!okta.oauthClients.findOneBy("client_id", "okta-test-app")) {
2197
+ okta.oauthClients.insert({
2198
+ client_id: "okta-test-app",
2199
+ client_secret: "",
2200
+ name: "Sample Public PKCE Client",
2201
+ redirect_uris: ["http://localhost:3000/official-sdk/callback", "http://localhost:3000/official-sdk"],
2202
+ response_types: ["code"],
2203
+ grant_types: ["authorization_code", "refresh_token"],
2204
+ token_endpoint_auth_method: "none",
2205
+ auth_server_id: DEFAULT_AUTH_SERVER_ID
2206
+ });
2207
+ }
2208
+ if (okta.apps.all().length === 0) {
2209
+ okta.apps.insert(createDefaultApp());
2210
+ }
2211
+ ensureMembership(okta, everyone.okta_id, user.okta_id);
2212
+ }
2213
+ function seedFromConfig(store, _baseUrl, config) {
2214
+ const okta = getOktaStore(store);
2215
+ if (config.authorization_servers) {
2216
+ for (const server of config.authorization_servers) {
2217
+ const existing = okta.authorizationServers.findOneBy("server_id", server.id);
2218
+ if (existing) continue;
2219
+ okta.authorizationServers.insert({
2220
+ server_id: server.id,
2221
+ name: server.name,
2222
+ description: server.description ?? "",
2223
+ audiences: server.audiences ?? ["api://default"],
2224
+ status: normalizeAuthServerStatus(server.status, "ACTIVE")
2225
+ });
2226
+ }
2227
+ }
2228
+ if (config.users) {
2229
+ for (const user of config.users) {
2230
+ const byLogin = okta.users.findOneBy("login", user.login);
2231
+ if (byLogin) continue;
2232
+ const resolvedStatus = normalizeStatus(user.status, "ACTIVE");
2233
+ okta.users.insert({
2234
+ okta_id: user.okta_id ?? generateOktaId("00u"),
2235
+ status: resolvedStatus,
2236
+ activated_at: resolvedStatus === "ACTIVE" ? (/* @__PURE__ */ new Date()).toISOString() : null,
2237
+ status_changed_at: (/* @__PURE__ */ new Date()).toISOString(),
2238
+ last_login_at: null,
2239
+ password_changed_at: null,
2240
+ transitioning_to_status: null,
2241
+ login: user.login,
2242
+ email: user.email ?? user.login,
2243
+ first_name: user.first_name ?? "Test",
2244
+ last_name: user.last_name ?? "User",
2245
+ display_name: user.display_name ?? `${user.first_name ?? "Test"} ${user.last_name ?? "User"}`.trim(),
2246
+ locale: user.locale ?? "en-US",
2247
+ time_zone: user.time_zone ?? "UTC"
2248
+ });
2249
+ }
2250
+ }
2251
+ if (config.groups) {
2252
+ for (const group of config.groups) {
2253
+ const byName = okta.groups.findOneBy("name", group.name);
2254
+ if (byName) continue;
2255
+ okta.groups.insert({
2256
+ okta_id: group.okta_id ?? generateOktaId("00g"),
2257
+ type: normalizeGroupType(group.type, "OKTA_GROUP"),
2258
+ name: group.name,
2259
+ description: group.description ?? null
2260
+ });
2261
+ }
2262
+ }
2263
+ if (config.apps) {
2264
+ for (const app of config.apps) {
2265
+ const byName = okta.apps.findOneBy("name", app.name);
2266
+ if (byName) continue;
2267
+ okta.apps.insert({
2268
+ okta_id: app.okta_id ?? generateOktaId("0oa"),
2269
+ name: app.name,
2270
+ label: app.label ?? app.name,
2271
+ status: normalizeAppStatus(app.status, "ACTIVE"),
2272
+ sign_on_mode: app.sign_on_mode ?? "OPENID_CONNECT",
2273
+ settings: app.settings ?? {},
2274
+ credentials: app.credentials ?? {}
2275
+ });
2276
+ }
2277
+ }
2278
+ if (config.oauth_clients) {
2279
+ for (const client of config.oauth_clients) {
2280
+ const existing = okta.oauthClients.findOneBy("client_id", client.client_id);
2281
+ if (existing) continue;
2282
+ const tokenEndpointAuthMethod = client.token_endpoint_auth_method ?? "client_secret_post";
2283
+ okta.oauthClients.insert({
2284
+ client_id: client.client_id,
2285
+ client_secret: client.client_secret ?? "",
2286
+ name: client.name,
2287
+ redirect_uris: client.redirect_uris,
2288
+ response_types: client.response_types ?? ["code"],
2289
+ grant_types: client.grant_types ?? ["authorization_code", "refresh_token", "client_credentials"],
2290
+ token_endpoint_auth_method: tokenEndpointAuthMethod,
2291
+ auth_server_id: client.auth_server_id ?? DEFAULT_AUTH_SERVER_ID
2292
+ });
2293
+ }
2294
+ }
2295
+ if (config.group_memberships) {
2296
+ for (const membership of config.group_memberships) {
2297
+ const group = okta.groups.findOneBy("okta_id", membership.group_okta_id);
2298
+ const user = okta.users.findOneBy("okta_id", membership.user_okta_id);
2299
+ if (!group || !user) continue;
2300
+ ensureMembership(okta, group.okta_id, user.okta_id);
2301
+ }
2302
+ }
2303
+ if (config.app_assignments) {
2304
+ for (const assignment of config.app_assignments) {
2305
+ const app = okta.apps.findOneBy("okta_id", assignment.app_okta_id);
2306
+ const user = okta.users.findOneBy("okta_id", assignment.user_okta_id);
2307
+ if (!app || !user) continue;
2308
+ ensureAppAssignment(okta, app.okta_id, user.okta_id);
2309
+ }
2310
+ }
2311
+ }
2312
+ var oktaPlugin = {
2313
+ name: "okta",
2314
+ register(app, store, webhooks, baseUrl, tokenMap) {
2315
+ const ctx = { app, store, webhooks, baseUrl, tokenMap };
2316
+ oauthRoutes(ctx);
2317
+ userRoutes(ctx);
2318
+ groupRoutes(ctx);
2319
+ appRoutes(ctx);
2320
+ authorizationServerRoutes(ctx);
2321
+ },
2322
+ seed(store, baseUrl) {
2323
+ seedDefaults(store, baseUrl);
2324
+ }
2325
+ };
2326
+ var index_default = oktaPlugin;
2327
+ export {
2328
+ index_default as default,
2329
+ getOktaStore,
2330
+ manifest,
2331
+ oktaPlugin,
2332
+ seedFromConfig
2333
+ };
2334
+ //# sourceMappingURL=dist-BTEY33DJ.js.map