@playcraft/cli 0.1.15 → 0.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/login.js +79 -42
- package/dist/config.js +59 -4
- package/dist/utils/agent-api-client.js +3 -2
- package/dist/utils/environment-endpoints.js +26 -0
- package/dist/workspace-runtime/codex/model-broker.js +2 -1
- package/dist/workspace-runtime/previews/publish.js +2 -1
- package/dist/workspace-runtime/workspaces/cli-credentials.js +2 -1
- package/dist/workspace-runtime/workspaces/git-credential-broker.js +2 -1
- package/dist/workspace-runtime/workspaces/skill-artifact-loader.js +2 -1
- package/package.json +3 -3
package/dist/commands/login.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import http from 'http';
|
|
2
|
-
import {
|
|
2
|
+
import { execFile } from 'child_process';
|
|
3
3
|
import crypto from 'crypto';
|
|
4
|
-
import {
|
|
4
|
+
import { loadGlobalConfig, normalizeEnvironmentBaseUrl, readEnvironmentProfile, saveEnvironmentProfile } from '../config.js';
|
|
5
5
|
import { activateAccount } from '../auth/account-activation.js';
|
|
6
6
|
/** 默认云端 Portal(Next.js `/cli-auth` 与 API 同源时的基址;可通过 --url 或 PLAYCRAFT_URL 覆盖) */
|
|
7
7
|
export const DEFAULT_PLAYCRAFT_CLOUD_BASE = 'https://playcraft.aix.intlgame.com';
|
|
@@ -19,10 +19,11 @@ function openBrowser(url) {
|
|
|
19
19
|
const cmd = process.platform === 'darwin'
|
|
20
20
|
? 'open'
|
|
21
21
|
: process.platform === 'win32'
|
|
22
|
-
? '
|
|
22
|
+
? 'rundll32'
|
|
23
23
|
: 'xdg-open';
|
|
24
24
|
try {
|
|
25
|
-
|
|
25
|
+
const arguments_ = process.platform === 'win32' ? ['url.dll,FileProtocolHandler', url] : [url];
|
|
26
|
+
execFile(cmd, arguments_, (err, _stdout, stderr) => {
|
|
26
27
|
if (err) {
|
|
27
28
|
console.error('Failed to open browser automatically:', err.message);
|
|
28
29
|
if (stderr?.trim()) {
|
|
@@ -45,21 +46,18 @@ function resolveLoginBaseUrl(options) {
|
|
|
45
46
|
globalConfig.url ||
|
|
46
47
|
globalConfig.backendUrl ||
|
|
47
48
|
DEFAULT_PLAYCRAFT_CLOUD_BASE;
|
|
48
|
-
|
|
49
|
+
const base = normalizeEnvironmentBaseUrl(raw);
|
|
50
|
+
if (!base)
|
|
51
|
+
throw new Error('Invalid environment address');
|
|
52
|
+
return base;
|
|
49
53
|
}
|
|
50
54
|
function normalizeLoginEnvironmentUrl(raw) {
|
|
51
|
-
|
|
52
|
-
const url = new URL(raw);
|
|
53
|
-
url.hash = '';
|
|
54
|
-
url.search = '';
|
|
55
|
-
url.pathname = url.pathname.replace(/\/+$/, '');
|
|
56
|
-
return url.toString().replace(/\/+$/, '');
|
|
57
|
-
}
|
|
58
|
-
catch {
|
|
59
|
-
return undefined;
|
|
60
|
-
}
|
|
55
|
+
return normalizeEnvironmentBaseUrl(raw);
|
|
61
56
|
}
|
|
62
57
|
export function resolveExistingTokenForLoginTarget(globalConfig, backendUrl) {
|
|
58
|
+
const profile = readEnvironmentProfile(globalConfig, backendUrl);
|
|
59
|
+
if (profile)
|
|
60
|
+
return profile.token;
|
|
63
61
|
const token = typeof globalConfig.token === 'string' ? globalConfig.token : undefined;
|
|
64
62
|
const storedBackendUrl = typeof globalConfig.backendUrl === 'string'
|
|
65
63
|
? globalConfig.backendUrl
|
|
@@ -75,9 +73,17 @@ export function resolveExistingTokenForLoginTarget(globalConfig, backendUrl) {
|
|
|
75
73
|
: undefined;
|
|
76
74
|
}
|
|
77
75
|
export function buildCliAuthUrl(options) {
|
|
78
|
-
const
|
|
76
|
+
const base = normalizeEnvironmentBaseUrl(options.backendUrl);
|
|
77
|
+
if (!base)
|
|
78
|
+
throw new Error('Invalid environment address');
|
|
79
|
+
const url = new URL(`${base}/cli-auth`);
|
|
80
|
+
url.searchParams.set('callbackUrl', options.callbackUrl);
|
|
79
81
|
url.searchParams.set('callback', options.callbackUrl);
|
|
80
82
|
url.searchParams.set('state', options.state);
|
|
83
|
+
if (options.codeChallenge) {
|
|
84
|
+
url.searchParams.set('codeChallenge', options.codeChallenge);
|
|
85
|
+
url.searchParams.set('codeChallengeMethod', 'S256');
|
|
86
|
+
}
|
|
81
87
|
return url.toString();
|
|
82
88
|
}
|
|
83
89
|
export async function renewExistingToken(options) {
|
|
@@ -88,12 +94,20 @@ export async function renewExistingToken(options) {
|
|
|
88
94
|
try {
|
|
89
95
|
const res = await fetchFn(`${options.backendUrl}/api/settings/personal-access-tokens/current/renew`, {
|
|
90
96
|
method: 'PATCH',
|
|
97
|
+
redirect: 'error',
|
|
98
|
+
signal: AbortSignal.timeout(10_000),
|
|
91
99
|
headers: {
|
|
92
100
|
Authorization: `Bearer ${options.token}`,
|
|
93
101
|
'Content-Type': 'application/json',
|
|
94
102
|
},
|
|
95
103
|
body: JSON.stringify({ expiresIn: '365d' }),
|
|
96
104
|
});
|
|
105
|
+
if (res.ok && res.json && options.onRenewed) {
|
|
106
|
+
const result = await res.json();
|
|
107
|
+
if (typeof result.expiresAt === 'string' && Date.parse(result.expiresAt) > Date.now()) {
|
|
108
|
+
options.onRenewed(result.expiresAt);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
97
111
|
return res.ok;
|
|
98
112
|
}
|
|
99
113
|
catch {
|
|
@@ -110,24 +124,34 @@ export async function loginCommand(options) {
|
|
|
110
124
|
const globalConfig = loadGlobalConfig();
|
|
111
125
|
const existingToken = resolveExistingTokenForLoginTarget(globalConfig, backendUrl);
|
|
112
126
|
if (existingToken) {
|
|
113
|
-
|
|
127
|
+
let expiresAt;
|
|
128
|
+
const renewed = await renewExistingToken({ backendUrl, token: existingToken,
|
|
129
|
+
onRenewed: (value) => { expiresAt = value; } });
|
|
114
130
|
if (renewed) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
});
|
|
131
|
+
const previous = readEnvironmentProfile(globalConfig, backendUrl);
|
|
132
|
+
saveEnvironmentProfile(backendUrl, { token: existingToken,
|
|
133
|
+
...(previous?.identity ? { identity: previous.identity } : {}),
|
|
134
|
+
...(expiresAt ? { expiresAt } : {}) });
|
|
120
135
|
console.log('Existing CLI token renewed. Token saved to ~/.playcraft/config.json');
|
|
121
136
|
return;
|
|
122
137
|
}
|
|
123
138
|
}
|
|
124
139
|
const state = crypto.randomBytes(16).toString('hex');
|
|
140
|
+
const codeVerifier = crypto.randomBytes(32).toString('base64url');
|
|
141
|
+
const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url');
|
|
142
|
+
let callbackUrl = '';
|
|
143
|
+
let exchanging = false;
|
|
125
144
|
return new Promise((resolve, reject) => {
|
|
126
145
|
let timeout;
|
|
127
146
|
const server = http.createServer((req, res) => {
|
|
128
|
-
res.setHeader('Access-Control-Allow-Origin',
|
|
147
|
+
res.setHeader('Access-Control-Allow-Origin', new URL(backendUrl).origin);
|
|
129
148
|
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
|
|
130
149
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
150
|
+
if (req.headers.origin && req.headers.origin !== new URL(backendUrl).origin) {
|
|
151
|
+
res.writeHead(403);
|
|
152
|
+
res.end();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
131
155
|
if (req.method === 'OPTIONS') {
|
|
132
156
|
res.writeHead(204);
|
|
133
157
|
res.end();
|
|
@@ -135,8 +159,9 @@ export async function loginCommand(options) {
|
|
|
135
159
|
}
|
|
136
160
|
if (req.url === '/callback' && req.method === 'POST') {
|
|
137
161
|
let body = '';
|
|
138
|
-
req.on('data', (chunk) =>
|
|
139
|
-
|
|
162
|
+
req.on('data', (chunk) => { body += chunk; if (body.length > 16_384)
|
|
163
|
+
req.destroy(); });
|
|
164
|
+
req.on('end', async () => {
|
|
140
165
|
try {
|
|
141
166
|
const data = JSON.parse(body);
|
|
142
167
|
if (data.state !== state) {
|
|
@@ -144,22 +169,36 @@ export async function loginCommand(options) {
|
|
|
144
169
|
'Content-Type': 'text/html',
|
|
145
170
|
Connection: 'close',
|
|
146
171
|
});
|
|
147
|
-
res.end('<html><body><h1>Invalid state</h1></body></html>'
|
|
148
|
-
closeServer(() => {
|
|
149
|
-
reject(new Error('State mismatch - possible CSRF attack'));
|
|
150
|
-
});
|
|
151
|
-
});
|
|
172
|
+
res.end('<html><body><h1>Invalid state</h1></body></html>');
|
|
152
173
|
return;
|
|
153
174
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
175
|
+
if (exchanging) {
|
|
176
|
+
res.writeHead(409);
|
|
177
|
+
res.end();
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (typeof data.code !== 'string' || !/^[A-Za-z0-9_-]{43}$/.test(data.code)) {
|
|
181
|
+
throw new Error('Login callback did not include an authorization code');
|
|
157
182
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
183
|
+
exchanging = true;
|
|
184
|
+
const base = normalizeEnvironmentBaseUrl(backendUrl);
|
|
185
|
+
if (!base)
|
|
186
|
+
throw new Error('Invalid environment address');
|
|
187
|
+
const exchange = await fetch(`${base}/api/cloud-environments/exchange`, {
|
|
188
|
+
method: 'POST', redirect: 'error', signal: AbortSignal.timeout(10_000),
|
|
189
|
+
headers: { 'Content-Type': 'application/json' },
|
|
190
|
+
body: JSON.stringify({ code: data.code, state, callbackUrl, codeVerifier }),
|
|
162
191
|
});
|
|
192
|
+
if (!exchange.ok)
|
|
193
|
+
throw new Error('Authorization code exchange failed');
|
|
194
|
+
const credential = await exchange.json();
|
|
195
|
+
if (!credential.token || !/^playcraft_pat_[A-Za-z0-9_-]{32}$/.test(credential.token)
|
|
196
|
+
|| !credential.identity?.userId || !Array.isArray(credential.identity.identities)
|
|
197
|
+
|| !credential.expiresAt || Date.parse(credential.expiresAt) <= Date.now()) {
|
|
198
|
+
throw new Error('Invalid environment credential response');
|
|
199
|
+
}
|
|
200
|
+
saveEnvironmentProfile(base, { token: credential.token,
|
|
201
|
+
identity: credential.identity, expiresAt: credential.expiresAt });
|
|
163
202
|
res.writeHead(200, {
|
|
164
203
|
'Content-Type': 'text/html',
|
|
165
204
|
Connection: 'close',
|
|
@@ -179,9 +218,6 @@ export async function loginCommand(options) {
|
|
|
179
218
|
catch (err) {
|
|
180
219
|
const cause = err instanceof Error ? err : new Error(String(err));
|
|
181
220
|
console.error('[playcraft login] Callback handling failed:', cause.message);
|
|
182
|
-
if (cause.stack) {
|
|
183
|
-
console.error(cause.stack);
|
|
184
|
-
}
|
|
185
221
|
const userFacing = cause instanceof SyntaxError
|
|
186
222
|
? 'Invalid JSON in login callback.'
|
|
187
223
|
: `Login callback failed: ${cause.message}`;
|
|
@@ -226,14 +262,15 @@ export async function loginCommand(options) {
|
|
|
226
262
|
});
|
|
227
263
|
});
|
|
228
264
|
try {
|
|
229
|
-
server.listen(callbackPort, () => {
|
|
265
|
+
server.listen(callbackPort, '127.0.0.1', () => {
|
|
230
266
|
const address = server.address();
|
|
231
267
|
const listeningPort = typeof address === 'object' && address ? address.port : callbackPort;
|
|
232
|
-
|
|
268
|
+
callbackUrl = `http://127.0.0.1:${listeningPort}/callback`;
|
|
233
269
|
const authUrl = buildCliAuthUrl({
|
|
234
270
|
backendUrl,
|
|
235
271
|
callbackUrl,
|
|
236
272
|
state,
|
|
273
|
+
codeChallenge,
|
|
237
274
|
});
|
|
238
275
|
console.log('Opening browser for login...');
|
|
239
276
|
console.log(`If the browser does not open, visit this URL manually:\n ${authUrl}\n`);
|
package/dist/config.js
CHANGED
|
@@ -2,7 +2,8 @@ import { cosmiconfig } from 'cosmiconfig';
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { homedir } from 'os';
|
|
5
|
-
import { mkdirSync, writeFileSync, readFileSync } from 'fs';
|
|
5
|
+
import { chmodSync, mkdirSync, renameSync, rmSync, writeFileSync, readFileSync } from 'fs';
|
|
6
|
+
import { randomUUID } from 'node:crypto';
|
|
6
7
|
import 'dotenv/config';
|
|
7
8
|
export const ConfigSchema = z.object({
|
|
8
9
|
projectId: z.string().optional(),
|
|
@@ -18,6 +19,33 @@ const explorer = cosmiconfig('playcraft', {
|
|
|
18
19
|
});
|
|
19
20
|
const GLOBAL_CONFIG_DIR = path.join(homedir(), '.playcraft');
|
|
20
21
|
const GLOBAL_CONFIG_FILE = path.join(GLOBAL_CONFIG_DIR, 'config.json');
|
|
22
|
+
export function normalizeEnvironmentBaseUrl(raw) {
|
|
23
|
+
try {
|
|
24
|
+
const url = new URL(raw);
|
|
25
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password)
|
|
26
|
+
return undefined;
|
|
27
|
+
url.hash = '';
|
|
28
|
+
url.search = '';
|
|
29
|
+
url.pathname = url.pathname.replace(/\/+$/, '').replace(/\/api$/, '');
|
|
30
|
+
return url.toString().replace(/\/+$/, '');
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export function readEnvironmentProfile(config, baseUrl) {
|
|
37
|
+
const key = normalizeEnvironmentBaseUrl(baseUrl);
|
|
38
|
+
const profiles = config.environmentProfiles;
|
|
39
|
+
if (!key || !profiles || typeof profiles !== 'object' || Array.isArray(profiles))
|
|
40
|
+
return undefined;
|
|
41
|
+
const profile = profiles[key];
|
|
42
|
+
if (!profile || typeof profile !== 'object' || Array.isArray(profile))
|
|
43
|
+
return undefined;
|
|
44
|
+
const value = profile;
|
|
45
|
+
if (typeof value.token !== 'string' || !value.token || normalizeEnvironmentBaseUrl(value.backendUrl) !== key)
|
|
46
|
+
return undefined;
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
21
49
|
export function loadGlobalConfig() {
|
|
22
50
|
try {
|
|
23
51
|
const content = readFileSync(GLOBAL_CONFIG_FILE, 'utf-8');
|
|
@@ -28,7 +56,8 @@ export function loadGlobalConfig() {
|
|
|
28
56
|
}
|
|
29
57
|
}
|
|
30
58
|
export function saveGlobalConfig(partial) {
|
|
31
|
-
mkdirSync(GLOBAL_CONFIG_DIR, { recursive: true });
|
|
59
|
+
mkdirSync(GLOBAL_CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
60
|
+
chmodSync(GLOBAL_CONFIG_DIR, 0o700);
|
|
32
61
|
let existing = {};
|
|
33
62
|
try {
|
|
34
63
|
existing = JSON.parse(readFileSync(GLOBAL_CONFIG_FILE, 'utf-8'));
|
|
@@ -37,7 +66,25 @@ export function saveGlobalConfig(partial) {
|
|
|
37
66
|
// ignore
|
|
38
67
|
}
|
|
39
68
|
const merged = { ...existing, ...partial };
|
|
40
|
-
|
|
69
|
+
const temporary = path.join(GLOBAL_CONFIG_DIR, `.config-${randomUUID()}.tmp`);
|
|
70
|
+
try {
|
|
71
|
+
writeFileSync(temporary, JSON.stringify(merged, null, 2), { encoding: 'utf-8', flag: 'wx', mode: 0o600 });
|
|
72
|
+
renameSync(temporary, GLOBAL_CONFIG_FILE);
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
rmSync(temporary, { force: true });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export function saveEnvironmentProfile(baseUrl, profile) {
|
|
79
|
+
const key = normalizeEnvironmentBaseUrl(baseUrl);
|
|
80
|
+
if (!key)
|
|
81
|
+
throw new Error('Invalid environment address');
|
|
82
|
+
const current = loadGlobalConfig();
|
|
83
|
+
const profiles = current.environmentProfiles && typeof current.environmentProfiles === 'object'
|
|
84
|
+
&& !Array.isArray(current.environmentProfiles) ? current.environmentProfiles : {};
|
|
85
|
+
saveGlobalConfig({ environmentProfiles: { ...profiles,
|
|
86
|
+
[key]: { ...profile, backendUrl: key, updatedAt: new Date().toISOString() } },
|
|
87
|
+
token: profile.token, url: key, backendUrl: key });
|
|
41
88
|
}
|
|
42
89
|
export async function loadConfig(cliOptions) {
|
|
43
90
|
const result = await explorer.search(cliOptions.dir || process.cwd());
|
|
@@ -46,12 +93,20 @@ export async function loadConfig(cliOptions) {
|
|
|
46
93
|
? (fileConfig.agent || {})
|
|
47
94
|
: fileConfig;
|
|
48
95
|
const globalConfig = loadGlobalConfig();
|
|
96
|
+
const explicitTarget = cliOptions.url || process.env.PLAYCRAFT_URL || agentConfig.url;
|
|
97
|
+
const targetUrl = explicitTarget || globalConfig.url;
|
|
98
|
+
const profile = typeof targetUrl === 'string' ? readEnvironmentProfile(globalConfig, targetUrl) : undefined;
|
|
99
|
+
const legacyTarget = typeof globalConfig.backendUrl === 'string' ? globalConfig.backendUrl
|
|
100
|
+
: typeof globalConfig.url === 'string' ? globalConfig.url : undefined;
|
|
101
|
+
const legacyToken = !explicitTarget || (legacyTarget && normalizeEnvironmentBaseUrl(String(targetUrl)) === normalizeEnvironmentBaseUrl(legacyTarget))
|
|
102
|
+
? globalConfig.token : undefined;
|
|
49
103
|
const config = ConfigSchema.parse({
|
|
50
104
|
...globalConfig,
|
|
51
105
|
...agentConfig,
|
|
52
106
|
...cliOptions,
|
|
53
107
|
projectId: cliOptions.projectId || process.env.PLAYCRAFT_PROJECT_ID || agentConfig.projectId || globalConfig.projectId,
|
|
54
|
-
|
|
108
|
+
url: targetUrl,
|
|
109
|
+
token: cliOptions.token || process.env.PLAYCRAFT_TOKEN || agentConfig.token || profile?.token || legacyToken,
|
|
55
110
|
port: cliOptions.port || (process.env.PLAYCRAFT_PORT ? parseInt(process.env.PLAYCRAFT_PORT) : undefined) || agentConfig.port || globalConfig.port,
|
|
56
111
|
mode: cliOptions.mode || process.env.PLAYCRAFT_MODE || agentConfig.mode || globalConfig.mode,
|
|
57
112
|
skillsDir: cliOptions.skillsDir || process.env.AGENT_SKILLS_DIR || agentConfig.skillsDir || globalConfig.skillsDir,
|
|
@@ -5,6 +5,7 @@ import { loadGlobalConfig } from '../config.js';
|
|
|
5
5
|
import { DEFAULT_PLAYCRAFT_CLOUD_BASE } from '../commands/login.js';
|
|
6
6
|
import { AGENT_TOOL_OPERATION_PROTOCOL, AGENT_TOOL_OPERATION_HEADERS, AGENT_TOOL_OPERATION_CAPABILITIES_PATH, } from '@playcraft/common/agent-tools';
|
|
7
7
|
import { beginToolOperation } from './tool-operation-journal.js';
|
|
8
|
+
import { businessEndpoint } from './environment-endpoints.js';
|
|
8
9
|
import { AccountActivationError, parseErrorPayload, pendingAccountActivationError, } from '../auth/account-activation.js';
|
|
9
10
|
/**
|
|
10
11
|
* 判断 token 是否为用户 PAT(Personal Access Token)。
|
|
@@ -71,10 +72,10 @@ export class AgentApiClient {
|
|
|
71
72
|
}
|
|
72
73
|
return headers;
|
|
73
74
|
}
|
|
74
|
-
/**
|
|
75
|
+
/** Deployment-relative pathname under /api/agent/tools (path may omit leading slash). */
|
|
75
76
|
toolsRequestUrl(path) {
|
|
76
77
|
const p = path.startsWith('/') ? path : `/${path}`;
|
|
77
|
-
return
|
|
78
|
+
return businessEndpoint(this.apiUrl, `/api/agent/tools${p}`);
|
|
78
79
|
}
|
|
79
80
|
async post(path, body, options) {
|
|
80
81
|
const url = this.toolsRequestUrl(path);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
function publicBase(value) {
|
|
2
|
+
const base = new URL(value);
|
|
3
|
+
if (!['http:', 'https:'].includes(base.protocol) || base.username || base.password || base.search || base.hash) {
|
|
4
|
+
throw new Error('invalid_environment_endpoint');
|
|
5
|
+
}
|
|
6
|
+
base.pathname = base.pathname.replace(/\/+$/, '');
|
|
7
|
+
return base;
|
|
8
|
+
}
|
|
9
|
+
function append(base, path) {
|
|
10
|
+
return new URL(`${base.toString().replace(/\/+$/, '')}/${path.replace(/^\/+/, '')}`);
|
|
11
|
+
}
|
|
12
|
+
/** Keep the deployment prefix when deriving a control endpoint from its callback address. */
|
|
13
|
+
export function runtimeCallbackEndpoint(callbackUrl, path) {
|
|
14
|
+
const base = publicBase(callbackUrl);
|
|
15
|
+
base.pathname = base.pathname.replace(/\/runtime\/(?:callbacks|internal\/events)$/, '');
|
|
16
|
+
// Older local runtimes used a root-level events callback.
|
|
17
|
+
if (base.pathname === '/events')
|
|
18
|
+
base.pathname = '';
|
|
19
|
+
return append(base, path);
|
|
20
|
+
}
|
|
21
|
+
/** Accept either a public deployment base or its /api endpoint, without duplicating /api. */
|
|
22
|
+
export function businessEndpoint(apiUrl, path) {
|
|
23
|
+
const base = publicBase(apiUrl);
|
|
24
|
+
base.pathname = base.pathname.replace(/\/api$/, '');
|
|
25
|
+
return append(base, path);
|
|
26
|
+
}
|
|
@@ -5,6 +5,7 @@ import { pipeline } from 'node:stream/promises';
|
|
|
5
5
|
import { basename } from 'node:path';
|
|
6
6
|
import { z } from 'zod';
|
|
7
7
|
import { workspaceRoot } from '../workspaces/registry.js';
|
|
8
|
+
import { runtimeCallbackEndpoint } from '../../utils/environment-endpoints.js';
|
|
8
9
|
const credentialSchema = z.object({
|
|
9
10
|
token: z.string().min(1), expiresAt: z.string().datetime(),
|
|
10
11
|
configId: z.string().uuid(), model: z.string().min(1).max(128),
|
|
@@ -81,7 +82,7 @@ export class WorkspaceModelBroker {
|
|
|
81
82
|
}
|
|
82
83
|
async renew(entry) {
|
|
83
84
|
const config = this.state.config;
|
|
84
|
-
const endpoint =
|
|
85
|
+
const endpoint = runtimeCallbackEndpoint(config.callbackUrl, '/runtime/internal/model-credentials');
|
|
85
86
|
if (endpoint.protocol !== 'https:' && !(endpoint.protocol === 'http:'
|
|
86
87
|
&& ['127.0.0.1', 'localhost', '[::1]'].includes(endpoint.hostname)))
|
|
87
88
|
throw new Error('model_callback_insecure');
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
|
+
import { runtimeCallbackEndpoint } from '../../utils/environment-endpoints.js';
|
|
3
4
|
import { constants } from 'node:fs';
|
|
4
5
|
import { lstat, open, readdir, rm } from 'node:fs/promises';
|
|
5
6
|
import { join } from 'node:path';
|
|
@@ -222,7 +223,7 @@ function git(cwd, args) {
|
|
|
222
223
|
});
|
|
223
224
|
}
|
|
224
225
|
async function postControl(state, path, body) {
|
|
225
|
-
const endpoint =
|
|
226
|
+
const endpoint = runtimeCallbackEndpoint(state.config.callbackUrl, path);
|
|
226
227
|
const local = endpoint.protocol === 'http:' && ['127.0.0.1', 'localhost', '[::1]'].includes(endpoint.hostname);
|
|
227
228
|
if (endpoint.protocol !== 'https:' && !local)
|
|
228
229
|
throw new PreviewFailure('preview_upload_failed');
|
|
@@ -2,6 +2,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { mkdir, rename, rm, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
+
import { runtimeCallbackEndpoint } from '../../utils/environment-endpoints.js';
|
|
5
6
|
const responseSchema = z.object({
|
|
6
7
|
token: z.string().min(1),
|
|
7
8
|
apiUrl: z.string().url(),
|
|
@@ -36,7 +37,7 @@ export async function ensureCliCredentials(state) {
|
|
|
36
37
|
async function renew(state, entry) {
|
|
37
38
|
const config = state.config;
|
|
38
39
|
const epoch = state.runtimeEpoch;
|
|
39
|
-
const endpoint =
|
|
40
|
+
const endpoint = runtimeCallbackEndpoint(config.callbackUrl, '/runtime/internal/cli-credentials');
|
|
40
41
|
if (endpoint.protocol !== 'https:' &&
|
|
41
42
|
!(endpoint.protocol === 'http:' &&
|
|
42
43
|
['127.0.0.1', 'localhost', '[::1]'].includes(endpoint.hostname)))
|
|
@@ -8,6 +8,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
8
8
|
import { promisify } from 'node:util';
|
|
9
9
|
import { isLocalWorkspaceGitAuthMode, isLocalWorkspaceGitEnvironment, matchWorkspaceGitGatewayCloneUrl, workspacePreparationSchema } from '@playcraft/common/cloud-workspace/runtime';
|
|
10
10
|
import { WorkspaceGitDirectStore } from './git-direct-store.js';
|
|
11
|
+
import { runtimeCallbackEndpoint } from '../../utils/environment-endpoints.js';
|
|
11
12
|
const exec = promisify(execFile);
|
|
12
13
|
/** Only this control process sees the callback credential. Development processes use the local socket. */
|
|
13
14
|
export class WorkspaceGitCredentialBroker {
|
|
@@ -164,7 +165,7 @@ export class WorkspaceGitCredentialBroker {
|
|
|
164
165
|
await this.direct.sweep(true);
|
|
165
166
|
}
|
|
166
167
|
async refresh(binding, key) {
|
|
167
|
-
const endpoint =
|
|
168
|
+
const endpoint = runtimeCallbackEndpoint(this.state.config.callbackUrl, '/runtime/internal/git-credentials');
|
|
168
169
|
if (endpoint.protocol !== 'https:' && !(endpoint.protocol === 'http:' && ['127.0.0.1', 'localhost', '[::1]'].includes(endpoint.hostname)))
|
|
169
170
|
throw new Error('git_broker_unavailable');
|
|
170
171
|
const epoch = this.state.runtimeEpoch;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { workspaceRoot } from './registry.js';
|
|
3
|
+
import { runtimeCallbackEndpoint } from '../../utils/environment-endpoints.js';
|
|
3
4
|
const MAX_ARTIFACT_BYTES = 80 * 1024 * 1024;
|
|
4
5
|
/** Requests an exact locked artifact using control identity; no storage credentials reach Codex. */
|
|
5
6
|
export async function loadWorkspaceSkillArtifact(state, workspaceId, entry) {
|
|
@@ -7,7 +8,7 @@ export async function loadWorkspaceSkillArtifact(state, workspaceId, entry) {
|
|
|
7
8
|
const epoch = state.runtimeEpoch;
|
|
8
9
|
if (state.journal.destroyed())
|
|
9
10
|
throw new Error('workspace_skills_unavailable');
|
|
10
|
-
const endpoint =
|
|
11
|
+
const endpoint = runtimeCallbackEndpoint(state.config.callbackUrl, '/runtime/internal/skill-artifacts');
|
|
11
12
|
if (endpoint.protocol !== 'https:' && !(endpoint.protocol === 'http:' && ['127.0.0.1', 'localhost', '[::1]'].includes(endpoint.hostname)))
|
|
12
13
|
throw new Error('workspace_skills_callback_insecure');
|
|
13
14
|
const response = await fetch(endpoint, {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playcraft/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -48,8 +48,8 @@
|
|
|
48
48
|
"@gltf-transform/core": "^4.3.0",
|
|
49
49
|
"@gltf-transform/extensions": "^4.3.0",
|
|
50
50
|
"@gltf-transform/functions": "^4.3.0",
|
|
51
|
-
"@playcraft/build": "^0.1.
|
|
52
|
-
"@playcraft/common": "^0.1.
|
|
51
|
+
"@playcraft/build": "^0.1.16",
|
|
52
|
+
"@playcraft/common": "^0.1.16",
|
|
53
53
|
"better-sqlite3-pinned": "npm:better-sqlite3@13.0.3",
|
|
54
54
|
"chokidar": "^4.0.3",
|
|
55
55
|
"commander": "^13.1.0",
|