aws-runtime-bridge 1.0.1 → 1.0.3

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.
@@ -1,49 +1,52 @@
1
- import { cpSync, existsSync, mkdirSync } from 'node:fs';
2
- import path from 'node:path';
3
- import { fileURLToPath } from 'node:url';
4
- import { getRuntimeHomeDir, schedulerBaseUrl } from '../config.js';
5
- import { logger } from '../utils/logger.js';
6
- export const AWS_MCP_SERVER_NAME = 'aws-mcp';
1
+ import { cpSync, existsSync, mkdirSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { getRuntimeHomeDir, port, schedulerBaseUrl } from "../config.js";
5
+ import { getRuntimeAccessToken, loadRuntimeBinding, } from "./runtime-binding.js";
6
+ import { logger } from "../utils/logger.js";
7
+ export const AWS_MCP_SERVER_NAME = "aws-mcp";
7
8
  const AWS_MCP_ALLOWED_ENV_KEYS = [
8
- 'AWS_INTERNAL_API_KEY',
9
- 'AWS_PROJECT_NAME',
10
- 'AWS_PROMPT',
11
- 'AWS_ROLE_NAME',
12
- 'AWS_SERVER_URL',
13
- 'AWS_MCP_HTTP_URL',
14
- 'AWS_HEARTBEAT_INTERVAL',
15
- 'AWS_HEARTBEAT_TIMEOUT',
16
- 'PATH',
17
- 'HOME',
18
- 'USERPROFILE',
19
- 'TMP',
20
- 'TEMP',
21
- 'SystemRoot',
22
- 'COMSPEC',
23
- 'PATHEXT',
24
- 'WINDIR',
9
+ "AWS_INTERNAL_API_KEY",
10
+ "AWS_PROJECT_NAME",
11
+ "AWS_PROMPT",
12
+ "AWS_ROLE_NAME",
13
+ "AWS_SERVER_URL",
14
+ "AWS_MCP_HTTP_URL",
15
+ "AWS_RUNTIME_ACCESS_TOKEN",
16
+ "AWS_RUNTIME_BRIDGE_BASE_URL",
17
+ "AWS_HEARTBEAT_INTERVAL",
18
+ "AWS_HEARTBEAT_TIMEOUT",
19
+ "PATH",
20
+ "HOME",
21
+ "USERPROFILE",
22
+ "TMP",
23
+ "TEMP",
24
+ "SystemRoot",
25
+ "COMSPEC",
26
+ "PATHEXT",
27
+ "WINDIR",
25
28
  ];
26
29
  function getPackageRoot() {
27
30
  const currentFile = fileURLToPath(import.meta.url);
28
- return path.resolve(path.dirname(currentFile), '..', '..');
31
+ return path.resolve(path.dirname(currentFile), "..", "..");
29
32
  }
30
33
  function getBundledEntryPath() {
31
- return path.join(getPackageRoot(), 'package', 'aws-client-agent-mcp', 'dist', 'index.js');
34
+ return path.join(getPackageRoot(), "package", "aws-client-agent-mcp", "dist", "index.js");
32
35
  }
33
36
  function getReleasedMcpRoot() {
34
- return path.join(getRuntimeHomeDir(), '.aws-bridge', 'mcp');
37
+ return path.join(getRuntimeHomeDir(), ".aws-bridge", "mcp");
35
38
  }
36
39
  export function getReleasedMcpDistPath() {
37
- return path.join(getReleasedMcpRoot(), 'dist');
40
+ return path.join(getReleasedMcpRoot(), "dist");
38
41
  }
39
42
  function getReleasedEntryPath() {
40
- return path.join(getReleasedMcpDistPath(), 'index.js');
43
+ return path.join(getReleasedMcpDistPath(), "index.js");
41
44
  }
42
45
  export function releaseBundledAwsClientAgentMcp() {
43
46
  const bundledDist = path.dirname(getBundledEntryPath());
44
47
  const releasedDist = getReleasedMcpDistPath();
45
- const releasedEntry = path.join(releasedDist, 'index.js');
46
- if (!existsSync(path.join(bundledDist, 'index.js'))) {
48
+ const releasedEntry = path.join(releasedDist, "index.js");
49
+ if (!existsSync(path.join(bundledDist, "index.js"))) {
47
50
  return null;
48
51
  }
49
52
  if (!existsSync(releasedEntry)) {
@@ -58,67 +61,90 @@ export function releaseBundledAwsClientAgentMcp() {
58
61
  return releasedEntry;
59
62
  }
60
63
  function getStringEnv() {
61
- return Object.fromEntries(Object.entries(process.env).filter((entry) => typeof entry[1] === 'string'));
64
+ return Object.fromEntries(Object.entries(process.env).filter((entry) => typeof entry[1] === "string"));
62
65
  }
63
66
  function getAllowedAwsMcpEnv(baseEnv) {
64
67
  return Object.fromEntries(AWS_MCP_ALLOWED_ENV_KEYS.flatMap((key) => {
65
68
  const value = baseEnv[key];
66
- return typeof value === 'string' && value.length > 0 ? [[key, value]] : [];
69
+ return typeof value === "string" && value.length > 0
70
+ ? [[key, value]]
71
+ : [];
67
72
  }));
68
73
  }
69
74
  function parseAwsClientAgentMcpArgs(raw) {
70
75
  try {
71
76
  const parsed = JSON.parse(raw);
72
- if (Array.isArray(parsed) && parsed.every((value) => typeof value === 'string')) {
77
+ if (Array.isArray(parsed) &&
78
+ parsed.every((value) => typeof value === "string")) {
73
79
  return parsed;
74
80
  }
75
81
  }
76
82
  catch (error) {
77
83
  logger.warn(`[runtime-bridge] AWS_CLIENT_AGENT_MCP_ARGS 不是合法 JSON,已忽略: ${error instanceof Error ? error.message : String(error)}`);
78
84
  }
79
- logger.warn('[runtime-bridge] AWS_CLIENT_AGENT_MCP_ARGS 必须是 string[] JSON,已忽略');
85
+ logger.warn("[runtime-bridge] AWS_CLIENT_AGENT_MCP_ARGS 必须是 string[] JSON,已忽略");
80
86
  return [];
81
87
  }
82
88
  function toWebSocketUrl(baseUrl) {
83
- const url = new URL('/ws/agent', baseUrl);
84
- if (url.protocol === 'https:') {
85
- url.protocol = 'wss:';
89
+ const url = new URL("/ws/agent", baseUrl);
90
+ if (url.protocol === "https:") {
91
+ url.protocol = "wss:";
86
92
  }
87
93
  else {
88
- url.protocol = 'ws:';
94
+ url.protocol = "ws:";
89
95
  }
90
96
  return url.toString();
91
97
  }
92
98
  function toMcpHttpUrl(baseUrl) {
93
- return new URL('/mcp/call', baseUrl).toString();
99
+ return new URL("/mcp/call", baseUrl).toString();
100
+ }
101
+ function resolveSchedulerBaseUrlForMcp() {
102
+ const envSchedulerBaseUrl = String(process.env.AWS_RUNTIME_SCHEDULER_BASE_URL || "").trim();
103
+ if (envSchedulerBaseUrl) {
104
+ return envSchedulerBaseUrl;
105
+ }
106
+ const binding = loadRuntimeBinding();
107
+ const pairedSchedulerBaseUrl = String(binding.schedulerBaseUrl || "").trim();
108
+ if (binding.status === "paired" && pairedSchedulerBaseUrl) {
109
+ return pairedSchedulerBaseUrl;
110
+ }
111
+ return schedulerBaseUrl;
94
112
  }
95
113
  export function resolveAwsClientAgentMcpCommand(options = {}) {
96
114
  return getAwsClientAgentMcpPreparedInfo(options);
97
115
  }
98
116
  export function getAwsClientAgentMcpPreparedInfo(options = {}) {
99
- const overrideCommand = String(process.env.AWS_CLIENT_AGENT_MCP_COMMAND || '').trim();
117
+ const overrideCommand = String(process.env.AWS_CLIENT_AGENT_MCP_COMMAND || "").trim();
100
118
  if (overrideCommand) {
101
119
  return {
102
- source: 'override',
120
+ source: "override",
103
121
  command: overrideCommand,
104
- args: process.env.AWS_CLIENT_AGENT_MCP_ARGS ? parseAwsClientAgentMcpArgs(process.env.AWS_CLIENT_AGENT_MCP_ARGS) : [],
122
+ args: process.env.AWS_CLIENT_AGENT_MCP_ARGS
123
+ ? parseAwsClientAgentMcpArgs(process.env.AWS_CLIENT_AGENT_MCP_ARGS)
124
+ : [],
105
125
  };
106
126
  }
107
127
  const exists = options.exists || existsSync;
108
128
  const releasedEntry = getReleasedEntryPath();
109
129
  if (exists(releasedEntry)) {
110
- return { source: 'bundled', command: process.execPath, args: [releasedEntry] };
130
+ return {
131
+ source: "bundled",
132
+ command: process.execPath,
133
+ args: [releasedEntry],
134
+ };
111
135
  }
112
136
  const release = options.release || releaseBundledAwsClientAgentMcp;
113
137
  const released = release();
114
138
  if (released && exists(released)) {
115
- return { source: 'bundled', command: process.execPath, args: [released] };
139
+ return { source: "bundled", command: process.execPath, args: [released] };
116
140
  }
117
- return { source: 'global', command: 'aws-client-agent-mcp', args: [] };
141
+ return { source: "global", command: "aws-client-agent-mcp", args: [] };
118
142
  }
119
143
  export function buildAwsMcpServerConfig(input) {
120
144
  const env = getStringEnv();
121
145
  const command = getAwsClientAgentMcpPreparedInfo();
146
+ const effectiveSchedulerBaseUrl = resolveSchedulerBaseUrlForMcp();
147
+ const issuedRuntimeAccessToken = getRuntimeAccessToken();
122
148
  return {
123
149
  command: command.command,
124
150
  args: command.args,
@@ -126,15 +152,21 @@ export function buildAwsMcpServerConfig(input) {
126
152
  ...getAllowedAwsMcpEnv(env),
127
153
  AWS_AGENT_ID: input.agentId,
128
154
  AWS_WORKSPACE_PATH: input.workspacePath,
129
- AWS_SERVER_URL: env.AWS_SERVER_URL || toWebSocketUrl(schedulerBaseUrl),
130
- AWS_MCP_HTTP_URL: env.AWS_MCP_HTTP_URL || toMcpHttpUrl(schedulerBaseUrl),
155
+ AWS_SERVER_URL: env.AWS_SERVER_URL || toWebSocketUrl(effectiveSchedulerBaseUrl),
156
+ AWS_MCP_HTTP_URL: env.AWS_MCP_HTTP_URL || toMcpHttpUrl(effectiveSchedulerBaseUrl),
157
+ AWS_RUNTIME_BRIDGE_BASE_URL: env.AWS_RUNTIME_BRIDGE_BASE_URL || `http://127.0.0.1:${port}`,
158
+ ...(issuedRuntimeAccessToken
159
+ ? { AWS_RUNTIME_ACCESS_TOKEN: issuedRuntimeAccessToken }
160
+ : {}),
131
161
  },
132
162
  };
133
163
  }
134
164
  export function ensureAwsClientAgentMcpReleased() {
135
165
  const prepared = getAwsClientAgentMcpPreparedInfo();
136
- const commandLine = prepared.args[0] ? `${prepared.command} ${prepared.args.join(' ')}` : prepared.command;
137
- if (prepared.source === 'global') {
166
+ const commandLine = prepared.args[0]
167
+ ? `${prepared.command} ${prepared.args.join(" ")}`
168
+ : prepared.command;
169
+ if (prepared.source === "global") {
138
170
  logger.warn(`[runtime-bridge] ${AWS_MCP_SERVER_NAME} MCP 未找到 bundled 产物,启动时将回退到 PATH 命令: ${commandLine}`);
139
171
  return;
140
172
  }
@@ -0,0 +1,42 @@
1
+ export type RuntimeBindingState = {
2
+ status: "unpaired" | "paired";
3
+ instanceId?: string;
4
+ userId?: string;
5
+ schedulerBaseUrl?: string;
6
+ /**
7
+ * Plain runtime access token issued by the scheduler.
8
+ * Stored locally with 0600 permissions so child MCP processes can authenticate
9
+ * scheduler calls without exposing the token in public status APIs.
10
+ */
11
+ accessToken?: string;
12
+ tokenHash?: string;
13
+ createdAt?: string;
14
+ updatedAt?: string;
15
+ revokedAt?: string;
16
+ };
17
+ export declare function getRuntimePairingCode(): string;
18
+ export declare function getRuntimeBindingFilePath(): string;
19
+ export declare function getRuntimeTokenStoreFilePath(): string;
20
+ export declare function buildRuntimeTokenKey(userId: unknown, serverBaseUrl: unknown): string;
21
+ export declare function saveScopedRuntimeAccessToken(input: {
22
+ userId: string;
23
+ serverBaseUrl: string;
24
+ accessToken: string;
25
+ }): string;
26
+ export declare function getScopedRuntimeAccessToken(userId: unknown, serverBaseUrl: unknown): string | undefined;
27
+ export declare function loadRuntimeBinding(): RuntimeBindingState;
28
+ export declare function getRuntimeBindingPublicState(): Omit<RuntimeBindingState, "tokenHash" | "accessToken"> & {
29
+ paired: boolean;
30
+ };
31
+ export declare function hasRuntimeBinding(): boolean;
32
+ export declare function validateRuntimeBindingToken(token: unknown): boolean;
33
+ export declare function getRuntimeAccessToken(userId?: unknown, serverBaseUrl?: unknown): string | undefined;
34
+ export declare function validateRuntimePairingCode(code: unknown): boolean;
35
+ export declare function saveRuntimeBinding(input: {
36
+ accessToken: string;
37
+ instanceId?: string;
38
+ userId?: string;
39
+ schedulerBaseUrl?: string;
40
+ }): RuntimeBindingState;
41
+ export declare function clearRuntimeBinding(): void;
42
+ //# sourceMappingURL=runtime-binding.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime-binding.d.ts","sourceRoot":"","sources":["../../src/services/runtime-binding.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,mBAAmB,GAAG;IAChC,MAAM,EAAE,UAAU,GAAG,QAAQ,CAAC;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAoDF,wBAAgB,qBAAqB,IAAI,MAAM,CAE9C;AAED,wBAAgB,yBAAyB,IAAI,MAAM,CAElD;AAED,wBAAgB,4BAA4B,IAAI,MAAM,CAErD;AAoBD,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,OAAO,EACf,aAAa,EAAE,OAAO,GACrB,MAAM,CAYR;AAsCD,wBAAgB,4BAA4B,CAAC,KAAK,EAAE;IAClD,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;CACrB,GAAG,MAAM,CAWT;AAED,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,OAAO,EACf,aAAa,EAAE,OAAO,GACrB,MAAM,GAAG,SAAS,CAUpB;AAcD,wBAAgB,kBAAkB,IAAI,mBAAmB,CAWxD;AAED,wBAAgB,4BAA4B,IAAI,IAAI,CAClD,mBAAmB,EACnB,WAAW,GAAG,aAAa,CAC5B,GAAG;IAAE,MAAM,EAAE,OAAO,CAAA;CAAE,CAWtB;AAED,wBAAgB,iBAAiB,IAAI,OAAO,CAG3C;AAED,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAcnE;AAED,wBAAgB,qBAAqB,CACnC,MAAM,CAAC,EAAE,OAAO,EAChB,aAAa,CAAC,EAAE,OAAO,GACtB,MAAM,GAAG,SAAS,CAWpB;AAED,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAEjE;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE;IACxC,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,GAAG,mBAAmB,CAuCtB;AAED,wBAAgB,mBAAmB,IAAI,IAAI,CAkB1C"}
@@ -0,0 +1,257 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { getRuntimeHomeDir } from "../config.js";
5
+ import { createLogger } from "../utils/logger.js";
6
+ const log = createLogger("runtime-binding");
7
+ const TOKEN_HASH_ALGORITHM = "sha256";
8
+ const pairingCode = generatePairingCode();
9
+ function generatePairingCode() {
10
+ const value = crypto.randomInt(0, 1_000_000);
11
+ return String(value)
12
+ .padStart(6, "0")
13
+ .replace(/(\d{3})(\d{3})/, "$1-$2");
14
+ }
15
+ function bindingDir() {
16
+ return path.join(getRuntimeHomeDir(), ".agentswork", "runtime-bridge");
17
+ }
18
+ function bindingFile() {
19
+ return path.join(bindingDir(), "binding.json");
20
+ }
21
+ function tokenStoreFile() {
22
+ return path.join(bindingDir(), "runtime-tokens.properties");
23
+ }
24
+ function hashToken(token) {
25
+ return crypto
26
+ .createHash(TOKEN_HASH_ALGORITHM)
27
+ .update(token, "utf8")
28
+ .digest("hex");
29
+ }
30
+ function safeEqual(a, b) {
31
+ const left = Buffer.from(a, "hex");
32
+ const right = Buffer.from(b, "hex");
33
+ if (left.length !== right.length) {
34
+ return false;
35
+ }
36
+ return crypto.timingSafeEqual(left, right);
37
+ }
38
+ function safeTextEqual(a, b) {
39
+ const left = Buffer.from(a, "utf8");
40
+ const right = Buffer.from(b, "utf8");
41
+ if (left.length !== right.length) {
42
+ return false;
43
+ }
44
+ return crypto.timingSafeEqual(left, right);
45
+ }
46
+ function normalizeToken(token) {
47
+ return String(token || "").trim();
48
+ }
49
+ export function getRuntimePairingCode() {
50
+ return pairingCode;
51
+ }
52
+ export function getRuntimeBindingFilePath() {
53
+ return bindingFile();
54
+ }
55
+ export function getRuntimeTokenStoreFilePath() {
56
+ return tokenStoreFile();
57
+ }
58
+ function normalizeServerIp(serverBaseUrl) {
59
+ const raw = String(serverBaseUrl || "").trim();
60
+ if (!raw) {
61
+ return "";
62
+ }
63
+ try {
64
+ return new URL(raw).hostname.toLowerCase();
65
+ }
66
+ catch {
67
+ return raw
68
+ .replace(/^wss?:\/\//i, "")
69
+ .replace(/^https?:\/\//i, "")
70
+ .split("/")[0]
71
+ .split(":")[0]
72
+ .toLowerCase();
73
+ }
74
+ }
75
+ export function buildRuntimeTokenKey(userId, serverBaseUrl) {
76
+ const normalizedUserId = String(userId || "").trim();
77
+ const serverIp = normalizeServerIp(serverBaseUrl);
78
+ if (!normalizedUserId || !serverIp) {
79
+ throw new Error("userId and serverBaseUrl are required to build runtime token key");
80
+ }
81
+ return crypto
82
+ .createHash("md5")
83
+ .update(`${normalizedUserId}:${serverIp}`, "utf8")
84
+ .digest("hex");
85
+ }
86
+ function loadRuntimeTokenStore() {
87
+ const result = new Map();
88
+ try {
89
+ const content = fs.readFileSync(tokenStoreFile(), "utf8");
90
+ for (const line of content.split(/\r?\n/)) {
91
+ const trimmed = line.trim();
92
+ if (!trimmed || trimmed.startsWith("#")) {
93
+ continue;
94
+ }
95
+ const separatorIndex = trimmed.indexOf("=");
96
+ if (separatorIndex <= 0) {
97
+ continue;
98
+ }
99
+ const key = trimmed.slice(0, separatorIndex).trim();
100
+ const token = trimmed.slice(separatorIndex + 1).trim();
101
+ if (key && token) {
102
+ result.set(key, token);
103
+ }
104
+ }
105
+ }
106
+ catch {
107
+ // Missing token store is valid for an unpaired bridge.
108
+ }
109
+ return result;
110
+ }
111
+ function saveRuntimeTokenStore(tokens) {
112
+ fs.mkdirSync(bindingDir(), { recursive: true });
113
+ const lines = [...tokens.entries()]
114
+ .sort(([left], [right]) => left.localeCompare(right))
115
+ .map(([key, token]) => `${key}=${token}`);
116
+ fs.writeFileSync(tokenStoreFile(), `${lines.join("\n")}\n`, {
117
+ encoding: "utf8",
118
+ mode: 0o600,
119
+ });
120
+ }
121
+ export function saveScopedRuntimeAccessToken(input) {
122
+ const accessToken = normalizeToken(input.accessToken);
123
+ if (accessToken.length < 16) {
124
+ throw new Error("accessToken must be at least 16 characters");
125
+ }
126
+ const key = buildRuntimeTokenKey(input.userId, input.serverBaseUrl);
127
+ const tokens = loadRuntimeTokenStore();
128
+ tokens.set(key, accessToken);
129
+ saveRuntimeTokenStore(tokens);
130
+ return key;
131
+ }
132
+ export function getScopedRuntimeAccessToken(userId, serverBaseUrl) {
133
+ if (!userId || !serverBaseUrl) {
134
+ return undefined;
135
+ }
136
+ try {
137
+ const key = buildRuntimeTokenKey(userId, serverBaseUrl);
138
+ return loadRuntimeTokenStore().get(key);
139
+ }
140
+ catch {
141
+ return undefined;
142
+ }
143
+ }
144
+ function validateRuntimeTokenStore(token) {
145
+ if (!token) {
146
+ return false;
147
+ }
148
+ for (const storedToken of loadRuntimeTokenStore().values()) {
149
+ if (safeTextEqual(token, storedToken)) {
150
+ return true;
151
+ }
152
+ }
153
+ return false;
154
+ }
155
+ export function loadRuntimeBinding() {
156
+ try {
157
+ const raw = fs.readFileSync(bindingFile(), "utf8");
158
+ const parsed = JSON.parse(raw);
159
+ if (parsed?.status === "paired" && parsed.tokenHash) {
160
+ return parsed;
161
+ }
162
+ }
163
+ catch {
164
+ // Missing or invalid binding file means the bridge is unpaired.
165
+ }
166
+ return { status: "unpaired" };
167
+ }
168
+ export function getRuntimeBindingPublicState() {
169
+ const state = loadRuntimeBinding();
170
+ const { tokenHash: _tokenHash, accessToken: _accessToken, ...publicState } = state;
171
+ return {
172
+ ...publicState,
173
+ paired: state.status === "paired",
174
+ };
175
+ }
176
+ export function hasRuntimeBinding() {
177
+ const state = loadRuntimeBinding();
178
+ return state.status === "paired" && Boolean(state.tokenHash);
179
+ }
180
+ export function validateRuntimeBindingToken(token) {
181
+ const candidate = normalizeToken(token);
182
+ if (!candidate) {
183
+ return false;
184
+ }
185
+ const state = loadRuntimeBinding();
186
+ if (state.status === "paired" && state.tokenHash) {
187
+ if (safeEqual(hashToken(candidate), state.tokenHash)) {
188
+ return true;
189
+ }
190
+ }
191
+ return validateRuntimeTokenStore(candidate);
192
+ }
193
+ export function getRuntimeAccessToken(userId, serverBaseUrl) {
194
+ const scopedToken = getScopedRuntimeAccessToken(userId, serverBaseUrl);
195
+ if (scopedToken) {
196
+ return scopedToken;
197
+ }
198
+ const state = loadRuntimeBinding();
199
+ if (state.status !== "paired") {
200
+ return undefined;
201
+ }
202
+ return normalizeToken(state.accessToken) || undefined;
203
+ }
204
+ export function validateRuntimePairingCode(code) {
205
+ return String(code || "").trim() === pairingCode;
206
+ }
207
+ export function saveRuntimeBinding(input) {
208
+ const accessToken = normalizeToken(input.accessToken);
209
+ if (accessToken.length < 16) {
210
+ throw new Error("accessToken must be at least 16 characters");
211
+ }
212
+ const previous = loadRuntimeBinding();
213
+ const now = new Date().toISOString();
214
+ const next = {
215
+ status: "paired",
216
+ instanceId: input.instanceId
217
+ ? String(input.instanceId)
218
+ : previous.instanceId,
219
+ userId: input.userId ? String(input.userId) : previous.userId,
220
+ schedulerBaseUrl: input.schedulerBaseUrl
221
+ ? String(input.schedulerBaseUrl)
222
+ : previous.schedulerBaseUrl,
223
+ accessToken,
224
+ tokenHash: hashToken(accessToken),
225
+ createdAt: previous.createdAt || now,
226
+ updatedAt: now,
227
+ };
228
+ fs.mkdirSync(bindingDir(), { recursive: true });
229
+ fs.writeFileSync(bindingFile(), `${JSON.stringify(next, null, 2)}\n`, {
230
+ encoding: "utf8",
231
+ mode: 0o600,
232
+ });
233
+ if (next.userId && next.schedulerBaseUrl) {
234
+ const key = saveScopedRuntimeAccessToken({
235
+ userId: next.userId,
236
+ serverBaseUrl: next.schedulerBaseUrl,
237
+ accessToken,
238
+ });
239
+ log.info(`[runtime-bridge] scoped runtime token saved: key=${key}`);
240
+ }
241
+ log.info(`[runtime-bridge] runtime binding saved: ${bindingFile()}`);
242
+ return next;
243
+ }
244
+ export function clearRuntimeBinding() {
245
+ const current = loadRuntimeBinding();
246
+ const revokedState = {
247
+ ...current,
248
+ status: "unpaired",
249
+ accessToken: undefined,
250
+ tokenHash: undefined,
251
+ revokedAt: new Date().toISOString(),
252
+ updatedAt: new Date().toISOString(),
253
+ };
254
+ fs.mkdirSync(bindingDir(), { recursive: true });
255
+ fs.writeFileSync(bindingFile(), `${JSON.stringify(revokedState, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
256
+ log.info("[runtime-bridge] runtime binding cleared");
257
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aws-runtime-bridge",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "AgentsWorkStudio runtime bridge service for machine-level agent runtime integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",