@productmaker/mcp 8.2.0 → 8.3.1

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/http.js CHANGED
@@ -1830,6 +1830,9 @@ var VideoCreativeJobStatus = z14.object({
1830
1830
  var VideoCreativeJobPayload = z14.object({
1831
1831
  jobId: z14.string().optional(),
1832
1832
  status: VideoCreativeJobStatusEnum.optional(),
1833
+ /** True when the submission replayed an existing task instead of billing
1834
+ * new work — callers surface the twin verbatim. Omitted on fresh creates. */
1835
+ replayed: z14.boolean().optional(),
1833
1836
  videoStructure: VideoStructure.optional(),
1834
1837
  videoQuality: VideoQuality.optional(),
1835
1838
  videoRouteId: VideoRouteId.optional(),
@@ -1923,7 +1926,8 @@ var ImageCreativeJobStatus = z14.object({
1923
1926
  });
1924
1927
  var ImageCreativeJobPayload = z14.object({
1925
1928
  jobId: z14.string().uuid(),
1926
- status: ImageCreativeJobStatusEnum
1929
+ status: ImageCreativeJobStatusEnum,
1930
+ replayed: z14.boolean().optional()
1927
1931
  });
1928
1932
  var ImageCreativeHistoryEntry = z14.object({
1929
1933
  id: z14.string().uuid(),
@@ -1950,7 +1954,8 @@ var LandingCreativeJobStatus = z14.object({
1950
1954
  });
1951
1955
  var LandingCreativeJobPayload = z14.object({
1952
1956
  jobId: z14.string().uuid(),
1953
- status: LandingCreativeJobStatusEnum
1957
+ status: LandingCreativeJobStatusEnum,
1958
+ replayed: z14.boolean().optional()
1954
1959
  });
1955
1960
  var LandingCreativeHistoryEntry = z14.object({
1956
1961
  id: z14.string().uuid(),
@@ -5621,6 +5626,10 @@ var CreateShopifySubscriptionRequest = z28.object({
5621
5626
  var CreateShopifyBillingChargeRequest = z28.object({
5622
5627
  packId: z28.string().min(1)
5623
5628
  });
5629
+ var ShopifyBillingCallbackQuery = z28.object({
5630
+ charge_id: z28.string().regex(/^\d+$/),
5631
+ transactionId: z28.string().uuid()
5632
+ });
5624
5633
  var CancelShopifySubscriptionRequest = z28.object({
5625
5634
  subscriptionId: z28.string().min(1)
5626
5635
  });
@@ -10848,6 +10857,7 @@ var MixSceneResult = z48.object({
10848
10857
  index: z48.number().int().positive(),
10849
10858
  status: z48.enum([
10850
10859
  "queued",
10860
+ "submitting",
10851
10861
  "submitted",
10852
10862
  "done",
10853
10863
  "failed"
@@ -13446,7 +13456,7 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
13446
13456
  ], [
13447
13457
  "product_costing"
13448
13458
  ]),
13449
- defineCapability("create_creative_variant", "6a777fbe7ad17966d16502e8872e15e4db2dcfacdfd2992d24853340d4f22a00", "credit_consuming", "creative", [
13459
+ defineCapability("create_creative_variant", "3e02399a7785df328f1c4d63cfbb6d7481abaac9f91d4a5cf51d410e657d63aa", "credit_consuming", "creative", [
13450
13460
  "credit_consuming",
13451
13461
  "state_change"
13452
13462
  ], [
@@ -13696,7 +13706,9 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
13696
13706
  // MCP 7.0.0 (Task 11, .agent/plans/2026-09-15-model-agnostic-video-routes.md):
13697
13707
  // `freeProModel` retired in favor of `modelId` (registry ids). Recomputed
13698
13708
  // from `pnpm --filter @productmaker/mcp test` printing the real digest.
13699
- "ead1f563a1915452157628d023ba203ecfa8f502a48259a5b5b15be4c84bf768",
13709
+ // Re-baselined 2026-09-26: `idempotencyKey` field description corrected —
13710
+ // paid tools ignore caller-provided keys and derive their own semantic key.
13711
+ "35616a32a42a0bc062218fdfbb4cc7f4ecfa814571fb86b4f11ef9e4184855e1",
13700
13712
  "credit_consuming",
13701
13713
  "creative",
13702
13714
  [
@@ -16195,6 +16207,9 @@ function buildProxyUrl(opts) {
16195
16207
  }
16196
16208
  __name(buildProxyUrl, "buildProxyUrl");
16197
16209
 
16210
+ // src/http.ts
16211
+ import { getOAuthProtectedResourceMetadataUrl as getOAuthProtectedResourceMetadataUrl2 } from "@modelcontextprotocol/sdk/server/auth/router.js";
16212
+
16198
16213
  // src/auth/http-oauth.ts
16199
16214
  import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
16200
16215
  import { getOAuthProtectedResourceMetadataUrl, mcpAuthMetadataRouter } from "@modelcontextprotocol/sdk/server/auth/router.js";
@@ -16604,6 +16619,7 @@ import { createRemoteJWKSet as createRemoteJWKSet2, jwtVerify as jwtVerify2 } fr
16604
16619
  import { z as z73 } from "zod";
16605
16620
  var MCP_RESOURCE = new URL("https://mcp.productmaker.app/mcp");
16606
16621
  var MCP_SCOPE = "mcp:access";
16622
+ var REFRESH_SCOPE = "offline_access";
16607
16623
  var STATIC_CLIENT_ISSUER = new URL(MCP_RESOURCE.origin);
16608
16624
  var oauthEnvSchema = z73.object({
16609
16625
  MCP_REMOTE_AUTH_MODE: z73.literal("oauth"),
@@ -16799,6 +16815,121 @@ var Auth0TokenVerifier = class {
16799
16815
  }
16800
16816
  };
16801
16817
 
16818
+ // src/oauth-analytics.ts
16819
+ import { PostHog } from "posthog-node";
16820
+ var DEFAULT_POSTHOG_HOST = "https://us.i.posthog.com";
16821
+ var REDACTED = "[REDACTED]";
16822
+ var SENSITIVE_KEYS = /* @__PURE__ */ new Set([
16823
+ "token",
16824
+ "authorization",
16825
+ "apikey",
16826
+ "clientsecret",
16827
+ "subjecttoken",
16828
+ "accesstoken",
16829
+ "refreshtoken",
16830
+ "idtoken",
16831
+ "code",
16832
+ "secret",
16833
+ "password"
16834
+ ]);
16835
+ var SENSITIVE_ASSIGNMENT_PATTERN = /(\b(?:access[_-]?token|authorization|client[_-]?secret|subject[_-]?token|refresh[_-]?token|id[_-]?token|api[_-]?key|token|secret|password|code)\b\s*[:=]\s*)(?:(bearer)\s+)?(?:(['"])(.*?)\3|([^\s,;&}"']+))/gi;
16836
+ var BEARER_TOKEN_PATTERN = /\b(bearer\s+)([A-Za-z0-9._~+/=-]+)/gi;
16837
+ var client = null;
16838
+ var KNOWN_MCP_OAUTH_ANALYTICS_EVENTS = [
16839
+ MCP_OAUTH_ANALYTICS_EVENTS.STARTED,
16840
+ MCP_OAUTH_ANALYTICS_EVENTS.CONNECTED,
16841
+ MCP_OAUTH_ANALYTICS_EVENTS.FAILED,
16842
+ MCP_OAUTH_ANALYTICS_EVENTS.SAFE_VARIANT
16843
+ ];
16844
+ function getPostHogClient() {
16845
+ if (client) return client;
16846
+ const apiKey = process.env.POSTHOG_API_KEY;
16847
+ if (!apiKey) return null;
16848
+ client = new PostHog(apiKey, {
16849
+ host: process.env.POSTHOG_HOST || DEFAULT_POSTHOG_HOST,
16850
+ flushAt: 20,
16851
+ flushInterval: 1e4
16852
+ });
16853
+ return client;
16854
+ }
16855
+ __name(getPostHogClient, "getPostHogClient");
16856
+ function normalizeKey(key) {
16857
+ return key.replace(/[^a-z0-9]/gi, "").toLowerCase();
16858
+ }
16859
+ __name(normalizeKey, "normalizeKey");
16860
+ function isSensitiveKey(key) {
16861
+ const normalizedKey = normalizeKey(key);
16862
+ return SENSITIVE_KEYS.has(normalizedKey) || normalizedKey.endsWith("token") || normalizedKey.endsWith("secret");
16863
+ }
16864
+ __name(isSensitiveKey, "isSensitiveKey");
16865
+ function sanitizeUrl(value) {
16866
+ try {
16867
+ const parsed = new URL(value);
16868
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
16869
+ return value;
16870
+ }
16871
+ parsed.username = "";
16872
+ parsed.password = "";
16873
+ parsed.search = "";
16874
+ parsed.hash = "";
16875
+ return parsed.toString();
16876
+ } catch {
16877
+ return value;
16878
+ }
16879
+ }
16880
+ __name(sanitizeUrl, "sanitizeUrl");
16881
+ function sanitizeString(value) {
16882
+ const redacted = value.replace(SENSITIVE_ASSIGNMENT_PATTERN, (_match, prefix, scheme, quote) => {
16883
+ const replacement = quote ? `${quote}${REDACTED}${quote}` : REDACTED;
16884
+ return `${prefix}${scheme ? `${scheme} ` : ""}${replacement}`;
16885
+ }).replace(BEARER_TOKEN_PATTERN, `$1${REDACTED}`);
16886
+ const sanitizedUrl = sanitizeUrl(redacted);
16887
+ if (sanitizedUrl !== redacted) return sanitizedUrl;
16888
+ return redacted.replace(/https?:\/\/[^\s"'<>]+/gi, sanitizeUrl);
16889
+ }
16890
+ __name(sanitizeString, "sanitizeString");
16891
+ function sanitizeValue(value) {
16892
+ if (Array.isArray(value)) {
16893
+ return value.map((entry) => sanitizeValue(entry));
16894
+ }
16895
+ if (value && typeof value === "object") {
16896
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => {
16897
+ if (isSensitiveKey(key)) {
16898
+ return [
16899
+ key,
16900
+ REDACTED
16901
+ ];
16902
+ }
16903
+ return [
16904
+ key,
16905
+ sanitizeValue(entry)
16906
+ ];
16907
+ }));
16908
+ }
16909
+ if (typeof value === "string") {
16910
+ return sanitizeString(value);
16911
+ }
16912
+ return value;
16913
+ }
16914
+ __name(sanitizeValue, "sanitizeValue");
16915
+ var OAuthAnalytics = class {
16916
+ static {
16917
+ __name(this, "OAuthAnalytics");
16918
+ }
16919
+ capture(event, subject, properties) {
16920
+ const posthog = getPostHogClient();
16921
+ if (!posthog) return;
16922
+ try {
16923
+ posthog.capture({
16924
+ distinctId: subject ?? "mcp_oauth_aggregate",
16925
+ event,
16926
+ properties: sanitizeValue(properties)
16927
+ });
16928
+ } catch {
16929
+ }
16930
+ }
16931
+ };
16932
+
16802
16933
  // src/auth/oauth-proxy.ts
16803
16934
  import { createHmac as createHmac2, hkdfSync, timingSafeEqual as timingSafeEqual2 } from "crypto";
16804
16935
  import express from "express";
@@ -16885,6 +17016,15 @@ var OAUTH_PROXY_PATHS = {
16885
17016
  token: "/oauth/token"
16886
17017
  };
16887
17018
  var PROXY_CALLBACK_URL = new URL(OAUTH_PROXY_PATHS.callback.slice(1), STATIC_CLIENT_ISSUER).href;
17019
+ function responseIssuesRefreshToken(payload) {
17020
+ try {
17021
+ const parsed = JSON.parse(payload);
17022
+ return typeof parsed === "object" && parsed !== null && "refresh_token" in parsed && typeof parsed.refresh_token === "string" && parsed.refresh_token !== "";
17023
+ } catch {
17024
+ return false;
17025
+ }
17026
+ }
17027
+ __name(responseIssuesRefreshToken, "responseIssuesRefreshToken");
16888
17028
  function firstQueryValue(value) {
16889
17029
  if (typeof value === "string") return value;
16890
17030
  if (Array.isArray(value) && typeof value[0] === "string") return value[0];
@@ -16898,6 +17038,12 @@ function badRequest(res, description) {
16898
17038
  });
16899
17039
  }
16900
17040
  __name(badRequest, "badRequest");
17041
+ function withRefreshScope(requested) {
17042
+ const scopes = (requested?.trim() ? requested : MCP_SCOPE).split(/\s+/).filter(Boolean);
17043
+ if (!scopes.includes(REFRESH_SCOPE)) scopes.push(REFRESH_SCOPE);
17044
+ return scopes.join(" ");
17045
+ }
17046
+ __name(withRefreshScope, "withRefreshScope");
16901
17047
  function createOAuthProxyRouter(options) {
16902
17048
  const router = express.Router();
16903
17049
  const stateKey3 = deriveStateKey(options.staticClient.clientSecret);
@@ -16921,13 +17067,14 @@ function createOAuthProxyRouter(options) {
16921
17067
  }
16922
17068
  const upstream = new URL(options.authorizationEndpoint);
16923
17069
  for (const [key, value] of Object.entries(req.query)) {
16924
- if (key === "redirect_uri" || key === "state") continue;
17070
+ if (key === "redirect_uri" || key === "state" || key === "scope") continue;
16925
17071
  for (const v of Array.isArray(value) ? value : [
16926
17072
  value
16927
17073
  ]) {
16928
17074
  if (typeof v === "string") upstream.searchParams.append(key, v);
16929
17075
  }
16930
17076
  }
17077
+ upstream.searchParams.set("scope", withRefreshScope(firstQueryValue(req.query.scope)));
16931
17078
  upstream.searchParams.set("redirect_uri", PROXY_CALLBACK_URL);
16932
17079
  upstream.searchParams.set("state", sealState(stateKey3, {
16933
17080
  redirectUri,
@@ -16987,13 +17134,10 @@ function createOAuthProxyRouter(options) {
16987
17134
  });
16988
17135
  return;
16989
17136
  }
17137
+ body.set("client_secret", options.staticClient.clientSecret);
16990
17138
  const headers = {
16991
17139
  "content-type": "application/x-www-form-urlencoded"
16992
17140
  };
16993
- const authorization = req.headers.authorization;
16994
- if (typeof authorization === "string") {
16995
- headers.authorization = authorization;
16996
- }
16997
17141
  let upstream;
16998
17142
  let payload;
16999
17143
  try {
@@ -17010,6 +17154,14 @@ function createOAuthProxyRouter(options) {
17010
17154
  });
17011
17155
  return;
17012
17156
  }
17157
+ try {
17158
+ options.onTokenExchange?.({
17159
+ grantType,
17160
+ status: upstream.status,
17161
+ refreshTokenIssued: responseIssuesRefreshToken(payload)
17162
+ });
17163
+ } catch {
17164
+ }
17013
17165
  res.status(upstream.status).type(upstream.headers.get("content-type") ?? "application/json").send(payload);
17014
17166
  });
17015
17167
  return router;
@@ -17026,11 +17178,12 @@ var REGISTRATION_RATE_LIMIT = {
17026
17178
  windowMs: 6e4,
17027
17179
  max: 120
17028
17180
  };
17181
+ var PUBLIC_CLIENT_SECRET_PLACEHOLDER = "pm-public-client";
17029
17182
  function buildStaticClientInformation(staticClient, requested) {
17030
17183
  return {
17031
17184
  client_id: staticClient.clientId,
17032
- client_secret: staticClient.clientSecret,
17033
- // 0 = never expires (RFC 7591 §3.2.1); the secret is Auth0's, not ours.
17185
+ client_secret: PUBLIC_CLIENT_SECRET_PLACEHOLDER,
17186
+ // 0 = never expires (RFC 7591 §3.2.1); the placeholder is a constant.
17034
17187
  client_secret_expires_at: 0,
17035
17188
  token_endpoint_auth_method: "client_secret_post",
17036
17189
  redirect_uris: requested.redirect_uris,
@@ -17041,7 +17194,12 @@ function buildStaticClientInformation(staticClient, requested) {
17041
17194
  response_types: [
17042
17195
  "code"
17043
17196
  ],
17044
- scope: requested.scope ?? MCP_SCOPE,
17197
+ // Clients that defer scope choice to the server get the full intended
17198
+ // set: `mcp:access` for the resource plus `offline_access` so the session
17199
+ // can refresh instead of dying an interactive re-auth every 900s access
17200
+ // token (the auth loop seen in production). A client that names its own
17201
+ // scope list is echoed verbatim — its choice wins.
17202
+ scope: requested.scope ?? `${MCP_SCOPE} ${REFRESH_SCOPE}`,
17045
17203
  ...requested.client_name !== void 0 ? {
17046
17204
  client_name: requested.client_name
17047
17205
  } : {}
@@ -17088,8 +17246,12 @@ function buildStaticClientAuthorizationServerMetadata(auth0Metadata) {
17088
17246
  token_endpoint_auth_methods_supported: [
17089
17247
  "client_secret_post"
17090
17248
  ],
17249
+ // `offline_access` is what lets a client ASK for a refresh token; without
17250
+ // it in scopes_supported, conforming clients never request it and every
17251
+ // 900s access-token expiry becomes an interactive re-auth loop.
17091
17252
  scopes_supported: [
17092
- MCP_SCOPE
17253
+ MCP_SCOPE,
17254
+ REFRESH_SCOPE
17093
17255
  ],
17094
17256
  service_documentation: "https://productmaker.app/"
17095
17257
  };
@@ -17141,11 +17303,23 @@ function createMetadataRouter(config, metadata, fetchFn) {
17141
17303
  staticClient: config.staticClient,
17142
17304
  authorizationEndpoint: metadata.authorization_endpoint,
17143
17305
  tokenEndpoint: metadata.token_endpoint,
17144
- fetchFn
17306
+ fetchFn,
17307
+ onTokenExchange: captureTokenExchange
17145
17308
  }));
17146
17309
  return router;
17147
17310
  }
17148
17311
  __name(createMetadataRouter, "createMetadataRouter");
17312
+ var tokenExchangeAnalytics = new OAuthAnalytics();
17313
+ function captureTokenExchange(outcome) {
17314
+ const ok = outcome.status >= 200 && outcome.status < 300;
17315
+ tokenExchangeAnalytics.capture(ok ? MCP_OAUTH_ANALYTICS_EVENTS.CONNECTED : MCP_OAUTH_ANALYTICS_EVENTS.FAILED, null, {
17316
+ stage: "token_exchange",
17317
+ grantType: outcome.grantType,
17318
+ status: outcome.status,
17319
+ refreshTokenIssued: outcome.refreshTokenIssued
17320
+ });
17321
+ }
17322
+ __name(captureTokenExchange, "captureTokenExchange");
17149
17323
  function createHttpOAuth(config, metadata, fetchFn) {
17150
17324
  const verifier = new Auth0TokenVerifier(config, metadata.jwks_uri);
17151
17325
  const oboClient = new Auth0OboClient(config, {
@@ -17424,121 +17598,6 @@ function registerProductMakerPrompts(server) {
17424
17598
  }
17425
17599
  __name(registerProductMakerPrompts, "registerProductMakerPrompts");
17426
17600
 
17427
- // src/oauth-analytics.ts
17428
- import { PostHog } from "posthog-node";
17429
- var DEFAULT_POSTHOG_HOST = "https://us.i.posthog.com";
17430
- var REDACTED = "[REDACTED]";
17431
- var SENSITIVE_KEYS = /* @__PURE__ */ new Set([
17432
- "token",
17433
- "authorization",
17434
- "apikey",
17435
- "clientsecret",
17436
- "subjecttoken",
17437
- "accesstoken",
17438
- "refreshtoken",
17439
- "idtoken",
17440
- "code",
17441
- "secret",
17442
- "password"
17443
- ]);
17444
- var SENSITIVE_ASSIGNMENT_PATTERN = /(\b(?:access[_-]?token|authorization|client[_-]?secret|subject[_-]?token|refresh[_-]?token|id[_-]?token|api[_-]?key|token|secret|password|code)\b\s*[:=]\s*)(?:(bearer)\s+)?(?:(['"])(.*?)\3|([^\s,;&}"']+))/gi;
17445
- var BEARER_TOKEN_PATTERN = /\b(bearer\s+)([A-Za-z0-9._~+/=-]+)/gi;
17446
- var client = null;
17447
- var KNOWN_MCP_OAUTH_ANALYTICS_EVENTS = [
17448
- MCP_OAUTH_ANALYTICS_EVENTS.STARTED,
17449
- MCP_OAUTH_ANALYTICS_EVENTS.CONNECTED,
17450
- MCP_OAUTH_ANALYTICS_EVENTS.FAILED,
17451
- MCP_OAUTH_ANALYTICS_EVENTS.SAFE_VARIANT
17452
- ];
17453
- function getPostHogClient() {
17454
- if (client) return client;
17455
- const apiKey = process.env.POSTHOG_API_KEY;
17456
- if (!apiKey) return null;
17457
- client = new PostHog(apiKey, {
17458
- host: process.env.POSTHOG_HOST || DEFAULT_POSTHOG_HOST,
17459
- flushAt: 20,
17460
- flushInterval: 1e4
17461
- });
17462
- return client;
17463
- }
17464
- __name(getPostHogClient, "getPostHogClient");
17465
- function normalizeKey(key) {
17466
- return key.replace(/[^a-z0-9]/gi, "").toLowerCase();
17467
- }
17468
- __name(normalizeKey, "normalizeKey");
17469
- function isSensitiveKey(key) {
17470
- const normalizedKey = normalizeKey(key);
17471
- return SENSITIVE_KEYS.has(normalizedKey) || normalizedKey.endsWith("token") || normalizedKey.endsWith("secret");
17472
- }
17473
- __name(isSensitiveKey, "isSensitiveKey");
17474
- function sanitizeUrl(value) {
17475
- try {
17476
- const parsed = new URL(value);
17477
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
17478
- return value;
17479
- }
17480
- parsed.username = "";
17481
- parsed.password = "";
17482
- parsed.search = "";
17483
- parsed.hash = "";
17484
- return parsed.toString();
17485
- } catch {
17486
- return value;
17487
- }
17488
- }
17489
- __name(sanitizeUrl, "sanitizeUrl");
17490
- function sanitizeString(value) {
17491
- const redacted = value.replace(SENSITIVE_ASSIGNMENT_PATTERN, (_match, prefix, scheme, quote) => {
17492
- const replacement = quote ? `${quote}${REDACTED}${quote}` : REDACTED;
17493
- return `${prefix}${scheme ? `${scheme} ` : ""}${replacement}`;
17494
- }).replace(BEARER_TOKEN_PATTERN, `$1${REDACTED}`);
17495
- const sanitizedUrl = sanitizeUrl(redacted);
17496
- if (sanitizedUrl !== redacted) return sanitizedUrl;
17497
- return redacted.replace(/https?:\/\/[^\s"'<>]+/gi, sanitizeUrl);
17498
- }
17499
- __name(sanitizeString, "sanitizeString");
17500
- function sanitizeValue(value) {
17501
- if (Array.isArray(value)) {
17502
- return value.map((entry) => sanitizeValue(entry));
17503
- }
17504
- if (value && typeof value === "object") {
17505
- return Object.fromEntries(Object.entries(value).map(([key, entry]) => {
17506
- if (isSensitiveKey(key)) {
17507
- return [
17508
- key,
17509
- REDACTED
17510
- ];
17511
- }
17512
- return [
17513
- key,
17514
- sanitizeValue(entry)
17515
- ];
17516
- }));
17517
- }
17518
- if (typeof value === "string") {
17519
- return sanitizeString(value);
17520
- }
17521
- return value;
17522
- }
17523
- __name(sanitizeValue, "sanitizeValue");
17524
- var OAuthAnalytics = class {
17525
- static {
17526
- __name(this, "OAuthAnalytics");
17527
- }
17528
- capture(event, subject, properties) {
17529
- const posthog = getPostHogClient();
17530
- if (!posthog) return;
17531
- try {
17532
- posthog.capture({
17533
- distinctId: subject ?? "mcp_oauth_aggregate",
17534
- event,
17535
- properties: sanitizeValue(properties)
17536
- });
17537
- } catch {
17538
- }
17539
- }
17540
- };
17541
-
17542
17601
  // src/server.ts
17543
17602
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
17544
17603
 
@@ -17601,7 +17660,10 @@ var ApiClient = class {
17601
17660
  method: "POST",
17602
17661
  headers: {
17603
17662
  ...authHeaders,
17604
- "Content-Type": "application/json"
17663
+ "Content-Type": "application/json",
17664
+ ...options?.idempotencyKey ? {
17665
+ "Idempotency-Key": options.idempotencyKey
17666
+ } : {}
17605
17667
  },
17606
17668
  body: JSON.stringify(body)
17607
17669
  }, options);
@@ -17723,7 +17785,7 @@ var ApiClient = class {
17723
17785
  };
17724
17786
 
17725
17787
  // src/index.ts
17726
- var MCP_VERSION = "8.2.0";
17788
+ var MCP_VERSION = "8.3.1";
17727
17789
 
17728
17790
  // src/meta-upload-app.ts
17729
17791
  import { readFileSync } from "fs";
@@ -18707,8 +18769,13 @@ async function submitCreative(api, opts) {
18707
18769
  const taskId = r.taskId ?? r.jobId;
18708
18770
  return asText({
18709
18771
  taskId,
18710
- status: "queued",
18711
- estimatedSeconds: opts.estimatedSeconds
18772
+ // Replay-aware: a keyed retry may land on a task that is already
18773
+ // processing/done — report the real status, not a hopeful "queued".
18774
+ status: r.status ?? "queued",
18775
+ estimatedSeconds: opts.estimatedSeconds,
18776
+ ...r.replayed === true ? {
18777
+ replayed: true
18778
+ } : {}
18712
18779
  });
18713
18780
  }
18714
18781
  __name(submitCreative, "submitCreative");
@@ -20072,6 +20139,170 @@ ${summary}`,
20072
20139
  }
20073
20140
  __name(requirePaidCreativeConfirmation, "requirePaidCreativeConfirmation");
20074
20141
 
20142
+ // src/tools/stable-uuid.ts
20143
+ import { createHash as createHash3 } from "crypto";
20144
+ function canonicalize(value) {
20145
+ if (Array.isArray(value)) return value.map(canonicalize);
20146
+ if (value === null || typeof value !== "object") return value;
20147
+ const record = value;
20148
+ return Object.fromEntries(Object.keys(record).sort().filter((key) => record[key] !== void 0).map((key) => [
20149
+ key,
20150
+ canonicalize(record[key])
20151
+ ]));
20152
+ }
20153
+ __name(canonicalize, "canonicalize");
20154
+ function canonicalJson(value) {
20155
+ return JSON.stringify(canonicalize(value));
20156
+ }
20157
+ __name(canonicalJson, "canonicalJson");
20158
+ function deriveUuidV5(name, namespace) {
20159
+ const namespaceBytes = Buffer.from(namespace.replaceAll("-", ""), "hex");
20160
+ const digest = createHash3("sha1").update(namespaceBytes).update(name, "utf8").digest();
20161
+ const bytes = Buffer.from(digest.subarray(0, 16));
20162
+ bytes[6] = (bytes[6] ?? 0) & 15 | 80;
20163
+ bytes[8] = (bytes[8] ?? 0) & 63 | 128;
20164
+ const hex = bytes.toString("hex");
20165
+ return [
20166
+ hex.slice(0, 8),
20167
+ hex.slice(8, 12),
20168
+ hex.slice(12, 16),
20169
+ hex.slice(16, 20),
20170
+ hex.slice(20)
20171
+ ].join("-");
20172
+ }
20173
+ __name(deriveUuidV5, "deriveUuidV5");
20174
+
20175
+ // src/tools/request-key.ts
20176
+ var PAID_REQUEST_KEY_WINDOW_MS = 15 * 60 * 1e3;
20177
+ var PAID_REQUEST_KEY_NAMESPACE = "8f3d6b1c-2a47-4e5b-9c0d-6e7f8a9b0c1d";
20178
+ var PARAPHRASE_PRONE_FIELDS = /* @__PURE__ */ new Set([
20179
+ "additionalContext",
20180
+ "productContext",
20181
+ "scriptNotes",
20182
+ "creativeDirection",
20183
+ "buyerPersona",
20184
+ "actorDescription",
20185
+ "description",
20186
+ "idempotencyKey",
20187
+ "clientRequestId"
20188
+ ]);
20189
+ var URL_FIELDS = /* @__PURE__ */ new Set([
20190
+ "landingUrl",
20191
+ "productUrl",
20192
+ "url",
20193
+ "serviceLandingUrl"
20194
+ ]);
20195
+ var TRACKING_PARAM_PATTERN = /^(utm_\w+|fbclid|gclid|msclkid|mc_cid|mc_eid|igshid)$/;
20196
+ var LANGUAGE_FIELDS = /* @__PURE__ */ new Set([
20197
+ "language",
20198
+ "lang"
20199
+ ]);
20200
+ var CASE_FOLD_FIELDS = /* @__PURE__ */ new Set([
20201
+ "country",
20202
+ "language",
20203
+ "lang"
20204
+ ]);
20205
+ var ORDER_INSIGNIFICANT_ARRAY_FIELDS = /* @__PURE__ */ new Set([
20206
+ "outputs",
20207
+ "selectedOutputs",
20208
+ "features"
20209
+ ]);
20210
+ function sortCanonical(items) {
20211
+ return [
20212
+ ...items
20213
+ ].sort((a, b) => {
20214
+ const ka = canonicalJson(a);
20215
+ const kb = canonicalJson(b);
20216
+ return ka < kb ? -1 : ka > kb ? 1 : 0;
20217
+ });
20218
+ }
20219
+ __name(sortCanonical, "sortCanonical");
20220
+ function isRecord3(value) {
20221
+ return typeof value === "object" && value !== null && !Array.isArray(value);
20222
+ }
20223
+ __name(isRecord3, "isRecord");
20224
+ function collectLanguageCodes(value, out) {
20225
+ if (Array.isArray(value)) {
20226
+ const items = value;
20227
+ for (const item of items) collectLanguageCodes(item, out);
20228
+ return out;
20229
+ }
20230
+ if (!isRecord3(value)) return out;
20231
+ for (const [key, entry] of Object.entries(value)) {
20232
+ if (LANGUAGE_FIELDS.has(key) && typeof entry === "string") {
20233
+ const base = entry.trim().split(/[-_]/)[0]?.toLowerCase();
20234
+ if (base) out.add(base);
20235
+ }
20236
+ collectLanguageCodes(entry, out);
20237
+ }
20238
+ return out;
20239
+ }
20240
+ __name(collectLanguageCodes, "collectLanguageCodes");
20241
+ function normalizeSearchForKey(search) {
20242
+ if (!search) return "";
20243
+ const params = [
20244
+ ...new URLSearchParams(search).entries()
20245
+ ].filter(([key]) => !TRACKING_PARAM_PATTERN.test(key.toLowerCase())).sort(([ka, va], [kb, vb]) => ka === kb ? va < vb ? -1 : va > vb ? 1 : 0 : ka < kb ? -1 : 1);
20246
+ if (!params.length) return "";
20247
+ const out = new URLSearchParams();
20248
+ for (const [key, value] of params) out.append(key, value);
20249
+ return `?${out.toString()}`;
20250
+ }
20251
+ __name(normalizeSearchForKey, "normalizeSearchForKey");
20252
+ function normalizeUrlForKey(value, languages) {
20253
+ const trimmed = value.trim();
20254
+ let url;
20255
+ try {
20256
+ url = new URL(trimmed);
20257
+ } catch {
20258
+ return trimmed.toLowerCase();
20259
+ }
20260
+ const host = url.hostname.toLowerCase().replace(/^www\./, "");
20261
+ const port = url.port ? `:${url.port}` : "";
20262
+ const segments = url.pathname.split("/").filter(Boolean);
20263
+ const soleSegment = segments.length === 1 ? segments[0] : void 0;
20264
+ if (soleSegment !== void 0 && languages.has(soleSegment.toLowerCase())) {
20265
+ segments.pop();
20266
+ }
20267
+ const path3 = segments.length ? `/${segments.join("/")}` : "";
20268
+ return host + port + path3 + normalizeSearchForKey(url.search);
20269
+ }
20270
+ __name(normalizeUrlForKey, "normalizeUrlForKey");
20271
+ function normalizeForKey(value, languages) {
20272
+ if (Array.isArray(value)) {
20273
+ const items = value;
20274
+ return items.map((item) => normalizeForKey(item, languages));
20275
+ }
20276
+ if (!isRecord3(value)) return value;
20277
+ const out = {};
20278
+ for (const [key, entry] of Object.entries(value)) {
20279
+ if (PARAPHRASE_PRONE_FIELDS.has(key)) {
20280
+ if (key === "description" && value.mode === "described") {
20281
+ out[key] = normalizeForKey(entry, languages);
20282
+ }
20283
+ continue;
20284
+ }
20285
+ out[key] = URL_FIELDS.has(key) && typeof entry === "string" ? normalizeUrlForKey(entry, languages) : CASE_FOLD_FIELDS.has(key) && typeof entry === "string" ? (
20286
+ // separator too or a host alternating between them splits the key.
20287
+ entry.trim().toLowerCase().replace(/_/g, "-")
20288
+ ) : ORDER_INSIGNIFICANT_ARRAY_FIELDS.has(key) && Array.isArray(entry) ? sortCanonical(entry.map((item) => normalizeForKey(item, languages))) : normalizeForKey(entry, languages);
20289
+ }
20290
+ return out;
20291
+ }
20292
+ __name(normalizeForKey, "normalizeForKey");
20293
+ function derivePaidRequestKey(args) {
20294
+ const languages = collectLanguageCodes(args.input, /* @__PURE__ */ new Set());
20295
+ const semanticInput = normalizeForKey(args.input, languages);
20296
+ const bucket = Math.floor(args.nowMs / PAID_REQUEST_KEY_WINDOW_MS);
20297
+ return deriveUuidV5(canonicalJson({
20298
+ user: args.fingerprint,
20299
+ tool: args.tool,
20300
+ input: semanticInput,
20301
+ bucket
20302
+ }), PAID_REQUEST_KEY_NAMESPACE);
20303
+ }
20304
+ __name(derivePaidRequestKey, "derivePaidRequestKey");
20305
+
20075
20306
  // src/tools/winning-product-image.ts
20076
20307
  import { z as z76 } from "zod";
20077
20308
  var WinnerReferenceInput = z76.object({
@@ -20608,10 +20839,25 @@ ANTES de invocar, resume outputs, cantidad de videos y datos comerciales, explic
20608
20839
  };
20609
20840
  const r = await api.postMultipart("/v1/ingest", {
20610
20841
  images: parts
20611
- }, fields);
20842
+ }, fields, {
20843
+ // LLM hosts retry paid tools with rephrased arguments — the API's
20844
+ // byte-fingerprint can't see the duplicate, this key can. Derived
20845
+ // from the semantic inputs, not the wire fields, for the same
20846
+ // reason (see request-key.ts, which also canonicalizes the
20847
+ // order-insignificant `outputs` set).
20848
+ idempotencyKey: derivePaidRequestKey({
20849
+ fingerprint: api.fingerprint(),
20850
+ tool: "create_product_task",
20851
+ input,
20852
+ nowMs: Date.now()
20853
+ })
20854
+ });
20612
20855
  return asText({
20613
20856
  taskId: r.taskId,
20614
- status: "queued",
20857
+ status: r.status ?? "queued",
20858
+ ...r.replayed ? {
20859
+ replayed: true
20860
+ } : {},
20615
20861
  estimatedSeconds: 300,
20616
20862
  outputs: selectedOutputs,
20617
20863
  outputMode: "single_task",
@@ -20648,26 +20894,56 @@ ANTES de invocar, resume outputs, cantidad de videos y datos comerciales, explic
20648
20894
  assets: []
20649
20895
  },
20650
20896
  appDemoVideos: []
20897
+ }, {
20898
+ // The 2026-09-25 incident: four retries of this exact tool created
20899
+ // four charged service tasks because the host rephrased
20900
+ // `additionalContext` and flipped the landing between `/` and
20901
+ // `/es`. The derived key is stable across both rewrites (see
20902
+ // request-key.ts).
20903
+ idempotencyKey: derivePaidRequestKey({
20904
+ fingerprint: api.fingerprint(),
20905
+ tool: "create_service_video",
20906
+ input,
20907
+ nowMs: Date.now()
20908
+ })
20651
20909
  });
20652
20910
  const angles = await api.postJson(`/v1/tasks/${encodeURIComponent(created.taskId)}/service-angles`, {});
20653
20911
  const first = angles[0];
20912
+ const replayed = created.replayed === true;
20654
20913
  if (input.reviewMode === "automatic") {
20655
20914
  if (!first) throw new Error("ProductMaker no pudo crear un concepto para este servicio.");
20656
- await api.postJson(`/v1/tasks/${encodeURIComponent(created.taskId)}/service-angles/select`, {
20657
- angleIds: [
20658
- first.id
20659
- ]
20660
- });
20915
+ if (!(replayed && created.status !== "queued")) {
20916
+ await api.postJson(`/v1/tasks/${encodeURIComponent(created.taskId)}/service-angles/select`, {
20917
+ angleIds: [
20918
+ first.id
20919
+ ]
20920
+ });
20921
+ }
20661
20922
  return asText({
20662
20923
  taskId: created.taskId,
20663
- status: "processing",
20924
+ status: replayed ? created.status ?? "processing" : "processing",
20925
+ ...replayed ? {
20926
+ replayed: true
20927
+ } : {},
20664
20928
  reviewMode: input.reviewMode,
20665
20929
  selectedConcept: first
20666
20930
  });
20667
20931
  }
20932
+ if (replayed && created.status !== "queued") {
20933
+ return asText({
20934
+ taskId: created.taskId,
20935
+ status: created.status,
20936
+ replayed: true,
20937
+ reviewMode: input.reviewMode,
20938
+ angles
20939
+ });
20940
+ }
20668
20941
  return asText({
20669
20942
  taskId: created.taskId,
20670
20943
  status: "waiting_for_user",
20944
+ ...replayed ? {
20945
+ replayed: true
20946
+ } : {},
20671
20947
  reviewMode: input.reviewMode,
20672
20948
  pendingUserAction: {
20673
20949
  kind: "service_angle_approval",
@@ -21243,7 +21519,6 @@ ANTES de invocar, resume outputs, cantidad de videos y datos comerciales, explic
21243
21519
  __name(registerTaskTools, "registerTaskTools");
21244
21520
 
21245
21521
  // src/tools/creatives.ts
21246
- import { randomUUID } from "crypto";
21247
21522
  import { z as z78 } from "zod";
21248
21523
  var generateFreeVideoGuidance = getProductMakerCapabilityModelGuidance("generate_free_video");
21249
21524
  var generateVideoCreativeGuidance = getProductMakerCapabilityModelGuidance("generate_video_creative");
@@ -21364,7 +21639,7 @@ var FreeVideoInput = z78.object({
21364
21639
  videoResolution: VideoResolutionEnum.describe("Resoluci\xF3n del video: 480p o 720p."),
21365
21640
  generateAiMusic: z78.boolean().default(false).describe("Opcional. Agrega m\xFAsica instrumental IA debajo del audio nativo. Default false."),
21366
21641
  addSubtitles: z78.boolean().default(false).describe("Opcional. Quema subt\xEDtulos usando el rawPrompt exacto. Default false."),
21367
- idempotencyKey: z78.string().uuid().optional().describe("Opcional. UUID para reintentar exactamente esta misma solicitud sin crear ni cobrar otro video. Reutiliza el mismo valor si el cliente no recibi\xF3 la respuesta.")
21642
+ idempotencyKey: z78.string().uuid().optional().describe("Opcional. Se acepta por compatibilidad pero se ignora: la identidad del reintento se deriva autom\xE1ticamente de los argumentos sem\xE1nticos, as\xED que un retry con los mismos argumentos ya es idempotente sin este campo.")
21368
21643
  }).strict();
21369
21644
  var imageStyleList = ImageCreativeStyleEnum.options.join(" | ");
21370
21645
  var ImageCreativeSetup = z78.discriminatedUnion("mode", [
@@ -21586,7 +21861,16 @@ function registerCreativeTools(server, api, { transport = "http" } = {}) {
21586
21861
  addSubtitles: input.addSubtitles
21587
21862
  },
21588
21863
  allowEmptyImages: true,
21589
- idempotencyKey: input.idempotencyKey ?? randomUUID(),
21864
+ // Always derived — never `input.idempotencyKey ??`: a host that
21865
+ // mints a fresh UUID per retry would defeat the dedupe entirely.
21866
+ // The semantic key is also a strictly better retry identity: a
21867
+ // reused caller key still lands on it (see request-key.ts).
21868
+ idempotencyKey: derivePaidRequestKey({
21869
+ fingerprint: api.fingerprint(),
21870
+ tool: "generate_free_video",
21871
+ input,
21872
+ nowMs: Date.now()
21873
+ }),
21590
21874
  transport
21591
21875
  });
21592
21876
  } catch (e) {
@@ -21651,6 +21935,12 @@ function registerCreativeTools(server, api, { transport = "http" } = {}) {
21651
21935
  path: "/v1/creatives/videos",
21652
21936
  estimatedSeconds: 300,
21653
21937
  input: wirePayload,
21938
+ idempotencyKey: derivePaidRequestKey({
21939
+ fingerprint: api.fingerprint(),
21940
+ tool: "generate_video_creative",
21941
+ input,
21942
+ nowMs: Date.now()
21943
+ }),
21654
21944
  transport
21655
21945
  });
21656
21946
  } catch (e) {
@@ -21685,6 +21975,12 @@ function registerCreativeTools(server, api, { transport = "http" } = {}) {
21685
21975
  pauseForAngleReview: true
21686
21976
  }
21687
21977
  },
21978
+ idempotencyKey: derivePaidRequestKey({
21979
+ fingerprint: api.fingerprint(),
21980
+ tool: "generate_image_creatives",
21981
+ input,
21982
+ nowMs: Date.now()
21983
+ }),
21688
21984
  transport
21689
21985
  });
21690
21986
  } catch (e) {
@@ -21728,6 +22024,12 @@ function registerCreativeTools(server, api, { transport = "http" } = {}) {
21728
22024
  pauseForAngleReview: true
21729
22025
  }
21730
22026
  },
22027
+ idempotencyKey: derivePaidRequestKey({
22028
+ fingerprint: api.fingerprint(),
22029
+ tool: "generate_landing",
22030
+ input,
22031
+ nowMs: Date.now()
22032
+ }),
21731
22033
  transport
21732
22034
  });
21733
22035
  } catch (e) {
@@ -21859,7 +22161,7 @@ var CreateCreativeVariantInputBase = z79.object({
21859
22161
  sourceTaskId: z79.string().uuid().describe("ID de la tarea original completada."),
21860
22162
  kind: CreativeVariantKindEnum,
21861
22163
  confirm: z79.literal(true).describe("Confirmaci\xF3n expl\xEDcita del usuario despu\xE9s de mostrarle el estimado de cr\xE9ditos."),
21862
- clientRequestId: z79.string().uuid().describe("UUID estable y obligatorio para esta intenci\xF3n. Reutil\xEDzalo ante timeouts o transporte ambiguo. Si la API devuelve status=error, una nueva generaci\xF3n exige otra confirmaci\xF3n y un UUID nuevo."),
22164
+ clientRequestId: z79.string().uuid().describe("UUID estable para esta intenci\xF3n (obligatorio por compatibilidad de contrato; el servidor deriva la identidad real del request desde los argumentos sem\xE1nticos, as\xED que un UUID nuevo en un retry NO crea una variante duplicada). Si la API devuelve status=error, una nueva generaci\xF3n exige otra confirmaci\xF3n."),
21863
22165
  angle: z79.string().trim().min(1).max(500).optional().describe("\xC1ngulo nuevo para video o imagen."),
21864
22166
  buyerPersona: z79.string().trim().min(1).max(360).optional().describe("Buyer persona coherente con el \xE1ngulo."),
21865
22167
  scriptNotes: z79.string().trim().min(1).max(2e3).optional().describe("Direcci\xF3n breve del guion para una variante de video."),
@@ -21981,7 +22283,7 @@ function registerCreativeVariantTools(server, api) {
21981
22283
  throw translateError(error);
21982
22284
  }
21983
22285
  });
21984
- server.tool("create_creative_variant", createCreativeVariantGuidance.summary + " ES UNA ACCI\xD3N DE PAGO: primero llama estimate_creative_variant, muestra el rango de cr\xE9ditos al usuario y solo despu\xE9s invoca este tool con confirm=true. Las recomendaciones IA tambi\xE9n son acciones pagas a 2x del costo real. clientRequestId es obligatorio: genera un UUID por intenci\xF3n y reutiliza el mismo ante timeouts o transporte ambiguo. Si recibes status=error, pide confirmaci\xF3n para un intento nuevo y usa otro UUID. Reutiliza el producto y referencias del task origen; crea un child task independiente y devuelve variantTaskId. Para landing puedes enviar instructions en texto libre y omitir sectionKey: el backend lo clasifica con IA. No expone ni permite cambiar videoModel, videoTier, videoStyle o narrativeStyle; esos valores se heredan del task origen.", CreateCreativeVariantInputBase.shape, CREATES_PAID, async (input) => {
22286
+ server.tool("create_creative_variant", createCreativeVariantGuidance.summary + " ES UNA ACCI\xD3N DE PAGO: primero llama estimate_creative_variant, muestra el rango de cr\xE9ditos al usuario y solo despu\xE9s invoca este tool con confirm=true. Las recomendaciones IA tambi\xE9n son acciones pagas a 2x del costo real. clientRequestId es obligatorio por contrato: genera un UUID por intenci\xF3n. El servidor deriva la identidad real del request desde los argumentos sem\xE1nticos, as\xED que un UUID nuevo NO desbloquea un intento distinto \u2014 si recibes status=error, pide confirmaci\xF3n al usuario y reinvoca con los mismos argumentos. Reutiliza el producto y referencias del task origen; crea un child task independiente y devuelve variantTaskId. Para landing puedes enviar instructions en texto libre y omitir sectionKey: el backend lo clasifica con IA. No expone ni permite cambiar videoModel, videoTier, videoStyle o narrativeStyle; esos valores se heredan del task origen.", CreateCreativeVariantInputBase.shape, CREATES_PAID, async (input) => {
21985
22287
  try {
21986
22288
  const validatedInput = parseVariantInput(CreateCreativeVariantInput, input);
21987
22289
  const estimateInput = EstimateCreativeVariantInput.parse({
@@ -22008,8 +22310,16 @@ function registerCreativeVariantTools(server, api) {
22008
22310
  });
22009
22311
  sectionKey = LandingSectionKeyEnum.parse(recommendation.value);
22010
22312
  }
22313
+ const derivedClientRequestId = derivePaidRequestKey({
22314
+ fingerprint: api.fingerprint(),
22315
+ tool: "create_creative_variant",
22316
+ input: {
22317
+ ...validatedInput
22318
+ },
22319
+ nowMs: Date.now()
22320
+ });
22011
22321
  const payload = {
22012
- clientRequestId: validatedInput.clientRequestId,
22322
+ clientRequestId: derivedClientRequestId,
22013
22323
  kind: validatedInput.kind,
22014
22324
  ...validatedInput.kind !== "landing" ? {
22015
22325
  angle: validatedInput.angle,
@@ -22035,7 +22345,7 @@ function registerCreativeVariantTools(server, api) {
22035
22345
  };
22036
22346
  const created = await api.postJson(`/v1/tasks/${encodeURIComponent(validatedInput.sourceTaskId)}/creative-variants`, payload);
22037
22347
  if (created.status === "error") {
22038
- throw mcp(McpErrorCode.CREATIVE_VARIANT_FAILED, "La variante anterior termin\xF3 con error. Pide confirmaci\xF3n para un intento nuevo y usa otro clientRequestId.", {
22348
+ throw mcp(McpErrorCode.CREATIVE_VARIANT_FAILED, "La variante anterior termin\xF3 con error. Pide confirmaci\xF3n al usuario y reinvoca con los mismos argumentos para un intento nuevo.", {
22039
22349
  variantTaskId: created.variantTaskId
22040
22350
  });
22041
22351
  }
@@ -22250,39 +22560,6 @@ __name(registerActorTools, "registerActorTools");
22250
22560
  // src/tools/publish.ts
22251
22561
  import { z as z83 } from "zod";
22252
22562
 
22253
- // src/tools/stable-uuid.ts
22254
- import { createHash as createHash3 } from "crypto";
22255
- function canonicalize(value) {
22256
- if (Array.isArray(value)) return value.map(canonicalize);
22257
- if (value === null || typeof value !== "object") return value;
22258
- const record = value;
22259
- return Object.fromEntries(Object.keys(record).sort().filter((key) => record[key] !== void 0).map((key) => [
22260
- key,
22261
- canonicalize(record[key])
22262
- ]));
22263
- }
22264
- __name(canonicalize, "canonicalize");
22265
- function canonicalJson(value) {
22266
- return JSON.stringify(canonicalize(value));
22267
- }
22268
- __name(canonicalJson, "canonicalJson");
22269
- function deriveUuidV5(name, namespace) {
22270
- const namespaceBytes = Buffer.from(namespace.replaceAll("-", ""), "hex");
22271
- const digest = createHash3("sha1").update(namespaceBytes).update(name, "utf8").digest();
22272
- const bytes = Buffer.from(digest.subarray(0, 16));
22273
- bytes[6] = (bytes[6] ?? 0) & 15 | 80;
22274
- bytes[8] = (bytes[8] ?? 0) & 63 | 128;
22275
- const hex = bytes.toString("hex");
22276
- return [
22277
- hex.slice(0, 8),
22278
- hex.slice(8, 12),
22279
- hex.slice(12, 16),
22280
- hex.slice(16, 20),
22281
- hex.slice(20)
22282
- ].join("-");
22283
- }
22284
- __name(deriveUuidV5, "deriveUuidV5");
22285
-
22286
22563
  // src/tools/meta-publish-attempt-id.ts
22287
22564
  var META_PUBLISH_NAMESPACE = "7b19c9ac-b4eb-4db1-970e-c2f24ce30b98";
22288
22565
  function deriveMetaPublishAttemptId(input) {
@@ -24931,6 +25208,16 @@ function firstHeader(value) {
24931
25208
  }
24932
25209
  __name(firstHeader, "firstHeader");
24933
25210
  var PROXY_FETCH_TIMEOUT_MS = 3e4;
25211
+ var DEFAULT_SUBJECT_TOKEN_REAUTH_WINDOW_MS = 18e4;
25212
+ function subjectTokenReauthWindowMs(env) {
25213
+ const raw = env.MCP_SUBJECT_TOKEN_REAUTH_WINDOW_MS;
25214
+ if (raw === void 0 || raw.trim() === "") {
25215
+ return DEFAULT_SUBJECT_TOKEN_REAUTH_WINDOW_MS;
25216
+ }
25217
+ const parsed = Number.parseInt(raw, 10);
25218
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_SUBJECT_TOKEN_REAUTH_WINDOW_MS;
25219
+ }
25220
+ __name(subjectTokenReauthWindowMs, "subjectTokenReauthWindowMs");
24934
25221
  var INLINE_RENDERABLE_MIMES = /* @__PURE__ */ new Set([
24935
25222
  "image/jpeg",
24936
25223
  "image/png",
@@ -25061,7 +25348,8 @@ function resolveDownstreamAuth(req, opts, oauthAnalytics) {
25061
25348
  credential,
25062
25349
  subject,
25063
25350
  clientId,
25064
- isLegacy: false
25351
+ isLegacy: false,
25352
+ subjectExpiresAt: expiresAt
25065
25353
  };
25066
25354
  }
25067
25355
  __name(resolveDownstreamAuth, "resolveDownstreamAuth");
@@ -25085,6 +25373,14 @@ function createAuthenticatedMcpHandler(opts, oauthAnalytics) {
25085
25373
  return;
25086
25374
  }
25087
25375
  const { credential, subject, clientId, isLegacy } = authContext;
25376
+ const reauthWindowMs = subjectTokenReauthWindowMs(process.env);
25377
+ if (reauthWindowMs > 0 && !isLegacy && typeof authContext.subjectExpiresAt === "number" && authContext.subjectExpiresAt * 1e3 <= Date.now() + reauthWindowMs) {
25378
+ res.status(401).set("WWW-Authenticate", `Bearer error="invalid_token", error_description="Access token expired or expiring soon", scope="${MCP_SCOPE}", resource_metadata="${getOAuthProtectedResourceMetadataUrl2(MCP_RESOURCE)}"`).json({
25379
+ error: "invalid_token",
25380
+ error_description: "Access token expired or expiring soon; refresh or re-authenticate."
25381
+ });
25382
+ return;
25383
+ }
25088
25384
  const abortController = new AbortController();
25089
25385
  const abortRequest = /* @__PURE__ */ __name(() => abortController.abort(), "abortRequest");
25090
25386
  req.once("aborted", abortRequest);