diff --git a/CHANGELOG.md b/CHANGELOG.md index eb1515ae..face3b95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 7.15.3 - 新增沙箱(Sandbox)模块,支持沙箱实例、文件系统、命令执行、Git、PTY、模板、资源挂载、请求注入等能力 - 新增沙箱模块的 TypeScript 类型声明、示例和测试覆盖 +- 沙箱请求注入支持 `baseUrl` 路径前缀匹配和 `ifHeaders`、`ifQueries` 条件匹配 ## 7.15.2 - 对象存储,修复 pfop pipeline 参数无效问题 diff --git a/examples/sandbox_injection_rules.js b/examples/sandbox_injection_rules.js index 0b45f673..9f039069 100644 --- a/examples/sandbox_injection_rules.js +++ b/examples/sandbox_injection_rules.js @@ -23,7 +23,10 @@ runExample(() => { name: ruleName, injection: { type: 'http', - baseUrl: 'https://httpbin.org', + baseUrl: 'https://httpbin.org/bearer', + ifHeaders: { + 'X-Sandbox-Example': 'qiniu-nodejs-sdk' + }, headers: { Authorization: `Bearer ${env('QINIU_SANDBOX_HTTP_INJECTION_TOKEN', 'real_token')}` } @@ -38,7 +41,10 @@ runExample(() => { name: `${ruleName}-updated`, injection: { type: 'http', - baseUrl: 'https://httpbin.org', + baseUrl: 'https://httpbin.org/bearer', + ifHeaders: { + 'X-Sandbox-Example': 'qiniu-nodejs-sdk' + }, headers: { Authorization: `Bearer ${env('QINIU_SANDBOX_HTTP_INJECTION_TOKEN', 'updated_token')}`, 'X-Sandbox-Example': 'qiniu-nodejs-sdk' @@ -64,7 +70,7 @@ runExample(() => { }); }).then(created => { sandbox = created; - return sandbox.commands.run('curl --max-time 20 -sSL https://httpbin.org/bearer -H "Authorization: Bearer fake_token"', { + return sandbox.commands.run('curl --max-time 20 -sSL https://httpbin.org/bearer -H "Authorization: Bearer fake_token" -H "X-Sandbox-Example: qiniu-nodejs-sdk"', { timeout: 30000 }); }).then(result => { diff --git a/examples/sandbox_request_injections.js b/examples/sandbox_request_injections.js index 189dde77..9a91490c 100644 --- a/examples/sandbox_request_injections.js +++ b/examples/sandbox_request_injections.js @@ -12,7 +12,10 @@ runExample(() => { injections: [ { type: 'http', - base_url: 'https://httpbin.org', + baseUrl: 'https://httpbin.org/bearer', + ifHeaders: { + 'X-Sandbox-Example': 'qiniu-nodejs-sdk' + }, headers: { Authorization: `Bearer ${env('QINIU_SANDBOX_HTTP_INJECTION_TOKEN', 'real_token')}` } @@ -23,7 +26,7 @@ runExample(() => { } }).then(created => { sandbox = created; - return sandbox.commands.run('curl --max-time 20 -sSL https://httpbin.org/bearer -H "Authorization: Bearer fake_token"', { + return sandbox.commands.run('curl --max-time 20 -sSL https://httpbin.org/bearer -H "Authorization: Bearer fake_token" -H "X-Sandbox-Example: qiniu-nodejs-sdk"', { timeout: 30000 }); }).then(result => { diff --git a/index.d.ts b/index.d.ts index 33f2f50e..e6824463 100644 --- a/index.d.ts +++ b/index.d.ts @@ -70,6 +70,85 @@ export declare namespace sandbox { type SandboxResource = GitRepositoryResource | KodoResource; + type InjectionConditions = {[key: string]: string}; + + interface InjectionMatchOptions { + base_url?: string; + baseUrl?: string; + if_headers?: InjectionConditions; + ifHeaders?: InjectionConditions; + if_queries?: InjectionConditions; + ifQueries?: InjectionConditions; + } + + interface InjectionById { + type: 'id'; + id?: string; + ruleID?: string; + ruleId?: string; + injectionRuleID?: string; + } + + interface InjectionRuleReference { + injectionRuleID: string; + } + + interface HttpInjection extends InjectionMatchOptions { + type: 'http'; + headers?: {[key: string]: string}; + } + + interface OpenaiInjection extends InjectionMatchOptions { + type: 'openai'; + api_key?: string; + apiKey?: string; + } + + interface AnthropicInjection extends InjectionMatchOptions { + type: 'anthropic'; + api_key?: string; + apiKey?: string; + } + + interface GeminiInjection extends InjectionMatchOptions { + type: 'gemini'; + api_key?: string; + apiKey?: string; + } + + interface QiniuInjection extends InjectionMatchOptions { + type: 'qiniu'; + api_key?: string; + apiKey?: string; + } + + interface GithubInjection extends InjectionMatchOptions { + type: 'github'; + token: string; + } + + type Injection = HttpInjection | OpenaiInjection | AnthropicInjection | GeminiInjection | QiniuInjection | GithubInjection; + type SandboxInjection = InjectionById | InjectionRuleReference | Injection; + + interface InjectionRule { + ruleID?: string; + id?: string; + name: string; + createdAt?: string | Date; + updatedAt?: string | Date; + injection: Injection; + } + + interface CreateInjectionRuleOptions { + name: string; + injection: Injection; + } + + interface UpdateInjectionRuleOptions { + name?: string; + injection?: Injection; + } + interface SandboxCreateOptions extends SandboxClientOptions { template?: string; templateID?: string; @@ -84,7 +163,7 @@ export declare namespace sandbox { envVars?: {[key: string]: string}; envs?: {[key: string]: string}; mcp?: any; - injections?: any[]; + injections?: SandboxInjection[]; resources?: SandboxResource[]; client?: SandboxClient; } @@ -403,10 +482,10 @@ export declare namespace sandbox { deleteTemplateTags(options?: any): Promise; getTemplateByAlias(alias: string): Promise; - listInjectionRules(): Promise; - createInjectionRule(options?: any): Promise; - getInjectionRule(ruleID: string): Promise; - updateInjectionRule(ruleID: string, options?: any): Promise; + listInjectionRules(): Promise; + createInjectionRule(options: CreateInjectionRuleOptions): Promise; + getInjectionRule(ruleID: string): Promise; + updateInjectionRule(ruleID: string, options: UpdateInjectionRuleOptions): Promise; deleteInjectionRule(ruleID: string): Promise; create(options?: SandboxCreateOptions): Promise; diff --git a/qiniu/sandbox/client.js b/qiniu/sandbox/client.js index 12fdddff..b5e825c7 100644 --- a/qiniu/sandbox/client.js +++ b/qiniu/sandbox/client.js @@ -64,6 +64,14 @@ function normalizeInjection (injection) { normalized.base_url = normalized.baseUrl; delete normalized.baseUrl; } + if (normalized.ifHeaders !== undefined && normalized.if_headers === undefined) { + normalized.if_headers = normalized.ifHeaders; + delete normalized.ifHeaders; + } + if (normalized.ifQueries !== undefined && normalized.if_queries === undefined) { + normalized.if_queries = normalized.ifQueries; + delete normalized.ifQueries; + } if (normalized.ruleId !== undefined && normalized.ruleID === undefined) { normalized.ruleID = normalized.ruleId; delete normalized.ruleId; @@ -172,6 +180,9 @@ SandboxClient.prototype._request = function (method, path, options) { const body = options.body; const hasBody = body !== undefined && body !== null; const headers = this._headers(options.authType); + if (!hasBody && (method === 'GET' || method === 'HEAD')) { + delete headers['Content-Type']; + } const urllibOptions = { method, headers, diff --git a/test/sandbox_client.test.js b/test/sandbox_client.test.js index 69d968a8..17a1d137 100644 --- a/test/sandbox_client.test.js +++ b/test/sandbox_client.test.js @@ -37,6 +37,12 @@ describe('test sandbox client module', function () { type: 'qiniu', apiKey: 'ak', baseUrl: 'https://example.com', + ifHeaders: { + 'X-Provider': 'qiniu' + }, + ifQueries: { + model: 'default' + }, ruleId: 'rule_1' } ] @@ -62,6 +68,12 @@ describe('test sandbox client module', function () { type: 'qiniu', api_key: 'ak', base_url: 'https://example.com', + if_headers: { + 'X-Provider': 'qiniu' + }, + if_queries: { + model: 'default' + }, ruleID: 'rule_1' } ] @@ -386,7 +398,14 @@ describe('test sandbox client module', function () { name: 'openai', injection: { type: 'openai', - apiKey: 'secret' + apiKey: 'secret', + baseUrl: 'https://api.openai.com/v1/*', + ifHeaders: { + 'X-Use-Injected-Key': 'true' + }, + ifQueries: { + source: 'sdk-test' + } } }).then(() => { fixture.requests[0].method.should.eql('POST'); @@ -397,7 +416,14 @@ describe('test sandbox client module', function () { name: 'openai', injection: { type: 'openai', - api_key: 'secret' + api_key: 'secret', + base_url: 'https://api.openai.com/v1/*', + if_headers: { + 'X-Use-Injected-Key': 'true' + }, + if_queries: { + source: 'sdk-test' + } } }); }).then(() => closeServer(fixture.server), err => { @@ -408,6 +434,38 @@ describe('test sandbox client module', function () { }); }); + it('uses the signing default content type for Qiniu-auth GET requests without a body', function () { + return startServer((req, res) => { + res.statusCode = 200; + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify({ + id: 'rule_1', + name: 'openai', + injection: { + type: 'openai' + } + })); + }).then(fixture => { + const client = new qiniu.sandbox.SandboxClient({ + endpoint: fixture.endpoint, + mac: new qiniu.auth.digest.Mac('ak', 'sk', { + disableQiniuTimestampSignature: true + }) + }); + + return client.getInjectionRule('rule_1').then(() => { + fixture.requests[0].method.should.eql('GET'); + fixture.requests[0].url.should.eql('/injection-rules/rule_1'); + should(fixture.requests[0].headers.authorization).startWith('Qiniu ak:'); + fixture.requests[0].headers['content-type'].should.eql('application/x-www-form-urlencoded'); + }).then(() => closeServer(fixture.server), err => { + return closeServer(fixture.server).then(() => { + throw err; + }); + }); + }); + }); + it('maps sandbox lifecycle and metrics APIs', function () { return startServer((req, res) => { res.setHeader('Content-Type', 'application/json'); diff --git a/test/sandbox_types.ts b/test/sandbox_types.ts index 09bc149f..f341f2f8 100644 --- a/test/sandbox_types.ts +++ b/test/sandbox_types.ts @@ -19,10 +19,38 @@ async function useSandboxTypes () { mount_path: '/workspace/repo', authorization_token: 'token' }; + const qiniuInjection: qiniu.sandbox.QiniuInjection = { + type: 'qiniu', + apiKey: 'sandbox-qiniu-ai-key', + baseUrl: 'https://api.qnaigc.com/v1/*', + ifHeaders: { + 'X-Use-Injected-Key': 'true' + }, + ifQueries: { + model: 'qiniu-default' + } + }; + const githubInjection: qiniu.sandbox.GithubInjection = { + type: 'github', + token: 'github-token', + base_url: 'https://api.github.com/repos/qiniu/*', + if_headers: { + Accept: 'application/vnd.github+json' + }, + if_queries: { + per_page: '100' + } + }; const sandbox = await qiniu.Sandbox.create('base', { client, resources: [kodoResource, gitResource], + injections: [ + { type: 'id', id: 'rule_1' }, + { injectionRuleID: 'rule_2' }, + qiniuInjection, + githubInjection + ], network: { allowOut: [qiniu.sandbox.ALL_TRAFFIC] }