mirror of
https://github.com/anthropics/financial-services.git
synced 2026-09-28 05:14:20 +08:00
build-manifest: put credentials in the URL fragment, not the query string (#356)
* build-manifest: put credentials in the URL fragment, not the query string Claude for Office reads manifest settings from the URL fragment; query-string credentials are deprecated there and ignored from 2026-10-19 (anthropics/office-agent#2857). Fragments are not sent in HTTP requests. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LTWZSYSeY8PZFPmQ3PXJin * manifest docs: trim the fragment explanation Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LTWZSYSeY8PZFPmQ3PXJin * build-manifest: rename FRAGMENT_KEYS to SENSITIVE_KEYS Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LTWZSYSeY8PZFPmQ3PXJin --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "claude-for-msft-365-install",
|
||||
"description": "Provision direct cloud access (Vertex AI, Bedrock, Azure AI Foundry, or LLM gateway) for the Claude Office add-in. Generates the customized add-in manifest, walks through Azure admin consent, and writes per-user config via Microsoft Graph extension attributes.",
|
||||
"version": "0.1.12",
|
||||
"version": "0.1.13",
|
||||
"author": {
|
||||
"name": "Anthropic",
|
||||
"email": "support@anthropic.com"
|
||||
|
||||
@@ -4,8 +4,11 @@ description: Generate the add-in manifest XML with your cloud config baked in
|
||||
|
||||
# Generate add-in manifest
|
||||
|
||||
The script fetches the canonical manifest and appends your config as URL query
|
||||
parameters. The add-in reads them at startup. Outlook uses a separate template
|
||||
The script fetches the canonical manifest and appends your config to the
|
||||
taskpane URL. Sensitive settings (`gateway_token`, `azure_api_key`,
|
||||
`google_client_secret`, `otlp_headers`, `inference_headers`, `mcp_servers`) go
|
||||
after `#`; other settings are query parameters. Regenerate and redeploy
|
||||
manifests built with earlier versions. Outlook uses a separate template
|
||||
because Microsoft's `MailApp` schema is distinct from the `TaskPaneApp` schema
|
||||
Excel/Word/PowerPoint share, so ask which apps they're deploying and generate
|
||||
one file per host.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
// Fetches the canonical add-in manifest and writes a customized copy with your
|
||||
// org's config baked into the taskpane URL as query parameters.
|
||||
// org's config baked into the taskpane URL: sensitive settings after #, other settings after ?.
|
||||
//
|
||||
// Usage: node build-manifest.mjs <office|outlook> <out.xml> key=value [key=value ...]
|
||||
// Example: node build-manifest.mjs office acme.xml gcp_project_id=acme gcp_region=us-east5
|
||||
@@ -16,6 +16,43 @@ const MANIFESTS = {
|
||||
// schema repeats Taskpane.Url across V1_0 and V1_1 VersionOverrides, hence /g.
|
||||
const URL_SLOTS = [/(<SourceLocation\s+DefaultValue=")([^"]+)(")/g, /(id="Taskpane\.Url"\s+DefaultValue=")([^"]+)(")/g];
|
||||
|
||||
// Sensitive settings; emitted in the URL fragment so they aren't part of the request.
|
||||
const SENSITIVE_KEYS = new Set([
|
||||
"gateway_token",
|
||||
"azure_api_key",
|
||||
"google_client_secret",
|
||||
"otlp_headers",
|
||||
"inference_headers",
|
||||
"mcp_servers",
|
||||
]);
|
||||
|
||||
function splitParams(params) {
|
||||
const query = new URLSearchParams();
|
||||
const fragment = new URLSearchParams();
|
||||
for (const [k, v] of params) {
|
||||
if (SENSITIVE_KEYS.has(k)) fragment.set(k, v);
|
||||
else query.set(k, v);
|
||||
}
|
||||
return { query, fragment };
|
||||
}
|
||||
|
||||
// URLSearchParams joins pairs with `&`; XML attribute values need it escaped.
|
||||
function xmlEscape(s) {
|
||||
return s.replaceAll("&", "&");
|
||||
}
|
||||
|
||||
// `url` is the template's taskpane URL as it appears in the XML (already escaped,
|
||||
// already carrying ?m=<tag>), so only the appended parts are escaped here.
|
||||
function appendParams(url, { query, fragment }) {
|
||||
if (url.includes("#")) throw new Error(`template URL already has a fragment: ${url}`);
|
||||
let result = url;
|
||||
const qs = query.toString();
|
||||
if (qs) result += (url.includes("?") ? "&" : "?") + xmlEscape(qs);
|
||||
const frag = fragment.toString();
|
||||
if (frag) result += "#" + xmlEscape(frag);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Recognized config keys. `pattern` is a shape hint — mismatches warn but don't block
|
||||
// (your infra may look different). `secret` keys warn louder: the manifest is an
|
||||
// org-wide file and its URL can land in deploy logs; per-user secrets typically go
|
||||
@@ -249,8 +286,7 @@ async function main() {
|
||||
throw new Error(`graph_cloud=${cloud} requires a graph_client_id registered in that cloud`);
|
||||
}
|
||||
|
||||
// URLSearchParams joins with `&`; XML attribute values need it escaped.
|
||||
const qs = params.toString().replaceAll("&", "&");
|
||||
const split = splitParams(params);
|
||||
|
||||
const res = await fetch(manifestUrl);
|
||||
if (!res.ok) throw new Error(`fetch ${manifestUrl}: ${res.status} ${res.statusText}`);
|
||||
@@ -260,8 +296,7 @@ async function main() {
|
||||
slot.lastIndex = 0;
|
||||
if (!slot.test(xml)) throw new Error(`manifest missing expected URL slot: ${slot.source}`);
|
||||
slot.lastIndex = 0;
|
||||
// The template URL already carries ?m=<tag> — append with & not a second ?
|
||||
xml = xml.replace(slot, (_, pre, url, post) => pre + url + (url.includes("?") ? "&" : "?") + qs + post);
|
||||
xml = xml.replace(slot, (_, pre, url, post) => pre + appendParams(url, split) + post);
|
||||
}
|
||||
|
||||
writeFileSync(out, xml);
|
||||
|
||||
Reference in New Issue
Block a user