open-managed-agents 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/LICENSE +93 -0
  2. package/README.md +248 -0
  3. package/bin/oma.mjs +499 -0
  4. package/dist/scripts/alpha-openai-compatible-fixture.mjs +134 -0
  5. package/dist/scripts/alpha-smoke-models.mjs +92 -0
  6. package/dist/scripts/alpha-smoke.mjs +581 -0
  7. package/dist/scripts/oma-doctor.js +230 -0
  8. package/dist/scripts/oma-models.js +299 -0
  9. package/dist/scripts/oma-workspaces.js +133 -0
  10. package/dist/src/control-plane/admin/auth.js +43 -0
  11. package/dist/src/control-plane/admin/routes.js +76 -0
  12. package/dist/src/control-plane/admin/service.js +102 -0
  13. package/dist/src/control-plane/admission.js +90 -0
  14. package/dist/src/control-plane/agents/routes.js +63 -0
  15. package/dist/src/control-plane/agents/service.js +687 -0
  16. package/dist/src/control-plane/agents/store.js +391 -0
  17. package/dist/src/control-plane/agents/types.js +1 -0
  18. package/dist/src/control-plane/api-constants.js +3 -0
  19. package/dist/src/control-plane/app.js +907 -0
  20. package/dist/src/control-plane/console/auth.js +68 -0
  21. package/dist/src/control-plane/console/static.js +112 -0
  22. package/dist/src/control-plane/deployment-runtime-config.js +328 -0
  23. package/dist/src/control-plane/deployment-runtime-event-coordinator.js +25 -0
  24. package/dist/src/control-plane/deployment-runtime-turn-guard.js +14 -0
  25. package/dist/src/control-plane/deployment-session-coordinator.js +25 -0
  26. package/dist/src/control-plane/deployment-session-output-coordinator.js +27 -0
  27. package/dist/src/control-plane/deployment-storage.js +354 -0
  28. package/dist/src/control-plane/egress/image.js +48 -0
  29. package/dist/src/control-plane/egress/policy.js +571 -0
  30. package/dist/src/control-plane/egress/presets.js +77 -0
  31. package/dist/src/control-plane/egress/proxy.js +72 -0
  32. package/dist/src/control-plane/egress/ssrf.js +106 -0
  33. package/dist/src/control-plane/egress/utils/debug.js +14 -0
  34. package/dist/src/control-plane/egress/vendor/http-proxy.js +335 -0
  35. package/dist/src/control-plane/egress/vendor/mitm-ca.js +226 -0
  36. package/dist/src/control-plane/egress/vendor/mitm-leaf.js +124 -0
  37. package/dist/src/control-plane/egress/vendor/parent-proxy.js +438 -0
  38. package/dist/src/control-plane/egress/vendor/request-filter.js +105 -0
  39. package/dist/src/control-plane/egress/vendor/sandbox-config.js +1 -0
  40. package/dist/src/control-plane/egress/vendor/tls-terminate-proxy.js +269 -0
  41. package/dist/src/control-plane/environments/routes.js +54 -0
  42. package/dist/src/control-plane/environments/service.js +112 -0
  43. package/dist/src/control-plane/environments/store.js +132 -0
  44. package/dist/src/control-plane/environments/types.js +1 -0
  45. package/dist/src/control-plane/errors.js +81 -0
  46. package/dist/src/control-plane/events/broadcaster.js +217 -0
  47. package/dist/src/control-plane/events/constants.js +2 -0
  48. package/dist/src/control-plane/events/persist.js +39 -0
  49. package/dist/src/control-plane/events/request.js +190 -0
  50. package/dist/src/control-plane/events/routes.js +159 -0
  51. package/dist/src/control-plane/events/runtime-helpers.js +134 -0
  52. package/dist/src/control-plane/events/service.js +1985 -0
  53. package/dist/src/control-plane/events/session-guards.js +29 -0
  54. package/dist/src/control-plane/events/sse.js +6 -0
  55. package/dist/src/control-plane/events/store.js +772 -0
  56. package/dist/src/control-plane/events/tool-persistence.js +102 -0
  57. package/dist/src/control-plane/events/types.js +21 -0
  58. package/dist/src/control-plane/files/routes.js +121 -0
  59. package/dist/src/control-plane/files/service.js +65 -0
  60. package/dist/src/control-plane/files/store-common.js +87 -0
  61. package/dist/src/control-plane/files/store-local.js +488 -0
  62. package/dist/src/control-plane/files/store-memory.js +270 -0
  63. package/dist/src/control-plane/files/store.js +2 -0
  64. package/dist/src/control-plane/files/types.js +7 -0
  65. package/dist/src/control-plane/http.js +25 -0
  66. package/dist/src/control-plane/ids.js +43 -0
  67. package/dist/src/control-plane/logging.js +263 -0
  68. package/dist/src/control-plane/models/auth-storage-backend.js +238 -0
  69. package/dist/src/control-plane/models/catalog.js +78 -0
  70. package/dist/src/control-plane/models/config-security.js +376 -0
  71. package/dist/src/control-plane/models/deployment-config.js +93 -0
  72. package/dist/src/control-plane/models/routes.js +32 -0
  73. package/dist/src/control-plane/models/service.js +138 -0
  74. package/dist/src/control-plane/observability/instruments.js +65 -0
  75. package/dist/src/control-plane/observability/metrics.js +125 -0
  76. package/dist/src/control-plane/observability/routes.js +95 -0
  77. package/dist/src/control-plane/openapi/document.js +282 -0
  78. package/dist/src/control-plane/openapi/routes.js +78 -0
  79. package/dist/src/control-plane/request-idempotency.js +85 -0
  80. package/dist/src/control-plane/secrets/envelope.js +132 -0
  81. package/dist/src/control-plane/secrets/master-key.js +44 -0
  82. package/dist/src/control-plane/secrets/routes.js +20 -0
  83. package/dist/src/control-plane/secrets/service.js +71 -0
  84. package/dist/src/control-plane/secrets/store.js +212 -0
  85. package/dist/src/control-plane/secrets/types.js +3 -0
  86. package/dist/src/control-plane/sessions/pi/custom-tools.js +146 -0
  87. package/dist/src/control-plane/sessions/pi/mcp/bridge.js +527 -0
  88. package/dist/src/control-plane/sessions/pi/mcp/client.js +157 -0
  89. package/dist/src/control-plane/sessions/pi/mcp/credential.js +28 -0
  90. package/dist/src/control-plane/sessions/pi/mcp/fetch.js +69 -0
  91. package/dist/src/control-plane/sessions/pi/mcp/probe.js +202 -0
  92. package/dist/src/control-plane/sessions/pi/mcp/runtime.js +13 -0
  93. package/dist/src/control-plane/sessions/pi/runner.js +1104 -0
  94. package/dist/src/control-plane/sessions/pi/sandbox/cma-glob.js +388 -0
  95. package/dist/src/control-plane/sessions/pi/sandbox/cma-grep.js +124 -0
  96. package/dist/src/control-plane/sessions/pi/sandbox/docker-egress.js +347 -0
  97. package/dist/src/control-plane/sessions/pi/sandbox/docker.js +1589 -0
  98. package/dist/src/control-plane/sessions/pi/sandbox/glob.js +48 -0
  99. package/dist/src/control-plane/sessions/pi/sandbox/image.js +7 -0
  100. package/dist/src/control-plane/sessions/pi/sandbox/microsandbox.js +1677 -0
  101. package/dist/src/control-plane/sessions/pi/sandbox/provider.js +561 -0
  102. package/dist/src/control-plane/sessions/pi/sandbox/selection.js +168 -0
  103. package/dist/src/control-plane/sessions/pi/span-normalizer.js +82 -0
  104. package/dist/src/control-plane/sessions/pi/tool-permissions.js +392 -0
  105. package/dist/src/control-plane/sessions/pi/translator.js +100 -0
  106. package/dist/src/control-plane/sessions/request.js +164 -0
  107. package/dist/src/control-plane/sessions/resources.js +63 -0
  108. package/dist/src/control-plane/sessions/routes.js +88 -0
  109. package/dist/src/control-plane/sessions/serialize.js +24 -0
  110. package/dist/src/control-plane/sessions/service.js +733 -0
  111. package/dist/src/control-plane/sessions/store.js +538 -0
  112. package/dist/src/control-plane/sessions/types.js +1 -0
  113. package/dist/src/control-plane/skills/archive.js +169 -0
  114. package/dist/src/control-plane/skills/routes.js +41 -0
  115. package/dist/src/control-plane/skills/service.js +29 -0
  116. package/dist/src/control-plane/skills/store.js +233 -0
  117. package/dist/src/control-plane/skills/types.js +5 -0
  118. package/dist/src/control-plane/sqlite-transaction.js +15 -0
  119. package/dist/src/control-plane/vaults/mcp-oauth-validate.js +165 -0
  120. package/dist/src/control-plane/vaults/oauth-refresh-ticker.js +43 -0
  121. package/dist/src/control-plane/vaults/oauth-refresh.js +448 -0
  122. package/dist/src/control-plane/vaults/routes.js +66 -0
  123. package/dist/src/control-plane/vaults/service.js +553 -0
  124. package/dist/src/control-plane/vaults/store.js +691 -0
  125. package/dist/src/control-plane/vaults/types.js +1 -0
  126. package/dist/src/control-plane/wake-loop.js +65 -0
  127. package/dist/src/control-plane/wiring.js +92 -0
  128. package/dist/src/control-plane/workspace.js +4 -0
  129. package/dist/src/control-plane/workspaces/store.js +158 -0
  130. package/dist/src/egress-proxy-main.js +103 -0
  131. package/dist/src/main.js +124 -0
  132. package/dist/src/types/agents.js +1 -0
  133. package/dist/src/types/common.js +1 -0
  134. package/dist/src/types/environments.js +1 -0
  135. package/dist/src/types/events.js +121 -0
  136. package/dist/src/types/files.js +1 -0
  137. package/dist/src/types/json.js +19 -0
  138. package/dist/src/types/sessions.js +1 -0
  139. package/dist/ui/managed-agents-console/README.md +52 -0
  140. package/dist/ui/managed-agents-console/docs/agents.md +23 -0
  141. package/dist/ui/managed-agents-console/docs/console.md +23 -0
  142. package/dist/ui/managed-agents-console/docs/dreams.md +5 -0
  143. package/dist/ui/managed-agents-console/docs/environments.md +27 -0
  144. package/dist/ui/managed-agents-console/docs/events.md +19 -0
  145. package/dist/ui/managed-agents-console/docs/files.md +17 -0
  146. package/dist/ui/managed-agents-console/docs/github.md +11 -0
  147. package/dist/ui/managed-agents-console/docs/integrations.md +19 -0
  148. package/dist/ui/managed-agents-console/docs/memory.md +11 -0
  149. package/dist/ui/managed-agents-console/docs/migration.md +17 -0
  150. package/dist/ui/managed-agents-console/docs/multiagent.md +11 -0
  151. package/dist/ui/managed-agents-console/docs/outcomes.md +11 -0
  152. package/dist/ui/managed-agents-console/docs/overview.md +41 -0
  153. package/dist/ui/managed-agents-console/docs/permissions.md +24 -0
  154. package/dist/ui/managed-agents-console/docs/quickstart.md +51 -0
  155. package/dist/ui/managed-agents-console/docs/reference.md +27 -0
  156. package/dist/ui/managed-agents-console/docs/sandbox-reference.md +19 -0
  157. package/dist/ui/managed-agents-console/docs/sandbox-security.md +15 -0
  158. package/dist/ui/managed-agents-console/docs/scheduled-deployments.md +5 -0
  159. package/dist/ui/managed-agents-console/docs/self-hosted-sandboxes.md +15 -0
  160. package/dist/ui/managed-agents-console/docs/session-operations.md +21 -0
  161. package/dist/ui/managed-agents-console/docs/sessions.md +21 -0
  162. package/dist/ui/managed-agents-console/docs/skills.md +15 -0
  163. package/dist/ui/managed-agents-console/docs/tools.md +23 -0
  164. package/dist/ui/managed-agents-console/docs/vaults.md +19 -0
  165. package/dist/ui/managed-agents-console/docs/webhooks.md +11 -0
  166. package/dist/ui/managed-agents-console/index.html +47 -0
  167. package/dist/ui/managed-agents-console/serve.mjs +116 -0
  168. package/dist/ui/managed-agents-console/src/agents-files.jsx +273 -0
  169. package/dist/ui/managed-agents-console/src/api.js +1043 -0
  170. package/dist/ui/managed-agents-console/src/app.jsx +633 -0
  171. package/dist/ui/managed-agents-console/src/auth.jsx +215 -0
  172. package/dist/ui/managed-agents-console/src/console.css +733 -0
  173. package/dist/ui/managed-agents-console/src/data.js +188 -0
  174. package/dist/ui/managed-agents-console/src/detail.jsx +719 -0
  175. package/dist/ui/managed-agents-console/src/docs.jsx +200 -0
  176. package/dist/ui/managed-agents-console/src/environments.jsx +365 -0
  177. package/dist/ui/managed-agents-console/src/forms.jsx +494 -0
  178. package/dist/ui/managed-agents-console/src/icons.jsx +48 -0
  179. package/dist/ui/managed-agents-console/src/skills.jsx +21 -0
  180. package/dist/ui/managed-agents-console/src/sse.js +185 -0
  181. package/dist/ui/managed-agents-console/src/states.jsx +90 -0
  182. package/dist/ui/managed-agents-console/src/tweaks-panel.jsx +541 -0
  183. package/dist/ui/managed-agents-console/src/ui.jsx +190 -0
  184. package/dist/ui/managed-agents-console/src/vaults-data.js +89 -0
  185. package/dist/ui/managed-agents-console/src/vaults.jsx +171 -0
  186. package/dist/ui/managed-agents-console/vendor/babel.min.js +4 -0
  187. package/dist/ui/managed-agents-console/vendor/react-dom.production.min.js +267 -0
  188. package/dist/ui/managed-agents-console/vendor/react.production.min.js +31 -0
  189. package/dist/ui/openapi-docs/VENDOR.md +27 -0
  190. package/dist/ui/openapi-docs/index.html +16 -0
  191. package/dist/ui/openapi-docs/swagger-initializer.js +14 -0
  192. package/dist/ui/openapi-docs/vendor/LICENSE +202 -0
  193. package/dist/ui/openapi-docs/vendor/NOTICE +2 -0
  194. package/dist/ui/openapi-docs/vendor/swagger-ui-bundle.js +2 -0
  195. package/dist/ui/openapi-docs/vendor/swagger-ui-standalone-preset.js +2 -0
  196. package/dist/ui/openapi-docs/vendor/swagger-ui.css +3 -0
  197. package/package.json +53 -0
