huaweicloud-devkit 1.1.10 → 1.1.11
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/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/package.json +1 -1
- package/plugins/huaweicloud-core/.claude-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.codex-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.cursor-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.hermes-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.workbuddy-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/openclaw.plugin.json +1 -1
- package/plugins/huaweicloud-core/skills/huawei-obs/SKILL.md +17 -16
- package/plugins/huaweicloud-core/skills/huawei-obs/references/common-pitfalls.md +168 -1
- package/plugins/huaweicloud-core/skills/huawei-sandbox/SKILL.md +75 -13
- package/plugins/huaweicloud-core/skills/huawei-sandbox/references/fullstack-postgrest.md +149 -0
- package/plugins/huaweicloud-core/skills/huawei-sandbox/references/tunnel-persistence.md +121 -0
- package/plugins/huaweicloud-core/skills/huawei-voucher/SKILL.md +13 -9
- package/plugins/huaweicloud-core/src/safety-policy.mjs +6 -2
- package/plugins/huaweicloud-core/src/sandbox/hwlink-api.mjs +75 -12
- package/plugins/huaweicloud-core/src/sandbox/session-manager.mjs +396 -9
- package/plugins/huaweicloud-core/src/tools.mjs +258 -5
|
@@ -22,6 +22,8 @@ import {
|
|
|
22
22
|
deployNginx,
|
|
23
23
|
deployCheck,
|
|
24
24
|
exposeTunnel,
|
|
25
|
+
renewTunnel,
|
|
26
|
+
installKeepaliveWatchdog,
|
|
25
27
|
getCurrentWorkspaceId,
|
|
26
28
|
setWorkspaceId,
|
|
27
29
|
} from './sandbox/session-manager.mjs';
|
|
@@ -764,7 +766,7 @@ export const TOOL_DEFINITIONS = [
|
|
|
764
766
|
{
|
|
765
767
|
name: 'huaweicloud_sandbox_expose_tunnel',
|
|
766
768
|
description:
|
|
767
|
-
'Expose a port on the sandbox to a public DevBridge tunnel URL (devbridge 0.2.x). Authenticates automatically (probing AK/SK vs API-Key builds, using /tmp/hw_creds.sh and /tmp/hw_api_key as injected by huaweicloud_sandbox_credentials), pre-cleans stale tunnels to avoid quota errors, starts the host, extracts the public URL (https://<id>-<port>.devbridge-s2.hwtunnel.com), and health-checks it before returning. Use the ACTUAL port reported by huaweicloud_sandbox_deploy_nginx (its "port" field auto-increments on conflict) — never the originally requested port.
|
|
769
|
+
'Expose a port on the sandbox to a public DevBridge tunnel URL (devbridge 0.2.x). Authenticates automatically (probing AK/SK vs API-Key builds, using /tmp/hw_creds.sh and /tmp/hw_api_key as injected by huaweicloud_sandbox_credentials), pre-cleans stale tunnels to avoid quota errors, starts the host, extracts the public URL (https://<id>-<port>.devbridge-s2.hwtunnel.com), and health-checks it before returning. AUTO-ENROLLS the tunnel into the keepalive watchdog (host re-attach + daily 720h renewal + health check) by default, merged into existing coverage — pass keepalive:false to opt out. Use the ACTUAL port reported by huaweicloud_sandbox_deploy_nginx (its "port" field auto-increments on conflict) — never the originally requested port. Returns ok, publicUrl, port, tunnelId, keepalive.enrolled, and warnings. If the API Key is missing on a release build it returns ok:false with instructions to create one.',
|
|
768
770
|
inputSchema: {
|
|
769
771
|
type: 'object',
|
|
770
772
|
required: ['port'],
|
|
@@ -774,6 +776,16 @@ export const TOOL_DEFINITIONS = [
|
|
|
774
776
|
description:
|
|
775
777
|
'Actual port nginx is listening on — use the "port" value returned by huaweicloud_sandbox_deploy_nginx, not the originally requested port.',
|
|
776
778
|
},
|
|
779
|
+
expiry_hours: {
|
|
780
|
+
type: 'number',
|
|
781
|
+
description:
|
|
782
|
+
'Tunnel lifetime in hours via devbridge -e (1-720; 720 = the 30-day maximum, default 72). The expiry is a rolling window — the keepalive watchdog renews daily to keep the URL alive indefinitely.',
|
|
783
|
+
},
|
|
784
|
+
keepalive: {
|
|
785
|
+
type: 'boolean',
|
|
786
|
+
description:
|
|
787
|
+
'Auto-enroll the tunnel into the keepalive watchdog (default true — every tunnel deployed to the sandbox must be keepalive-protected). Pass false only when the caller explicitly manages keepalive itself.',
|
|
788
|
+
},
|
|
777
789
|
workspace_id: {
|
|
778
790
|
type: 'string',
|
|
779
791
|
description:
|
|
@@ -784,6 +796,65 @@ export const TOOL_DEFINITIONS = [
|
|
|
784
796
|
},
|
|
785
797
|
},
|
|
786
798
|
},
|
|
799
|
+
{
|
|
800
|
+
name: 'huaweicloud_sandbox_renew_tunnel',
|
|
801
|
+
description:
|
|
802
|
+
'Renew a DevBridge tunnel lifetime via devbridge update <tunnelId> -e <hours> (1-720; 720 = the 30-day maximum). The expiry is a rolling window: renewal sets expiration = now + hours regardless of remaining time, so renewing before expiry keeps the SAME tunnel URL alive indefinitely. A renewal counts as ok only when the update succeeds AND show -v reports the absolute tunnelExpiration — the management plane can lose a tunnel record (10002 "tunnel not found") while the data plane keeps serving. Returns ok, tunnelId, expiresAt (ISO), renewedForHours. Pair with huaweicloud_sandbox_keepalive_watchdog for unattended daily renewal.',
|
|
803
|
+
inputSchema: {
|
|
804
|
+
type: 'object',
|
|
805
|
+
required: ['tunnel_id'],
|
|
806
|
+
properties: {
|
|
807
|
+
tunnel_id: {
|
|
808
|
+
type: 'string',
|
|
809
|
+
description:
|
|
810
|
+
'Short tunnel ID from huaweicloud_sandbox_expose_tunnel (e.g. nv2dvv6w) — extractable from the public URL as https://<tunnel_id>-<port>.devbridge-s2.hwtunnel.com.',
|
|
811
|
+
},
|
|
812
|
+
expiry_hours: {
|
|
813
|
+
type: 'number',
|
|
814
|
+
description:
|
|
815
|
+
'Renewal horizon in hours (1-720, default 720). The expiration becomes now + expiry_hours (rolling window).',
|
|
816
|
+
},
|
|
817
|
+
workspace_id: {
|
|
818
|
+
type: 'string',
|
|
819
|
+
description:
|
|
820
|
+
'Workspace ID from huaweicloud_sandbox_connect return value. Required - must be passed explicitly when HW_WORKSPACE_ID is not set.',
|
|
821
|
+
},
|
|
822
|
+
username: { type: 'string', description: 'Login username (default: root)' },
|
|
823
|
+
timeout_ms: { type: 'number', description: 'Execution timeout in milliseconds (default: 60000)' },
|
|
824
|
+
},
|
|
825
|
+
},
|
|
826
|
+
},
|
|
827
|
+
{
|
|
828
|
+
name: 'huaweicloud_sandbox_keepalive_watchdog',
|
|
829
|
+
description:
|
|
830
|
+
'Install a keepalive watchdog in the sandbox that keeps DevBridge tunnel URLs alive indefinitely (three-layer pattern, proven in the 2026-10 tunnel-persistence test): (1) re-attaches the host with devbridge host <tunnelId> if the process dies — the URL is bound to the tunnel ID so it never changes; (2) renews every tunnel daily with devbridge update <tunnelId> -e 720 (29-day failure tolerance); (3) health-checks each URL every 5 minutes, logging warnings to /tmp/tunnel-keepalive.log. Installs as a setsid-detached loop at /tmp/tunnel-keepalive.sh; re-running replaces the old loop. URLs are hardcoded per tunnel because devbridge port list output is wrapped in OSC escapes that break anchored greps. Call AFTER huaweicloud_sandbox_expose_tunnel; requires DevBridge credentials (auth) in the sandbox.',
|
|
831
|
+
inputSchema: {
|
|
832
|
+
type: 'object',
|
|
833
|
+
required: ['tunnels'],
|
|
834
|
+
properties: {
|
|
835
|
+
tunnels: {
|
|
836
|
+
type: 'array',
|
|
837
|
+
description:
|
|
838
|
+
'Tunnels to watch. Get tunnelId + url from huaweicloud_sandbox_expose_tunnel (publicUrl contains the tunnelId as https://<tunnel_id>-<port>.devbridge-s2.hwtunnel.com).',
|
|
839
|
+
items: {
|
|
840
|
+
type: 'object',
|
|
841
|
+
required: ['tunnel_id', 'url'],
|
|
842
|
+
properties: {
|
|
843
|
+
tunnel_id: { type: 'string', description: 'Short tunnel ID (e.g. kag7rozs).' },
|
|
844
|
+
url: { type: 'string', description: 'Full public https URL of the tunnel (used for the health check).' },
|
|
845
|
+
},
|
|
846
|
+
},
|
|
847
|
+
},
|
|
848
|
+
workspace_id: {
|
|
849
|
+
type: 'string',
|
|
850
|
+
description:
|
|
851
|
+
'Workspace ID from huaweicloud_sandbox_connect return value. Required - must be passed explicitly when HW_WORKSPACE_ID is not set.',
|
|
852
|
+
},
|
|
853
|
+
username: { type: 'string', description: 'Login username (default: root)' },
|
|
854
|
+
timeout_ms: { type: 'number', description: 'Execution timeout in milliseconds (default: 60000)' },
|
|
855
|
+
},
|
|
856
|
+
},
|
|
857
|
+
},
|
|
787
858
|
{
|
|
788
859
|
name: 'huaweicloud_sandbox_check_user',
|
|
789
860
|
description:
|
|
@@ -951,6 +1022,47 @@ export const TOOL_DEFINITIONS = [
|
|
|
951
1022
|
required: ['action', 'bucket', 'region'],
|
|
952
1023
|
},
|
|
953
1024
|
},
|
|
1025
|
+
{
|
|
1026
|
+
name: 'huaweicloud_obs_set_cors',
|
|
1027
|
+
description:
|
|
1028
|
+
'配置 OBS 桶的 CORS 规则(浏览器直传前置条件)。KooCLI OBS 不支持 cors 子命令,此工具内部实现 AWS4 签名调用 OBS REST API(PUT/GET/DELETE /?cors),屏蔽签名细节。支持 set(配置)、get(查询)、delete(删除)三种操作。allowedMethod 必须含 HEAD(BrowserJS SDK 每次操作前发 HEAD ?apiversion 探测,缺 HEAD 时浏览器吞掉探测请求)。配置成功后用返回体中的 verify 命令做 OPTIONS 预检验证(200 + Access-Control-Allow-Origin 才算成功)。',
|
|
1029
|
+
inputSchema: {
|
|
1030
|
+
type: 'object',
|
|
1031
|
+
properties: {
|
|
1032
|
+
action: {
|
|
1033
|
+
type: 'string',
|
|
1034
|
+
enum: ['set', 'get', 'delete'],
|
|
1035
|
+
description: '操作类型:set=配置 CORS 规则,get=查询当前规则,delete=删除规则',
|
|
1036
|
+
},
|
|
1037
|
+
bucket: { type: 'string', description: 'OBS 桶名称' },
|
|
1038
|
+
region: { type: 'string', description: 'OBS 桶所在区域,如 cn-north-4' },
|
|
1039
|
+
rules: {
|
|
1040
|
+
type: 'array',
|
|
1041
|
+
description:
|
|
1042
|
+
'CORS 规则数组(action=set 时必填)。每条规则:allowedOrigin(如 https://your-app)、allowedMethod 数组(GET/PUT/POST/DELETE/HEAD,必须含 HEAD)、allowedHeader 数组(默认 ["*"])、maxAgeSeconds(默认 3000)',
|
|
1043
|
+
items: {
|
|
1044
|
+
type: 'object',
|
|
1045
|
+
properties: {
|
|
1046
|
+
allowedOrigin: { type: 'string', description: '允许的来源,如 https://your-app.example.com 或 *' },
|
|
1047
|
+
allowedMethod: {
|
|
1048
|
+
type: 'array',
|
|
1049
|
+
items: { type: 'string' },
|
|
1050
|
+
description: '允许的 HTTP 方法,必须包含 HEAD',
|
|
1051
|
+
},
|
|
1052
|
+
allowedHeader: {
|
|
1053
|
+
type: 'array',
|
|
1054
|
+
items: { type: 'string' },
|
|
1055
|
+
description: '允许的请求头,默认 ["*"]',
|
|
1056
|
+
},
|
|
1057
|
+
maxAgeSeconds: { type: 'number', description: '预检结果缓存秒数,默认 3000' },
|
|
1058
|
+
},
|
|
1059
|
+
required: ['allowedOrigin', 'allowedMethod'],
|
|
1060
|
+
},
|
|
1061
|
+
},
|
|
1062
|
+
},
|
|
1063
|
+
required: ['action', 'bucket', 'region'],
|
|
1064
|
+
},
|
|
1065
|
+
},
|
|
954
1066
|
];
|
|
955
1067
|
|
|
956
1068
|
function toolInvokeValue(name, args) {
|
|
@@ -1512,7 +1624,53 @@ export async function callTool(name, rawArgs = {}, opts = {}) {
|
|
|
1512
1624
|
}
|
|
1513
1625
|
const sandboxUser9 = args.username || 'root';
|
|
1514
1626
|
const sandboxTimeout9 = args.timeout_ms || 90000;
|
|
1515
|
-
return await exposeTunnel(
|
|
1627
|
+
return await exposeTunnel(
|
|
1628
|
+
sandboxWsId9,
|
|
1629
|
+
{ port: args.port, expiryHours: args.expiry_hours, keepalive: args.keepalive },
|
|
1630
|
+
sandboxUser9,
|
|
1631
|
+
sandboxTimeout9,
|
|
1632
|
+
);
|
|
1633
|
+
}
|
|
1634
|
+
case 'huaweicloud_sandbox_renew_tunnel': {
|
|
1635
|
+
if (!args.tunnel_id) {
|
|
1636
|
+
throw new Error(
|
|
1637
|
+
'tunnel_id is required. Get it from huaweicloud_sandbox_expose_tunnel or extract it from the public URL (https://<tunnel_id>-<port>.devbridge-s2.hwtunnel.com).',
|
|
1638
|
+
);
|
|
1639
|
+
}
|
|
1640
|
+
const sandboxWsId10 = args.workspace_id || getCurrentWorkspaceId();
|
|
1641
|
+
if (!sandboxWsId10) {
|
|
1642
|
+
throw new Error(
|
|
1643
|
+
'workspace_id is required. No sandbox connected — call huaweicloud_sandbox_connect first, ' +
|
|
1644
|
+
'or set HW_WORKSPACE_ID environment variable before starting the agent.',
|
|
1645
|
+
);
|
|
1646
|
+
}
|
|
1647
|
+
return await renewTunnel(
|
|
1648
|
+
sandboxWsId10,
|
|
1649
|
+
{ tunnelId: args.tunnel_id, expiryHours: args.expiry_hours },
|
|
1650
|
+
args.username || 'root',
|
|
1651
|
+
args.timeout_ms || 60000,
|
|
1652
|
+
);
|
|
1653
|
+
}
|
|
1654
|
+
case 'huaweicloud_sandbox_keepalive_watchdog': {
|
|
1655
|
+
if (!Array.isArray(args.tunnels) || args.tunnels.length === 0) {
|
|
1656
|
+
throw new Error(
|
|
1657
|
+
'tunnels is required — a non-empty array of {tunnel_id, url} from huaweicloud_sandbox_expose_tunnel results.',
|
|
1658
|
+
);
|
|
1659
|
+
}
|
|
1660
|
+
const sandboxWsId11 = args.workspace_id || getCurrentWorkspaceId();
|
|
1661
|
+
if (!sandboxWsId11) {
|
|
1662
|
+
throw new Error(
|
|
1663
|
+
'workspace_id is required. No sandbox connected — call huaweicloud_sandbox_connect first, ' +
|
|
1664
|
+
'or set HW_WORKSPACE_ID environment variable before starting the agent.',
|
|
1665
|
+
);
|
|
1666
|
+
}
|
|
1667
|
+
const tunnels11 = args.tunnels.map((t) => ({ tunnelId: t.tunnel_id || t.tunnelId, url: t.url }));
|
|
1668
|
+
return await installKeepaliveWatchdog(
|
|
1669
|
+
sandboxWsId11,
|
|
1670
|
+
{ tunnels: tunnels11 },
|
|
1671
|
+
args.username || 'root',
|
|
1672
|
+
args.timeout_ms || 60000,
|
|
1673
|
+
);
|
|
1516
1674
|
}
|
|
1517
1675
|
case 'huaweicloud_sandbox_check_user':
|
|
1518
1676
|
return await hdkitCheckUser();
|
|
@@ -1527,7 +1685,12 @@ export async function callTool(name, rawArgs = {}, opts = {}) {
|
|
|
1527
1685
|
await execOneShot(devStageId, 'devbridge delete-all 2>/dev/null || true', 'root', 15000);
|
|
1528
1686
|
} catch {}
|
|
1529
1687
|
try {
|
|
1530
|
-
await execWithSession(
|
|
1688
|
+
await execWithSession(
|
|
1689
|
+
devStageId,
|
|
1690
|
+
'export PATH=/workspace/.local/bin:$HOME/.huawei/bin${PATH:+:$PATH}',
|
|
1691
|
+
'root',
|
|
1692
|
+
10000,
|
|
1693
|
+
);
|
|
1531
1694
|
} catch {}
|
|
1532
1695
|
await transferGitRepo(args, devStageId, connectResult);
|
|
1533
1696
|
}
|
|
@@ -1632,6 +1795,8 @@ export async function callTool(name, rawArgs = {}, opts = {}) {
|
|
|
1632
1795
|
return await handleUpgrade(args, { sessionId: opts?.sessionId });
|
|
1633
1796
|
case 'huaweicloud_obs_set_website_config':
|
|
1634
1797
|
return await handleObsWebsiteConfig(args);
|
|
1798
|
+
case 'huaweicloud_obs_set_cors':
|
|
1799
|
+
return await handleObsCorsConfig(args);
|
|
1635
1800
|
default:
|
|
1636
1801
|
throw new Error(`Unknown tool: ${name}`);
|
|
1637
1802
|
}
|
|
@@ -2768,7 +2933,95 @@ async function handleObsWebsiteConfig(args) {
|
|
|
2768
2933
|
throw new Error(`Unknown action: ${action}. Use set, get, or delete.`);
|
|
2769
2934
|
}
|
|
2770
2935
|
|
|
2771
|
-
|
|
2936
|
+
// #1003: same-structure extension of handleObsWebsiteConfig — KooCLI OBS does
|
|
2937
|
+
// not expose a cors subcommand and there is no SetBucketCors MCP path, so
|
|
2938
|
+
// agents fell back to installing a Python SDK in the sandbox just to configure
|
|
2939
|
+
// CORS. This reuses obsSignedRequest (AWS4, s3 service) against /?cors.
|
|
2940
|
+
async function handleObsCorsConfig(args = {}) {
|
|
2941
|
+
const { action, bucket, region, rules } = args;
|
|
2942
|
+
if (!bucket || !region) {
|
|
2943
|
+
throw new Error('bucket and region are required');
|
|
2944
|
+
}
|
|
2945
|
+
const creds = resolveCredentialsWithRuntime({});
|
|
2946
|
+
if (!creds?.ak || !creds?.sk) {
|
|
2947
|
+
throw new Error('OBS CORS config requires AK/SK credentials. Run huaweicloud_auth_init first.');
|
|
2948
|
+
}
|
|
2949
|
+
|
|
2950
|
+
const host = `${bucket}.obs.${region}.myhuaweicloud.com`;
|
|
2951
|
+
const endpoint = `https://${host}`;
|
|
2952
|
+
const preflightHint =
|
|
2953
|
+
'Verify with an OPTIONS preflight before declaring success: ' +
|
|
2954
|
+
`curl -i -X OPTIONS --noproxy '*' 'https://${host}/<object-key>' -H 'Origin: <your-app-origin>' -H 'Access-Control-Request-Method: HEAD' ` +
|
|
2955
|
+
'must return HTTP 200 with an Access-Control-Allow-Origin header (200 without CORS headers = config not effective; ' +
|
|
2956
|
+
'403 = rule missing the requested method). See the huawei-obs skill common-pitfalls.md p05.';
|
|
2957
|
+
|
|
2958
|
+
if (action === 'get') {
|
|
2959
|
+
const res = await obsSignedRequest('GET', endpoint, '/?cors', '', creds, region, 'cors');
|
|
2960
|
+
return { ok: res.status === 200, status: res.status, rules: res.body, verify: preflightHint };
|
|
2961
|
+
}
|
|
2962
|
+
|
|
2963
|
+
if (action === 'delete') {
|
|
2964
|
+
const res = await obsSignedRequest('DELETE', endpoint, '/?cors', '', creds, region, 'cors');
|
|
2965
|
+
return { ok: res.status === 204, status: res.status };
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
if (action === 'set') {
|
|
2969
|
+
if (!Array.isArray(rules) || rules.length === 0) {
|
|
2970
|
+
throw new Error('rules array is required for action=set (allowedOrigin + allowedMethod, HEAD included)');
|
|
2971
|
+
}
|
|
2972
|
+
for (const rule of rules) {
|
|
2973
|
+
if (!rule?.allowedOrigin || !Array.isArray(rule?.allowedMethod) || rule.allowedMethod.length === 0) {
|
|
2974
|
+
throw new Error('each CORS rule needs allowedOrigin and a non-empty allowedMethod array');
|
|
2975
|
+
}
|
|
2976
|
+
if (!rule.allowedMethod.includes('HEAD')) {
|
|
2977
|
+
// p01: the BrowserJS SDK's HEAD ?apiversion probe gets swallowed by the
|
|
2978
|
+
// browser when HEAD is missing — the exact failure this tool exists to
|
|
2979
|
+
// prevent, so reject it at the input boundary instead of at runtime.
|
|
2980
|
+
throw new Error(
|
|
2981
|
+
"allowedMethod must include 'HEAD' — the BrowserJS SDK sends a HEAD ?apiversion probe before every " +
|
|
2982
|
+
'operation, and a CORS rule without HEAD makes the browser swallow it as Network Error (p01).',
|
|
2983
|
+
);
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
const escapeXml = (s) =>
|
|
2987
|
+
String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
2988
|
+
const xmlParts = ['<CORSConfiguration>'];
|
|
2989
|
+
for (const rule of rules) {
|
|
2990
|
+
xmlParts.push(' <CORSRule>');
|
|
2991
|
+
xmlParts.push(` <AllowedOrigin>${escapeXml(rule.allowedOrigin)}</AllowedOrigin>`);
|
|
2992
|
+
for (const method of rule.allowedMethod) {
|
|
2993
|
+
xmlParts.push(` <AllowedMethod>${escapeXml(method)}</AllowedMethod>`);
|
|
2994
|
+
}
|
|
2995
|
+
for (const header of rule.allowedHeader || ['*']) {
|
|
2996
|
+
xmlParts.push(` <AllowedHeader>${escapeXml(header)}</AllowedHeader>`);
|
|
2997
|
+
}
|
|
2998
|
+
if (rule.maxAgeSeconds != null) {
|
|
2999
|
+
xmlParts.push(` <MaxAgeSeconds>${Number(rule.maxAgeSeconds)}</MaxAgeSeconds>`);
|
|
3000
|
+
}
|
|
3001
|
+
xmlParts.push(' </CORSRule>');
|
|
3002
|
+
}
|
|
3003
|
+
xmlParts.push('</CORSConfiguration>');
|
|
3004
|
+
const body = xmlParts.join('\n');
|
|
3005
|
+
const res = await obsSignedRequest('PUT', endpoint, '/?cors', body, creds, region, 'cors');
|
|
3006
|
+
return {
|
|
3007
|
+
ok: res.status === 200,
|
|
3008
|
+
status: res.status,
|
|
3009
|
+
message:
|
|
3010
|
+
res.status === 200
|
|
3011
|
+
? `CORS configured for ${bucket} (${rules.length} rule(s)).`
|
|
3012
|
+
: `Failed to configure CORS: HTTP ${res.status}`,
|
|
3013
|
+
verify: preflightHint,
|
|
3014
|
+
};
|
|
3015
|
+
}
|
|
3016
|
+
|
|
3017
|
+
throw new Error(`Unknown action: ${action}. Use set, get, or delete.`);
|
|
3018
|
+
}
|
|
3019
|
+
|
|
3020
|
+
// Single AWS4-signed OBS REST API caller shared by the website/CORS tools
|
|
3021
|
+
// (#1003): subresource is the bucket subresource being addressed — 'website'
|
|
3022
|
+
// for PUT/GET/DELETE /?website, 'cors' for /?cors. Credential scope uses the
|
|
3023
|
+
// s3 service name (OBS is S3-compatible).
|
|
3024
|
+
async function obsSignedRequest(method, endpoint, pathAndQuery, body, creds, region, subresource = 'website') {
|
|
2772
3025
|
const now = new Date();
|
|
2773
3026
|
const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, '');
|
|
2774
3027
|
const amzDate = dateStamp + 'T' + now.toISOString().slice(11, 19).replace(/:/g, '') + 'Z';
|
|
@@ -2776,7 +3029,7 @@ async function obsSignedRequest(method, endpoint, pathAndQuery, body, creds, reg
|
|
|
2776
3029
|
|
|
2777
3030
|
const url = new URL(endpoint + pathAndQuery);
|
|
2778
3031
|
const canonicalUri = '/';
|
|
2779
|
-
const canonicalQueryString =
|
|
3032
|
+
const canonicalQueryString = `${subresource}=`;
|
|
2780
3033
|
const canonicalHeaders = `host:${url.host}\n` + `x-amz-content-sha256:${payloadHash}\n` + `x-amz-date:${amzDate}\n`;
|
|
2781
3034
|
const signedHeaders = 'host;x-amz-content-sha256;x-amz-date';
|
|
2782
3035
|
const canonicalRequest = [
|