@@ -0,0 +1,43 @@
1
+ import { createWakeLoop } from "../wake-loop.js";
2
+ export const OAUTH_TICKER_BATCH_SIZE = 50;
3
+ export const OAUTH_TICKER_CONCURRENCY = 5;
4
+ export const OAUTH_TICKER_MAX_SLEEP_MS = 15 * 60_000;
5
+ export const OAUTH_TICKER_MIN_SLEEP_MS = 30_000;
6
+ export function createOauthRefreshTicker(opts) {
7
+ const now = opts.now ?? (() => new Date());
8
+ return createWakeLoop({
9
+ nextWakeAt: () => {
10
+ const next = opts.store.nextDueRefreshAt(now().toISOString());
11
+ return next === null ? null : new Date(next);
12
+ },
13
+ run: async () => {
14
+ const due = opts.store.listDueRefreshes(now().toISOString(), OAUTH_TICKER_BATCH_SIZE);
15
+ await mapConcurrent(due, OAUTH_TICKER_CONCURRENCY, async (credential) => {
16
+ await refreshDue(opts.refresh, credential);
17
+ });
18
+ },
19
+ maxSleepMs: OAUTH_TICKER_MAX_SLEEP_MS,
20
+ minSleepMs: OAUTH_TICKER_MIN_SLEEP_MS,
21
+ onError: opts.onError,
22
+ now: () => now().getTime(),
23
+ });
24
+ }
25
+ function refreshDue(refresh, credential) {
26
+ return refresh.refreshCredential({
27
+ workspaceId: credential.workspaceId,
28
+ vaultId: credential.vaultId,
29
+ credentialId: credential.credentialId,
30
+ expectedAuthVersion: credential.authVersion,
31
+ });
32
+ }
33
+ async function mapConcurrent(values, concurrency, visit) {
34
+ let index = 0;
35
+ await Promise.all(Array.from({ length: Math.min(concurrency, values.length) }, async () => {
36
+ while (index < values.length) {
37
+ const value = values[index];
38
+ index += 1;
39
+ if (value !== undefined)
40
+ await visit(value);
41
+ }
42
+ }));
43
+ }
@@ -0,0 +1,448 @@
1
+ import { Buffer } from "node:buffer";
2
+ export const OAUTH_REFRESH_TIMEOUT_MS = 30_000;
3
+ export const OAUTH_REFRESH_MAX_BODY_BYTES = 64 * 1024;
4
+ export const OAUTH_REFRESH_LEAD_MS = 5 * 60_000;
5
+ export const OAUTH_REFRESH_FLOOR_MS = 30_000;
6
+ export const OAUTH_REFRESH_MAX_BACKOFF_MS = 15 * 60_000;
7
+ export const OAUTH_REFRESH_LONG_LIVED_RECHECK_MS = 15 * 60_000;
8
+ export const OAUTH_FORCED_REFRESH_FLOOR_MS = 60_000;
9
+ export const OAUTH_VALIDATE_REFRESH_FLOOR_MS = 10_000;
10
+ const PERMANENT_OAUTH_ERRORS = new Set([
11
+ "invalid_grant",
12
+ "invalid_refresh_token",
13
+ "invalid_client",
14
+ "unauthorized_client",
15
+ "invalid_scope",
16
+ "unsupported_grant_type",
17
+ ]);
18
+ export class RefreshCoordinator {
19
+ opts;
20
+ inflight = new Map();
21
+ forcedAdmissions = new Map();
22
+ validateAdmissions = new Map();
23
+ fetchImpl;
24
+ now;
25
+ timeoutMs;
26
+ maxBodyBytes;
27
+ random;
28
+ constructor(opts) {
29
+ this.opts = opts;
30
+ this.fetchImpl = opts.fetch;
31
+ this.now = opts.now ?? (() => new Date());
32
+ this.timeoutMs = opts.timeoutMs ?? OAUTH_REFRESH_TIMEOUT_MS;
33
+ this.maxBodyBytes = opts.maxBodyBytes ?? OAUTH_REFRESH_MAX_BODY_BYTES;
34
+ this.random = opts.random ?? Math.random;
35
+ }
36
+ refreshCredential(input) {
37
+ const key = `${input.workspaceId}\0${input.vaultId}\0${input.credentialId}`;
38
+ const existing = this.inflight.get(key);
39
+ if (existing)
40
+ return existing;
41
+ const admittedAt = this.now().getTime();
42
+ const forcedKey = `${key}\0${input.expectedAuthVersion ?? "unversioned"}`;
43
+ pruneAdmissions(this.forcedAdmissions, admittedAt, OAUTH_FORCED_REFRESH_FLOOR_MS);
44
+ pruneAdmissions(this.validateAdmissions, admittedAt, OAUTH_VALIDATE_REFRESH_FLOOR_MS);
45
+ if (input.trigger === "validate") {
46
+ const previousAdmission = this.validateAdmissions.get(key);
47
+ if (previousAdmission !== undefined &&
48
+ admittedAt - previousAdmission < OAUTH_VALIDATE_REFRESH_FLOOR_MS) {
49
+ return Promise.resolve({
50
+ outcome: "skipped",
51
+ reason: "validate_floor",
52
+ state: undefined,
53
+ });
54
+ }
55
+ this.validateAdmissions.set(key, admittedAt);
56
+ this.forcedAdmissions.set(forcedKey, admittedAt);
57
+ }
58
+ else if (input.force === true) {
59
+ const previousAdmission = this.forcedAdmissions.get(forcedKey);
60
+ if (previousAdmission !== undefined &&
61
+ admittedAt - previousAdmission < OAUTH_FORCED_REFRESH_FLOOR_MS) {
62
+ return Promise.resolve({
63
+ outcome: "skipped",
64
+ reason: "forced_refresh_floor",
65
+ state: undefined,
66
+ });
67
+ }
68
+ this.forcedAdmissions.set(forcedKey, admittedAt);
69
+ }
70
+ const promise = this.refreshCredentialOnce(input).finally(() => {
71
+ this.inflight.delete(key);
72
+ });
73
+ this.inflight.set(key, promise);
74
+ return promise;
75
+ }
76
+ recordAuthHint(input) {
77
+ return this.opts.store.persistAuthHint(input);
78
+ }
79
+ async refreshCredentialOnce(input) {
80
+ const state = this.opts.store.readOauthRefreshState(input.workspaceId, input.vaultId, input.credentialId);
81
+ if (state === undefined) {
82
+ const row = this.opts.store.retrieveCredential(input.workspaceId, input.vaultId, input.credentialId);
83
+ if (row?.auth.type === "static_bearer") {
84
+ return {
85
+ outcome: "skipped",
86
+ reason: "unsupported_credential_type",
87
+ state: undefined,
88
+ };
89
+ }
90
+ return { outcome: "skipped", reason: "missing", state };
91
+ }
92
+ if (input.expectedAuthVersion !== undefined &&
93
+ state.authVersion !== input.expectedAuthVersion) {
94
+ return {
95
+ outcome: "skipped",
96
+ reason: "stale_version",
97
+ state: publicState(state),
98
+ };
99
+ }
100
+ if (state.refreshStatus === "invalid" &&
101
+ input.force !== true &&
102
+ input.trigger !== "validate") {
103
+ return {
104
+ outcome: "skipped",
105
+ reason: "invalid_status",
106
+ state: publicState(state),
107
+ };
108
+ }
109
+ if (state.refresh === undefined) {
110
+ return {
111
+ outcome: "skipped",
112
+ reason: "no_refresh_metadata",
113
+ state: publicState(state),
114
+ };
115
+ }
116
+ if (state.secrets.refreshToken === undefined) {
117
+ return {
118
+ outcome: "skipped",
119
+ reason: "no_refresh_token",
120
+ state: publicState(state),
121
+ };
122
+ }
123
+ if (state.refresh.tokenEndpointAuth.type !== "none" &&
124
+ state.secrets.clientSecret === undefined) {
125
+ return {
126
+ outcome: "skipped",
127
+ reason: "missing_client_secret",
128
+ state: publicState(state),
129
+ };
130
+ }
131
+ const refreshed = await this.performTokenRefresh(state);
132
+ if ("accessToken" in refreshed) {
133
+ const now = this.now();
134
+ const persisted = this.opts.store.persistOauthRefreshSuccess({
135
+ workspaceId: input.workspaceId,
136
+ vaultId: input.vaultId,
137
+ credentialId: input.credentialId,
138
+ expectedAuthVersion: state.authVersion,
139
+ accessToken: refreshed.accessToken,
140
+ ...(refreshed.refreshToken === undefined
141
+ ? {}
142
+ : { refreshToken: refreshed.refreshToken }),
143
+ expiresAt: refreshed.expiresAt,
144
+ ...(refreshed.scope === undefined ? {} : { scope: refreshed.scope }),
145
+ nextRefreshAt: refreshed.expiresAt === null
146
+ ? new Date(now.getTime() + OAUTH_REFRESH_LONG_LIVED_RECHECK_MS).toISOString()
147
+ : nextRefreshAt(now, new Date(refreshed.expiresAt)).toISOString(),
148
+ updatedAt: now.toISOString(),
149
+ });
150
+ if (persisted.status === "updated")
151
+ this.opts.onScheduled?.();
152
+ return {
153
+ outcome: "ok",
154
+ state: publicState(persisted.state ?? state),
155
+ persisted: persisted.status,
156
+ tokenEndpointResponse: refreshed.tokenEndpointResponse,
157
+ };
158
+ }
159
+ const attempts = state.refreshAttempts + 1;
160
+ const now = this.now();
161
+ const status = refreshed.outcome === "invalid" ? "invalid" : "transient";
162
+ const persisted = this.opts.store.persistOauthRefreshFailure({
163
+ workspaceId: input.workspaceId,
164
+ vaultId: input.vaultId,
165
+ credentialId: input.credentialId,
166
+ expectedAuthVersion: state.authVersion,
167
+ status,
168
+ refreshAttempts: attempts,
169
+ nextRefreshAt: status === "invalid"
170
+ ? null
171
+ : new Date(now.getTime() +
172
+ (refreshed.retryAfterMs ?? transientBackoffMs(attempts, this.random()))).toISOString(),
173
+ });
174
+ if (status === "transient" && persisted.status === "updated") {
175
+ this.opts.onScheduled?.();
176
+ }
177
+ return {
178
+ outcome: refreshed.outcome,
179
+ reason: refreshed.reason,
180
+ state: persisted.state === undefined ? undefined : publicState(persisted.state),
181
+ persisted: persisted.status,
182
+ ...(refreshed.tokenEndpointResponse === undefined
183
+ ? {}
184
+ : { tokenEndpointResponse: refreshed.tokenEndpointResponse }),
185
+ };
186
+ }
187
+ async performTokenRefresh(state) {
188
+ const controller = new AbortController();
189
+ const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
190
+ let tokenEndpointResponse;
191
+ try {
192
+ const init = buildRefreshRequest(state, controller.signal);
193
+ const response = await this.fetchImpl(state.refresh.tokenEndpoint, init);
194
+ tokenEndpointResponse = {
195
+ statusCode: response.status,
196
+ // This crosses the control-plane response boundary. Keep only a validated
197
+ // media type: token endpoint headers are server controlled and may echo a
198
+ // freshly issued grant which is not in the pre-refresh scrub set yet.
199
+ contentType: safeMediaType(response.headers.get("content-type")),
200
+ };
201
+ const body = await readLimitedText(response, this.maxBodyBytes);
202
+ const parsed = parseJsonObject(body);
203
+ if (!parsed.ok) {
204
+ return {
205
+ outcome: "transient_error",
206
+ reason: parsed.reason,
207
+ retryAfterMs: retryAfterMs(response),
208
+ tokenEndpointResponse,
209
+ };
210
+ }
211
+ return {
212
+ ...classifyTokenResponse(response, parsed.value, this.now()),
213
+ tokenEndpointResponse,
214
+ };
215
+ }
216
+ catch (error) {
217
+ if (isAbortError(error)) {
218
+ return {
219
+ outcome: "transient_error",
220
+ reason: "timeout",
221
+ ...(tokenEndpointResponse === undefined ? {} : { tokenEndpointResponse }),
222
+ };
223
+ }
224
+ if (error instanceof ResponseBodyTooLargeError) {
225
+ return {
226
+ outcome: "transient_error",
227
+ reason: "response_body_too_large",
228
+ ...(tokenEndpointResponse === undefined ? {} : { tokenEndpointResponse }),
229
+ };
230
+ }
231
+ return {
232
+ outcome: "transient_error",
233
+ reason: isRedirectError(error) ? "redirect_blocked" : "request_failed",
234
+ ...(tokenEndpointResponse === undefined ? {} : { tokenEndpointResponse }),
235
+ };
236
+ }
237
+ finally {
238
+ clearTimeout(timeout);
239
+ }
240
+ }
241
+ }
242
+ export function nextRefreshAt(now, expiresAt) {
243
+ const ttlMs = expiresAt.getTime() - now.getTime();
244
+ if (ttlMs <= 0)
245
+ return new Date(now.getTime() + OAUTH_REFRESH_FLOOR_MS);
246
+ if (ttlMs < OAUTH_REFRESH_LEAD_MS) {
247
+ return new Date(now.getTime() + Math.max(OAUTH_REFRESH_FLOOR_MS, Math.floor(ttlMs / 2)));
248
+ }
249
+ return new Date(expiresAt.getTime() - OAUTH_REFRESH_LEAD_MS);
250
+ }
251
+ function transientBackoffMs(attempts, random) {
252
+ const base = Math.min(OAUTH_REFRESH_MAX_BACKOFF_MS, 60_000 * 2 ** Math.max(0, attempts - 1));
253
+ const jitter = 0.9 + Math.min(1, Math.max(0, random)) * 0.2;
254
+ return Math.min(OAUTH_REFRESH_MAX_BACKOFF_MS, Math.round(base * jitter));
255
+ }
256
+ function buildRefreshRequest(state, signal) {
257
+ const refresh = state.refresh;
258
+ const body = new URLSearchParams();
259
+ body.set("grant_type", "refresh_token");
260
+ body.set("refresh_token", state.secrets.refreshToken);
261
+ if (refresh.scope !== undefined)
262
+ body.set("scope", refresh.scope);
263
+ const headers = new Headers();
264
+ headers.set("content-type", "application/x-www-form-urlencoded");
265
+ const mode = refresh.tokenEndpointAuth.type;
266
+ if (mode === "none") {
267
+ body.set("client_id", refresh.clientId);
268
+ }
269
+ else if (mode === "client_secret_post") {
270
+ body.set("client_id", refresh.clientId);
271
+ body.set("client_secret", state.secrets.clientSecret);
272
+ }
273
+ else {
274
+ headers.set("authorization", `Basic ${Buffer.from(`${formEncodeComponent(refresh.clientId)}:${formEncodeComponent(state.secrets.clientSecret)}`).toString("base64")}`);
275
+ }
276
+ return {
277
+ method: "POST",
278
+ redirect: "error",
279
+ headers,
280
+ body,
281
+ signal,
282
+ };
283
+ }
284
+ function formEncodeComponent(value) {
285
+ const params = new URLSearchParams();
286
+ params.set("x", value);
287
+ return params.toString().slice(2);
288
+ }
289
+ function classifyTokenResponse(response, value, now) {
290
+ const oauthError = oauthErrorCode(value);
291
+ if (!response.ok || oauthError !== undefined) {
292
+ return {
293
+ outcome: oauthError !== undefined && PERMANENT_OAUTH_ERRORS.has(oauthError)
294
+ ? "invalid"
295
+ : "transient_error",
296
+ reason: oauthError ?? `http_${response.status}`,
297
+ retryAfterMs: retryAfterMs(response, now.getTime()),
298
+ };
299
+ }
300
+ if (value.ok === false &&
301
+ typeof value.error === "string" &&
302
+ value.error.length > 0) {
303
+ const error = value.error;
304
+ return {
305
+ outcome: PERMANENT_OAUTH_ERRORS.has(error) ? "invalid" : "transient_error",
306
+ reason: error,
307
+ retryAfterMs: retryAfterMs(response, now.getTime()),
308
+ };
309
+ }
310
+ if (typeof value.access_token !== "string" || value.access_token.length === 0) {
311
+ return { outcome: "transient_error", reason: "missing_access_token" };
312
+ }
313
+ if (typeof value.token_type === "string" &&
314
+ value.token_type.toLowerCase() !== "bearer") {
315
+ return { outcome: "invalid", reason: "unsupported_token_type" };
316
+ }
317
+ const expiresAt = expiresAtFromResponse(value, now);
318
+ return {
319
+ accessToken: value.access_token,
320
+ ...(typeof value.refresh_token === "string" && value.refresh_token.length > 0
321
+ ? { refreshToken: value.refresh_token }
322
+ : {}),
323
+ expiresAt,
324
+ ...(typeof value.scope === "string" ? { scope: value.scope } : {}),
325
+ };
326
+ }
327
+ function oauthErrorCode(value) {
328
+ return typeof value.error === "string" && value.error.length > 0
329
+ ? value.error
330
+ : undefined;
331
+ }
332
+ function expiresAtFromResponse(value, now) {
333
+ if (typeof value.expires_in !== "number" || !Number.isFinite(value.expires_in)) {
334
+ return null;
335
+ }
336
+ return new Date(now.getTime() + Math.max(0, value.expires_in) * 1000).toISOString();
337
+ }
338
+ function retryAfterMs(response, now = Date.now()) {
339
+ const value = response.headers.get("retry-after");
340
+ if (value === null)
341
+ return undefined;
342
+ const seconds = Number(value);
343
+ if (Number.isFinite(seconds) && seconds >= 0) {
344
+ return Math.min(OAUTH_REFRESH_MAX_BACKOFF_MS, seconds * 1000);
345
+ }
346
+ const date = Date.parse(value);
347
+ if (Number.isNaN(date))
348
+ return undefined;
349
+ return Math.min(OAUTH_REFRESH_MAX_BACKOFF_MS, Math.max(0, date - now));
350
+ }
351
+ async function readLimitedText(response, maxBytes) {
352
+ const contentType = response.headers.get("content-type") ?? "";
353
+ if (!contentType.toLowerCase().includes("json")) {
354
+ await response.body?.cancel().catch(() => undefined);
355
+ return "";
356
+ }
357
+ if (response.body === null)
358
+ return "";
359
+ const reader = response.body.getReader();
360
+ const chunks = [];
361
+ let total = 0;
362
+ try {
363
+ while (true) {
364
+ const { value, done } = await reader.read();
365
+ if (done)
366
+ break;
367
+ if (value === undefined)
368
+ continue;
369
+ total += value.byteLength;
370
+ if (total > maxBytes)
371
+ throw new ResponseBodyTooLargeError();
372
+ chunks.push(value);
373
+ }
374
+ const bytes = new Uint8Array(total);
375
+ let offset = 0;
376
+ for (const chunk of chunks) {
377
+ bytes.set(chunk, offset);
378
+ offset += chunk.byteLength;
379
+ }
380
+ return new TextDecoder().decode(bytes);
381
+ }
382
+ catch (error) {
383
+ await reader.cancel().catch(() => undefined);
384
+ throw error;
385
+ }
386
+ finally {
387
+ reader.releaseLock();
388
+ }
389
+ }
390
+ const MEDIA_TYPE = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+\/[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
391
+ function safeMediaType(value) {
392
+ const mediaType = (value ?? "").split(";", 1)[0]?.trim().toLowerCase() ?? "";
393
+ return MEDIA_TYPE.test(mediaType) ? mediaType : "application/octet-stream";
394
+ }
395
+ function parseJsonObject(text) {
396
+ if (text.length === 0)
397
+ return { ok: false, reason: "non_json_response" };
398
+ try {
399
+ const parsed = JSON.parse(text);
400
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
401
+ ? { ok: true, value: parsed }
402
+ : { ok: false, reason: "invalid_json_response" };
403
+ }
404
+ catch {
405
+ return { ok: false, reason: "invalid_json_response" };
406
+ }
407
+ }
408
+ function isAbortError(error) {
409
+ return error instanceof DOMException && error.name === "AbortError";
410
+ }
411
+ function isRedirectError(error) {
412
+ let current = error;
413
+ for (let i = 0; i < 4 && current !== undefined; i++) {
414
+ if (current instanceof Error &&
415
+ /redirect/i.test(`${current.name} ${current.message}`)) {
416
+ return true;
417
+ }
418
+ current = typeof current === "object" && current !== null && "cause" in current
419
+ ? current.cause
420
+ : undefined;
421
+ }
422
+ return false;
423
+ }
424
+ function publicState(state) {
425
+ return {
426
+ workspaceId: state.workspaceId,
427
+ vaultId: state.vaultId,
428
+ credentialId: state.credentialId,
429
+ authVersion: state.authVersion,
430
+ mcpServerUrl: state.mcpServerUrl,
431
+ ...(state.expiresAt === undefined ? {} : { expiresAt: state.expiresAt }),
432
+ refreshStatus: state.refreshStatus,
433
+ refreshAttempts: state.refreshAttempts,
434
+ nextRefreshAt: state.nextRefreshAt,
435
+ authHintAt: state.authHintAt,
436
+ hasAccessToken: state.secrets.accessToken !== undefined,
437
+ hasRefreshToken: state.secrets.refreshToken !== undefined,
438
+ hasClientSecret: state.secrets.clientSecret !== undefined,
439
+ };
440
+ }
441
+ class ResponseBodyTooLargeError extends Error {
442
+ }
443
+ function pruneAdmissions(admissions, now, floorMs) {
444
+ for (const [key, timestamp] of admissions) {
445
+ if (now - timestamp >= floorMs)
446
+ admissions.delete(key);
447
+ }
448
+ }
@@ -0,0 +1,66 @@
1
+ import { Hono } from "hono";
2
+ import { parseJsonBody, parseLimit } from "../http.js";
3
+ import { workspaceIdFrom } from "../workspace.js";
4
+ import { invalidRequest } from "../errors.js";
5
+ import { validateMcpOauthCredential, } from "./mcp-oauth-validate.js";
6
+ export function vaultsRoutes(service, mcp) {
7
+ const app = new Hono();
8
+ app.post("/", async (c) => {
9
+ const body = await parseJsonBody(c.req);
10
+ return c.json(service.createVault(workspaceIdFrom(c), body), 200);
11
+ });
12
+ app.get("/", (c) => {
13
+ return c.json(service.listVaults(workspaceIdFrom(c), listOpts((key) => c.req.query(key))), 200);
14
+ });
15
+ app.get("/:vaultId", (c) => {
16
+ return c.json(service.retrieveVault(workspaceIdFrom(c), c.req.param("vaultId")), 200);
17
+ });
18
+ app.post("/:vaultId/archive", (c) => {
19
+ return c.json(service.archiveVault(workspaceIdFrom(c), c.req.param("vaultId")), 200);
20
+ });
21
+ app.post("/:vaultId", async (c) => {
22
+ const body = await parseJsonBody(c.req);
23
+ return c.json(service.updateVault(workspaceIdFrom(c), c.req.param("vaultId"), body), 200);
24
+ });
25
+ app.delete("/:vaultId", (c) => {
26
+ return c.json(service.deleteVault(workspaceIdFrom(c), c.req.param("vaultId")), 200);
27
+ });
28
+ app.post("/:vaultId/credentials", async (c) => {
29
+ const body = await parseJsonBody(c.req);
30
+ return c.json(service.createCredential(workspaceIdFrom(c), c.req.param("vaultId"), body), 200);
31
+ });
32
+ app.get("/:vaultId/credentials", (c) => {
33
+ return c.json(service.listCredentials(workspaceIdFrom(c), c.req.param("vaultId"), listOpts((key) => c.req.query(key))), 200);
34
+ });
35
+ app.post("/:vaultId/credentials/:credentialId/archive", (c) => {
36
+ return c.json(service.archiveCredential(workspaceIdFrom(c), c.req.param("vaultId"), c.req.param("credentialId")), 200);
37
+ });
38
+ app.post("/:vaultId/credentials/:credentialId/mcp_oauth_validate", async (c) => {
39
+ if (mcp === undefined) {
40
+ throw invalidRequest("MCP is disabled on this deployment");
41
+ }
42
+ return c.json(await validateMcpOauthCredential(service, mcp, workspaceIdFrom(c), c.req.param("vaultId"), c.req.param("credentialId")), 200);
43
+ });
44
+ app.get("/:vaultId/credentials/:credentialId", (c) => {
45
+ return c.json(service.retrieveCredential(workspaceIdFrom(c), c.req.param("vaultId"), c.req.param("credentialId")), 200);
46
+ });
47
+ app.post("/:vaultId/credentials/:credentialId", async (c) => {
48
+ const body = await parseJsonBody(c.req);
49
+ return c.json(service.updateCredential(workspaceIdFrom(c), c.req.param("vaultId"), c.req.param("credentialId"), body), 200);
50
+ });
51
+ app.delete("/:vaultId/credentials/:credentialId", (c) => {
52
+ service.deleteCredential(workspaceIdFrom(c), c.req.param("vaultId"), c.req.param("credentialId"));
53
+ return c.body(null, 204);
54
+ });
55
+ return app;
56
+ }
57
+ function listOpts(query) {
58
+ const limit = parseLimit(query("limit"));
59
+ const page = query("page") || undefined;
60
+ const includeArchived = query("include_archived") === "true";
61
+ return {
62
+ ...(limit === undefined ? {} : { limit }),
63
+ ...(page === undefined ? {} : { page }),
64
+ ...(includeArchived ? { includeArchived } : {}),
65
+ };
66
+ }