@astrasyncai/verification-gateway 5.18.0 → 5.19.0

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.
Files changed (115) hide show
  1. package/CHANGELOG.md +178 -0
  2. package/README.md +168 -4
  3. package/dist/adapters/express.d.mts +9 -1
  4. package/dist/adapters/express.d.ts +9 -1
  5. package/dist/adapters/express.js +96 -37
  6. package/dist/adapters/express.js.map +1 -1
  7. package/dist/adapters/express.mjs +96 -37
  8. package/dist/adapters/express.mjs.map +1 -1
  9. package/dist/adapters/http-pdlss.d.mts +7 -1
  10. package/dist/adapters/http-pdlss.d.ts +7 -1
  11. package/dist/adapters/http-pdlss.js +18 -2
  12. package/dist/adapters/http-pdlss.js.map +1 -1
  13. package/dist/adapters/http-pdlss.mjs +16 -1
  14. package/dist/adapters/http-pdlss.mjs.map +1 -1
  15. package/dist/adapters/kit-express.d.mts +93 -1
  16. package/dist/adapters/kit-express.d.ts +93 -1
  17. package/dist/adapters/kit-express.js +181 -2
  18. package/dist/adapters/kit-express.js.map +1 -1
  19. package/dist/adapters/kit-express.mjs +180 -2
  20. package/dist/adapters/kit-express.mjs.map +1 -1
  21. package/dist/adapters/mcp.d.mts +80 -9
  22. package/dist/adapters/mcp.d.ts +80 -9
  23. package/dist/adapters/mcp.js +153 -5
  24. package/dist/adapters/mcp.js.map +1 -1
  25. package/dist/adapters/mcp.mjs +151 -5
  26. package/dist/adapters/mcp.mjs.map +1 -1
  27. package/dist/adapters/nextjs.d.mts +1 -1
  28. package/dist/adapters/nextjs.d.ts +1 -1
  29. package/dist/adapters/nextjs.js +41 -4
  30. package/dist/adapters/nextjs.js.map +1 -1
  31. package/dist/adapters/nextjs.mjs +41 -4
  32. package/dist/adapters/nextjs.mjs.map +1 -1
  33. package/dist/adapters/sdk.d.mts +44 -1
  34. package/dist/adapters/sdk.d.ts +44 -1
  35. package/dist/adapters/sdk.js +76 -2
  36. package/dist/adapters/sdk.js.map +1 -1
  37. package/dist/adapters/sdk.mjs +76 -2
  38. package/dist/adapters/sdk.mjs.map +1 -1
  39. package/dist/agent/index.js +1 -1
  40. package/dist/agent/index.js.map +1 -1
  41. package/dist/agent/index.mjs +1 -1
  42. package/dist/agent/index.mjs.map +1 -1
  43. package/dist/bin/astrasync-claude-hook.js +46 -6
  44. package/dist/bin/astrasync-codex-hook.js +46 -6
  45. package/dist/bin/astrasync-guard.js +375 -117
  46. package/dist/bin/astrasync.js +417 -117
  47. package/dist/browser/background.js +46 -6
  48. package/dist/browser/background.js.map +1 -1
  49. package/dist/browser/background.mjs +46 -6
  50. package/dist/browser/background.mjs.map +1 -1
  51. package/dist/cli/index.js +1 -1
  52. package/dist/cli/index.js.map +1 -1
  53. package/dist/cli/index.mjs +1 -1
  54. package/dist/cli/index.mjs.map +1 -1
  55. package/dist/codex/index.js +46 -6
  56. package/dist/codex/index.js.map +1 -1
  57. package/dist/codex/index.mjs +46 -6
  58. package/dist/codex/index.mjs.map +1 -1
  59. package/dist/cursor/extension.js +46 -6
  60. package/dist/cursor/extension.js.map +1 -1
  61. package/dist/cursor/extension.mjs +46 -6
  62. package/dist/cursor/extension.mjs.map +1 -1
  63. package/dist/edge-config.d.mts +1 -1
  64. package/dist/edge-config.d.ts +1 -1
  65. package/dist/edge-config.js +1 -1
  66. package/dist/edge-config.js.map +1 -1
  67. package/dist/edge-config.mjs +1 -1
  68. package/dist/edge-config.mjs.map +1 -1
  69. package/dist/edge-core/index.d.mts +1 -1
  70. package/dist/edge-core/index.d.ts +1 -1
  71. package/dist/edge-core/index.js +21 -2
  72. package/dist/edge-core/index.js.map +1 -1
  73. package/dist/edge-core/index.mjs +21 -2
  74. package/dist/edge-core/index.mjs.map +1 -1
  75. package/dist/gateway/gateway.js +46 -6
  76. package/dist/gateway/gateway.js.map +1 -1
  77. package/dist/gateway/gateway.mjs +46 -6
  78. package/dist/gateway/gateway.mjs.map +1 -1
  79. package/dist/git-trigger/git-hooks.d.mts +1 -1
  80. package/dist/git-trigger/git-hooks.d.ts +1 -1
  81. package/dist/index.d.mts +542 -30
  82. package/dist/index.d.ts +542 -30
  83. package/dist/index.js +1171 -183
  84. package/dist/index.js.map +1 -1
  85. package/dist/index.mjs +1157 -173
  86. package/dist/index.mjs.map +1 -1
  87. package/dist/registration/index.d.mts +1 -1
  88. package/dist/registration/index.d.ts +1 -1
  89. package/dist/registration/index.js +1 -1
  90. package/dist/registration/index.js.map +1 -1
  91. package/dist/registration/index.mjs +1 -1
  92. package/dist/registration/index.mjs.map +1 -1
  93. package/dist/transport/index.js +3 -2
  94. package/dist/transport/index.js.map +1 -1
  95. package/dist/transport/index.mjs +3 -2
  96. package/dist/transport/index.mjs.map +1 -1
  97. package/dist/{types-3ObBpsnf.d.mts → types-BoGjyon0.d.mts} +81 -2
  98. package/dist/{types-Cns4l8gX.d.mts → types-DqWPkiYl.d.mts} +90 -12
  99. package/dist/{types-Cns4l8gX.d.ts → types-DqWPkiYl.d.ts} +90 -12
  100. package/dist/{types-BoxZEYPS.d.ts → types-eWyZuz-3.d.ts} +81 -2
  101. package/dist/ui/index.d.mts +1 -1
  102. package/dist/ui/index.d.ts +1 -1
  103. package/dist/verify.d.mts +23 -2
  104. package/dist/verify.d.ts +23 -2
  105. package/dist/verify.js +23 -4
  106. package/dist/verify.js.map +1 -1
  107. package/dist/verify.mjs +23 -4
  108. package/dist/verify.mjs.map +1 -1
  109. package/dist/webhooks.d.mts +16 -3
  110. package/dist/webhooks.d.ts +16 -3
  111. package/dist/webhooks.js +24 -13
  112. package/dist/webhooks.js.map +1 -1
  113. package/dist/webhooks.mjs +24 -13
  114. package/dist/webhooks.mjs.map +1 -1
  115. package/package.json +1 -1
@@ -77,5 +77,11 @@ declare function normalizePurposeHeader(value: string): {
77
77
  * outranks agent-supplied headers the way MCP toolGates do.
78
78
  */
79
79
  declare function resolveHttpPdlss(input: HttpPdlssInput): HttpPdlssResolution;
80
+ /**
81
+ * 5.19.0 — the coarse purpose source sent to verify-access as
82
+ * `purposeSource`. Only `'route_config'` changes server behaviour (checked
83
+ * against the endpoint's stored rules); the rest are attribution.
84
+ */
85
+ declare function wirePurposeSource(source: HttpPurposeSource): 'route_config' | 'header' | 'body' | 'default';
80
86
 
81
- export { DEFAULT_HTTP_ACTION, DEFAULT_HTTP_PURPOSE, HTTP_METHOD_ACTION_TABLE, type HttpActionSource, type HttpPdlssInput, type HttpPdlssResolution, type HttpPurposeSource, actionForHttpMethod, normalizePurposeHeader, resolveHttpPdlss };
87
+ export { DEFAULT_HTTP_ACTION, DEFAULT_HTTP_PURPOSE, HTTP_METHOD_ACTION_TABLE, type HttpActionSource, type HttpPdlssInput, type HttpPdlssResolution, type HttpPurposeSource, actionForHttpMethod, normalizePurposeHeader, resolveHttpPdlss, wirePurposeSource };
@@ -77,5 +77,11 @@ declare function normalizePurposeHeader(value: string): {
77
77
  * outranks agent-supplied headers the way MCP toolGates do.
78
78
  */
79
79
  declare function resolveHttpPdlss(input: HttpPdlssInput): HttpPdlssResolution;
80
+ /**
81
+ * 5.19.0 — the coarse purpose source sent to verify-access as
82
+ * `purposeSource`. Only `'route_config'` changes server behaviour (checked
83
+ * against the endpoint's stored rules); the rest are attribution.
84
+ */
85
+ declare function wirePurposeSource(source: HttpPurposeSource): 'route_config' | 'header' | 'body' | 'default';
80
86
 
81
- export { DEFAULT_HTTP_ACTION, DEFAULT_HTTP_PURPOSE, HTTP_METHOD_ACTION_TABLE, type HttpActionSource, type HttpPdlssInput, type HttpPdlssResolution, type HttpPurposeSource, actionForHttpMethod, normalizePurposeHeader, resolveHttpPdlss };
87
+ export { DEFAULT_HTTP_ACTION, DEFAULT_HTTP_PURPOSE, HTTP_METHOD_ACTION_TABLE, type HttpActionSource, type HttpPdlssInput, type HttpPdlssResolution, type HttpPurposeSource, actionForHttpMethod, normalizePurposeHeader, resolveHttpPdlss, wirePurposeSource };
@@ -25,7 +25,8 @@ __export(http_pdlss_exports, {
25
25
  HTTP_METHOD_ACTION_TABLE: () => HTTP_METHOD_ACTION_TABLE,
26
26
  actionForHttpMethod: () => actionForHttpMethod,
27
27
  normalizePurposeHeader: () => normalizePurposeHeader,
28
- resolveHttpPdlss: () => resolveHttpPdlss
28
+ resolveHttpPdlss: () => resolveHttpPdlss,
29
+ wirePurposeSource: () => wirePurposeSource
29
30
  });
30
31
  module.exports = __toCommonJS(http_pdlss_exports);
31
32
  var HTTP_METHOD_ACTION_TABLE = {
@@ -105,6 +106,20 @@ function resolveHttpPdlss(input) {
105
106
  }
106
107
  return { purpose, action, purposeSource, actionSource };
107
108
  }
109
+ function wirePurposeSource(source) {
110
+ switch (source) {
111
+ case "route_config":
112
+ return "route_config";
113
+ case "header":
114
+ case "legacy_header":
115
+ case "query":
116
+ return "header";
117
+ case "custom_extractor":
118
+ return "body";
119
+ default:
120
+ return "default";
121
+ }
122
+ }
108
123
  // Annotate the CommonJS export names for ESM import in node:
109
124
  0 && (module.exports = {
110
125
  DEFAULT_HTTP_ACTION,
@@ -112,6 +127,7 @@ function resolveHttpPdlss(input) {
112
127
  HTTP_METHOD_ACTION_TABLE,
113
128
  actionForHttpMethod,
114
129
  normalizePurposeHeader,
115
- resolveHttpPdlss
130
+ resolveHttpPdlss,
131
+ wirePurposeSource
116
132
  });
117
133
  //# sourceMappingURL=http-pdlss.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/adapters/http-pdlss.ts"],"sourcesContent":["/**\n * Shared purpose/action resolver for the HTTP adapters (express + nextjs) —\n * the vocabulary-unification layer.\n *\n * The PDLSS two-axis contract: `purpose` = bare category noun (`shopping`,\n * `data`, custom nouns like `trading`); `action` = dotted verb\n * (`shopping.search`) or an enumerated transport token. Transport verbs\n * (GET/POST/...) NEVER travel as PDLSS actions — they map through the pinned\n * method table below.\n *\n * SELF-CONTAINED on purpose: this published package cannot depend on the\n * private @astrasyncai/pdlss-vocab workspace package, so the constants are\n * duplicated here and pinned strictly equal by a monorepo CI test\n * (vocab-equality.test.ts). Change them in BOTH places or that test fails.\n */\n\n/** Pinned method→action derivation table. */\nexport const HTTP_METHOD_ACTION_TABLE: Readonly<Record<string, string>> = {\n GET: 'data.read',\n HEAD: 'data.read',\n OPTIONS: 'data.read',\n POST: 'data.write',\n PUT: 'data.write',\n PATCH: 'data.write',\n DELETE: 'data.delete',\n};\n\n/** Unknown methods are treated as mutating until proven otherwise. */\nexport const DEFAULT_HTTP_ACTION = 'data.write';\n\n/** Generic category emitted when nothing supplies a domain purpose. */\nexport const DEFAULT_HTTP_PURPOSE = 'data';\n\nexport function actionForHttpMethod(method: string): string {\n return HTTP_METHOD_ACTION_TABLE[method.toUpperCase()] ?? DEFAULT_HTTP_ACTION;\n}\n\nexport type HttpPurposeSource =\n | 'route_config'\n | 'custom_extractor'\n | 'header'\n | 'legacy_header'\n | 'query'\n | 'action_derived'\n | 'transport_default';\n\nexport type HttpActionSource =\n | 'route_config'\n | 'custom_extractor'\n | 'header'\n | 'purpose_header_derived'\n | 'method_table';\n\nexport interface HttpPdlssInput {\n method: string;\n /** `X-Astra-Purpose` header value (agent's declaration). */\n astraPurpose?: string;\n /** `X-Astra-Action` header value (agent's declaration, symmetric with MCP). */\n astraAction?: string;\n /** Legacy `x-purpose` header — deprecated, removal scheduled for 4.0.0. */\n legacyPurpose?: string;\n /** Legacy `?purpose` query param (express only) — deprecated, removal 4.0.0. */\n queryPurpose?: string;\n /** Dashboard route config send-mapping (RouteAccessConfig.purpose/.action). */\n routePurpose?: string;\n routeAction?: string;\n /** Results of the merchant's extractPurpose/extractAction options. */\n customPurpose?: string;\n hasCustomPurposeExtractor?: boolean;\n customAction?: string;\n hasCustomActionExtractor?: boolean;\n}\n\nexport interface HttpPdlssResolution {\n purpose: string;\n action: string;\n purposeSource: HttpPurposeSource;\n actionSource: HttpActionSource;\n}\n\n/**\n * Normalize an `X-Astra-Purpose` header value.\n * - colon form `\"category:action\"` (legacy wire shape): take the prefix; the\n * remainder is free-text and exists in no vocabulary — discard it and let\n * the method table supply the action (matches pre-3.1.0 discard behavior).\n * - dotted form `\"shopping.purchase\"` (the shape older docs taught): prefix\n * becomes the purpose, the WHOLE token becomes an action candidate.\n * - bare token: purpose verbatim.\n */\nexport function normalizePurposeHeader(value: string): {\n purpose: string;\n actionCandidate?: string;\n} {\n const colon = value.indexOf(':');\n if (colon >= 0) {\n return { purpose: value.slice(0, colon) };\n }\n const dot = value.indexOf('.');\n if (dot > 0 && dot < value.length - 1) {\n return { purpose: value.slice(0, dot), actionCandidate: value };\n }\n return { purpose: value };\n}\n\n/**\n * Resolve the {purpose, action} pair for one HTTP request.\n *\n * Precedence (both axes):\n * action: route mapping → extractAction option → X-Astra-Action header →\n * candidate from a dotted X-Astra-Purpose → pinned method table\n * purpose: route mapping → extractPurpose option → X-Astra-Purpose →\n * legacy x-purpose / ?purpose → category prefix of the resolved\n * action → 'data'\n *\n * A configured custom extractor masks the header/legacy/query steps of its\n * axis (it replaces the default chain, as extractPurpose always has); when it\n * returns undefined the axis falls through to the table/prefix fallback.\n * The route mapping is merchant policy fetched from the dashboard — it\n * outranks agent-supplied headers the way MCP toolGates do.\n */\nexport function resolveHttpPdlss(input: HttpPdlssInput): HttpPdlssResolution {\n const fromHeader = input.astraPurpose ? normalizePurposeHeader(input.astraPurpose) : undefined;\n\n // ── Action axis ──\n let action: string;\n let actionSource: HttpActionSource;\n if (input.routeAction) {\n action = input.routeAction;\n actionSource = 'route_config';\n } else if (input.hasCustomActionExtractor && input.customAction) {\n action = input.customAction;\n actionSource = 'custom_extractor';\n } else if (!input.hasCustomActionExtractor && input.astraAction) {\n action = input.astraAction;\n actionSource = 'header';\n } else if (!input.hasCustomActionExtractor && fromHeader?.actionCandidate) {\n action = fromHeader.actionCandidate;\n actionSource = 'purpose_header_derived';\n } else {\n action = actionForHttpMethod(input.method);\n actionSource = 'method_table';\n }\n\n // ── Purpose axis ──\n let purpose: string | undefined;\n let purposeSource: HttpPurposeSource | undefined;\n if (input.routePurpose) {\n purpose = input.routePurpose;\n purposeSource = 'route_config';\n } else if (input.hasCustomPurposeExtractor) {\n if (input.customPurpose) {\n purpose = input.customPurpose;\n purposeSource = 'custom_extractor';\n }\n } else if (fromHeader) {\n purpose = fromHeader.purpose;\n purposeSource = 'header';\n } else if (input.legacyPurpose) {\n purpose = input.legacyPurpose;\n purposeSource = 'legacy_header';\n } else if (input.queryPurpose) {\n purpose = input.queryPurpose;\n purposeSource = 'query';\n }\n\n if (!purpose) {\n // Keep the pair coherent: an agent that sent only X-Astra-Action (or a\n // route that mapped only the action) gets that action's own category.\n const dot = action.indexOf('.');\n if (dot > 0) {\n purpose = action.slice(0, dot);\n purposeSource = 'action_derived';\n } else {\n purpose = DEFAULT_HTTP_PURPOSE;\n purposeSource = 'transport_default';\n }\n }\n\n return { purpose, action, purposeSource: purposeSource!, actionSource };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAiBO,IAAM,2BAA6D;AAAA,EACxE,KAAK;AAAA,EACL,MAAM;AAAA,EACN,SAAS;AAAA,EACT,MAAM;AAAA,EACN,KAAK;AAAA,EACL,OAAO;AAAA,EACP,QAAQ;AACV;AAGO,IAAM,sBAAsB;AAG5B,IAAM,uBAAuB;AAE7B,SAAS,oBAAoB,QAAwB;AAC1D,SAAO,yBAAyB,OAAO,YAAY,CAAC,KAAK;AAC3D;AAsDO,SAAS,uBAAuB,OAGrC;AACA,QAAM,QAAQ,MAAM,QAAQ,GAAG;AAC/B,MAAI,SAAS,GAAG;AACd,WAAO,EAAE,SAAS,MAAM,MAAM,GAAG,KAAK,EAAE;AAAA,EAC1C;AACA,QAAM,MAAM,MAAM,QAAQ,GAAG;AAC7B,MAAI,MAAM,KAAK,MAAM,MAAM,SAAS,GAAG;AACrC,WAAO,EAAE,SAAS,MAAM,MAAM,GAAG,GAAG,GAAG,iBAAiB,MAAM;AAAA,EAChE;AACA,SAAO,EAAE,SAAS,MAAM;AAC1B;AAkBO,SAAS,iBAAiB,OAA4C;AAC3E,QAAM,aAAa,MAAM,eAAe,uBAAuB,MAAM,YAAY,IAAI;AAGrF,MAAI;AACJ,MAAI;AACJ,MAAI,MAAM,aAAa;AACrB,aAAS,MAAM;AACf,mBAAe;AAAA,EACjB,WAAW,MAAM,4BAA4B,MAAM,cAAc;AAC/D,aAAS,MAAM;AACf,mBAAe;AAAA,EACjB,WAAW,CAAC,MAAM,4BAA4B,MAAM,aAAa;AAC/D,aAAS,MAAM;AACf,mBAAe;AAAA,EACjB,WAAW,CAAC,MAAM,4BAA4B,YAAY,iBAAiB;AACzE,aAAS,WAAW;AACpB,mBAAe;AAAA,EACjB,OAAO;AACL,aAAS,oBAAoB,MAAM,MAAM;AACzC,mBAAe;AAAA,EACjB;AAGA,MAAI;AACJ,MAAI;AACJ,MAAI,MAAM,cAAc;AACtB,cAAU,MAAM;AAChB,oBAAgB;AAAA,EAClB,WAAW,MAAM,2BAA2B;AAC1C,QAAI,MAAM,eAAe;AACvB,gBAAU,MAAM;AAChB,sBAAgB;AAAA,IAClB;AAAA,EACF,WAAW,YAAY;AACrB,cAAU,WAAW;AACrB,oBAAgB;AAAA,EAClB,WAAW,MAAM,eAAe;AAC9B,cAAU,MAAM;AAChB,oBAAgB;AAAA,EAClB,WAAW,MAAM,cAAc;AAC7B,cAAU,MAAM;AAChB,oBAAgB;AAAA,EAClB;AAEA,MAAI,CAAC,SAAS;AAGZ,UAAM,MAAM,OAAO,QAAQ,GAAG;AAC9B,QAAI,MAAM,GAAG;AACX,gBAAU,OAAO,MAAM,GAAG,GAAG;AAC7B,sBAAgB;AAAA,IAClB,OAAO;AACL,gBAAU;AACV,sBAAgB;AAAA,IAClB;AAAA,EACF;AAEA,SAAO,EAAE,SAAS,QAAQ,eAA+B,aAAa;AACxE;","names":[]}
1
+ {"version":3,"sources":["../../src/adapters/http-pdlss.ts"],"sourcesContent":["/**\n * Shared purpose/action resolver for the HTTP adapters (express + nextjs) —\n * the vocabulary-unification layer.\n *\n * The PDLSS two-axis contract: `purpose` = bare category noun (`shopping`,\n * `data`, custom nouns like `trading`); `action` = dotted verb\n * (`shopping.search`) or an enumerated transport token. Transport verbs\n * (GET/POST/...) NEVER travel as PDLSS actions — they map through the pinned\n * method table below.\n *\n * SELF-CONTAINED on purpose: this published package cannot depend on the\n * private @astrasyncai/pdlss-vocab workspace package, so the constants are\n * duplicated here and pinned strictly equal by a monorepo CI test\n * (vocab-equality.test.ts). Change them in BOTH places or that test fails.\n */\n\n/** Pinned method→action derivation table. */\nexport const HTTP_METHOD_ACTION_TABLE: Readonly<Record<string, string>> = {\n GET: 'data.read',\n HEAD: 'data.read',\n OPTIONS: 'data.read',\n POST: 'data.write',\n PUT: 'data.write',\n PATCH: 'data.write',\n DELETE: 'data.delete',\n};\n\n/** Unknown methods are treated as mutating until proven otherwise. */\nexport const DEFAULT_HTTP_ACTION = 'data.write';\n\n/** Generic category emitted when nothing supplies a domain purpose. */\nexport const DEFAULT_HTTP_PURPOSE = 'data';\n\nexport function actionForHttpMethod(method: string): string {\n return HTTP_METHOD_ACTION_TABLE[method.toUpperCase()] ?? DEFAULT_HTTP_ACTION;\n}\n\nexport type HttpPurposeSource =\n | 'route_config'\n | 'custom_extractor'\n | 'header'\n | 'legacy_header'\n | 'query'\n | 'action_derived'\n | 'transport_default';\n\nexport type HttpActionSource =\n | 'route_config'\n | 'custom_extractor'\n | 'header'\n | 'purpose_header_derived'\n | 'method_table';\n\nexport interface HttpPdlssInput {\n method: string;\n /** `X-Astra-Purpose` header value (agent's declaration). */\n astraPurpose?: string;\n /** `X-Astra-Action` header value (agent's declaration, symmetric with MCP). */\n astraAction?: string;\n /** Legacy `x-purpose` header — deprecated, removal scheduled for 4.0.0. */\n legacyPurpose?: string;\n /** Legacy `?purpose` query param (express only) — deprecated, removal 4.0.0. */\n queryPurpose?: string;\n /** Dashboard route config send-mapping (RouteAccessConfig.purpose/.action). */\n routePurpose?: string;\n routeAction?: string;\n /** Results of the merchant's extractPurpose/extractAction options. */\n customPurpose?: string;\n hasCustomPurposeExtractor?: boolean;\n customAction?: string;\n hasCustomActionExtractor?: boolean;\n}\n\nexport interface HttpPdlssResolution {\n purpose: string;\n action: string;\n purposeSource: HttpPurposeSource;\n actionSource: HttpActionSource;\n}\n\n/**\n * Normalize an `X-Astra-Purpose` header value.\n * - colon form `\"category:action\"` (legacy wire shape): take the prefix; the\n * remainder is free-text and exists in no vocabulary — discard it and let\n * the method table supply the action (matches pre-3.1.0 discard behavior).\n * - dotted form `\"shopping.purchase\"` (the shape older docs taught): prefix\n * becomes the purpose, the WHOLE token becomes an action candidate.\n * - bare token: purpose verbatim.\n */\nexport function normalizePurposeHeader(value: string): {\n purpose: string;\n actionCandidate?: string;\n} {\n const colon = value.indexOf(':');\n if (colon >= 0) {\n return { purpose: value.slice(0, colon) };\n }\n const dot = value.indexOf('.');\n if (dot > 0 && dot < value.length - 1) {\n return { purpose: value.slice(0, dot), actionCandidate: value };\n }\n return { purpose: value };\n}\n\n/**\n * Resolve the {purpose, action} pair for one HTTP request.\n *\n * Precedence (both axes):\n * action: route mapping → extractAction option → X-Astra-Action header →\n * candidate from a dotted X-Astra-Purpose → pinned method table\n * purpose: route mapping → extractPurpose option → X-Astra-Purpose →\n * legacy x-purpose / ?purpose → category prefix of the resolved\n * action → 'data'\n *\n * A configured custom extractor masks the header/legacy/query steps of its\n * axis (it replaces the default chain, as extractPurpose always has); when it\n * returns undefined the axis falls through to the table/prefix fallback.\n * The route mapping is merchant policy fetched from the dashboard — it\n * outranks agent-supplied headers the way MCP toolGates do.\n */\nexport function resolveHttpPdlss(input: HttpPdlssInput): HttpPdlssResolution {\n const fromHeader = input.astraPurpose ? normalizePurposeHeader(input.astraPurpose) : undefined;\n\n // ── Action axis ──\n let action: string;\n let actionSource: HttpActionSource;\n if (input.routeAction) {\n action = input.routeAction;\n actionSource = 'route_config';\n } else if (input.hasCustomActionExtractor && input.customAction) {\n action = input.customAction;\n actionSource = 'custom_extractor';\n } else if (!input.hasCustomActionExtractor && input.astraAction) {\n action = input.astraAction;\n actionSource = 'header';\n } else if (!input.hasCustomActionExtractor && fromHeader?.actionCandidate) {\n action = fromHeader.actionCandidate;\n actionSource = 'purpose_header_derived';\n } else {\n action = actionForHttpMethod(input.method);\n actionSource = 'method_table';\n }\n\n // ── Purpose axis ──\n let purpose: string | undefined;\n let purposeSource: HttpPurposeSource | undefined;\n if (input.routePurpose) {\n purpose = input.routePurpose;\n purposeSource = 'route_config';\n } else if (input.hasCustomPurposeExtractor) {\n if (input.customPurpose) {\n purpose = input.customPurpose;\n purposeSource = 'custom_extractor';\n }\n } else if (fromHeader) {\n purpose = fromHeader.purpose;\n purposeSource = 'header';\n } else if (input.legacyPurpose) {\n purpose = input.legacyPurpose;\n purposeSource = 'legacy_header';\n } else if (input.queryPurpose) {\n purpose = input.queryPurpose;\n purposeSource = 'query';\n }\n\n if (!purpose) {\n // Keep the pair coherent: an agent that sent only X-Astra-Action (or a\n // route that mapped only the action) gets that action's own category.\n const dot = action.indexOf('.');\n if (dot > 0) {\n purpose = action.slice(0, dot);\n purposeSource = 'action_derived';\n } else {\n purpose = DEFAULT_HTTP_PURPOSE;\n purposeSource = 'transport_default';\n }\n }\n\n return { purpose, action, purposeSource: purposeSource!, actionSource };\n}\n\n/**\n * 5.19.0 — the coarse purpose source sent to verify-access as\n * `purposeSource`. Only `'route_config'` changes server behaviour (checked\n * against the endpoint's stored rules); the rest are attribution.\n */\nexport function wirePurposeSource(\n source: HttpPurposeSource\n): 'route_config' | 'header' | 'body' | 'default' {\n switch (source) {\n case 'route_config':\n return 'route_config';\n case 'header':\n case 'legacy_header':\n case 'query':\n return 'header';\n case 'custom_extractor':\n return 'body';\n default:\n return 'default';\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAiBO,IAAM,2BAA6D;AAAA,EACxE,KAAK;AAAA,EACL,MAAM;AAAA,EACN,SAAS;AAAA,EACT,MAAM;AAAA,EACN,KAAK;AAAA,EACL,OAAO;AAAA,EACP,QAAQ;AACV;AAGO,IAAM,sBAAsB;AAG5B,IAAM,uBAAuB;AAE7B,SAAS,oBAAoB,QAAwB;AAC1D,SAAO,yBAAyB,OAAO,YAAY,CAAC,KAAK;AAC3D;AAsDO,SAAS,uBAAuB,OAGrC;AACA,QAAM,QAAQ,MAAM,QAAQ,GAAG;AAC/B,MAAI,SAAS,GAAG;AACd,WAAO,EAAE,SAAS,MAAM,MAAM,GAAG,KAAK,EAAE;AAAA,EAC1C;AACA,QAAM,MAAM,MAAM,QAAQ,GAAG;AAC7B,MAAI,MAAM,KAAK,MAAM,MAAM,SAAS,GAAG;AACrC,WAAO,EAAE,SAAS,MAAM,MAAM,GAAG,GAAG,GAAG,iBAAiB,MAAM;AAAA,EAChE;AACA,SAAO,EAAE,SAAS,MAAM;AAC1B;AAkBO,SAAS,iBAAiB,OAA4C;AAC3E,QAAM,aAAa,MAAM,eAAe,uBAAuB,MAAM,YAAY,IAAI;AAGrF,MAAI;AACJ,MAAI;AACJ,MAAI,MAAM,aAAa;AACrB,aAAS,MAAM;AACf,mBAAe;AAAA,EACjB,WAAW,MAAM,4BAA4B,MAAM,cAAc;AAC/D,aAAS,MAAM;AACf,mBAAe;AAAA,EACjB,WAAW,CAAC,MAAM,4BAA4B,MAAM,aAAa;AAC/D,aAAS,MAAM;AACf,mBAAe;AAAA,EACjB,WAAW,CAAC,MAAM,4BAA4B,YAAY,iBAAiB;AACzE,aAAS,WAAW;AACpB,mBAAe;AAAA,EACjB,OAAO;AACL,aAAS,oBAAoB,MAAM,MAAM;AACzC,mBAAe;AAAA,EACjB;AAGA,MAAI;AACJ,MAAI;AACJ,MAAI,MAAM,cAAc;AACtB,cAAU,MAAM;AAChB,oBAAgB;AAAA,EAClB,WAAW,MAAM,2BAA2B;AAC1C,QAAI,MAAM,eAAe;AACvB,gBAAU,MAAM;AAChB,sBAAgB;AAAA,IAClB;AAAA,EACF,WAAW,YAAY;AACrB,cAAU,WAAW;AACrB,oBAAgB;AAAA,EAClB,WAAW,MAAM,eAAe;AAC9B,cAAU,MAAM;AAChB,oBAAgB;AAAA,EAClB,WAAW,MAAM,cAAc;AAC7B,cAAU,MAAM;AAChB,oBAAgB;AAAA,EAClB;AAEA,MAAI,CAAC,SAAS;AAGZ,UAAM,MAAM,OAAO,QAAQ,GAAG;AAC9B,QAAI,MAAM,GAAG;AACX,gBAAU,OAAO,MAAM,GAAG,GAAG;AAC7B,sBAAgB;AAAA,IAClB,OAAO;AACL,gBAAU;AACV,sBAAgB;AAAA,IAClB;AAAA,EACF;AAEA,SAAO,EAAE,SAAS,QAAQ,eAA+B,aAAa;AACxE;AAOO,SAAS,kBACd,QACgD;AAChD,UAAQ,QAAQ;AAAA,IACd,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT;AACE,aAAO;AAAA,EACX;AACF;","names":[]}
@@ -76,12 +76,27 @@ function resolveHttpPdlss(input) {
76
76
  }
77
77
  return { purpose, action, purposeSource, actionSource };
78
78
  }
79
+ function wirePurposeSource(source) {
80
+ switch (source) {
81
+ case "route_config":
82
+ return "route_config";
83
+ case "header":
84
+ case "legacy_header":
85
+ case "query":
86
+ return "header";
87
+ case "custom_extractor":
88
+ return "body";
89
+ default:
90
+ return "default";
91
+ }
92
+ }
79
93
  export {
80
94
  DEFAULT_HTTP_ACTION,
81
95
  DEFAULT_HTTP_PURPOSE,
82
96
  HTTP_METHOD_ACTION_TABLE,
83
97
  actionForHttpMethod,
84
98
  normalizePurposeHeader,
85
- resolveHttpPdlss
99
+ resolveHttpPdlss,
100
+ wirePurposeSource
86
101
  };
87
102
  //# sourceMappingURL=http-pdlss.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/adapters/http-pdlss.ts"],"sourcesContent":["/**\n * Shared purpose/action resolver for the HTTP adapters (express + nextjs) —\n * the vocabulary-unification layer.\n *\n * The PDLSS two-axis contract: `purpose` = bare category noun (`shopping`,\n * `data`, custom nouns like `trading`); `action` = dotted verb\n * (`shopping.search`) or an enumerated transport token. Transport verbs\n * (GET/POST/...) NEVER travel as PDLSS actions — they map through the pinned\n * method table below.\n *\n * SELF-CONTAINED on purpose: this published package cannot depend on the\n * private @astrasyncai/pdlss-vocab workspace package, so the constants are\n * duplicated here and pinned strictly equal by a monorepo CI test\n * (vocab-equality.test.ts). Change them in BOTH places or that test fails.\n */\n\n/** Pinned method→action derivation table. */\nexport const HTTP_METHOD_ACTION_TABLE: Readonly<Record<string, string>> = {\n GET: 'data.read',\n HEAD: 'data.read',\n OPTIONS: 'data.read',\n POST: 'data.write',\n PUT: 'data.write',\n PATCH: 'data.write',\n DELETE: 'data.delete',\n};\n\n/** Unknown methods are treated as mutating until proven otherwise. */\nexport const DEFAULT_HTTP_ACTION = 'data.write';\n\n/** Generic category emitted when nothing supplies a domain purpose. */\nexport const DEFAULT_HTTP_PURPOSE = 'data';\n\nexport function actionForHttpMethod(method: string): string {\n return HTTP_METHOD_ACTION_TABLE[method.toUpperCase()] ?? DEFAULT_HTTP_ACTION;\n}\n\nexport type HttpPurposeSource =\n | 'route_config'\n | 'custom_extractor'\n | 'header'\n | 'legacy_header'\n | 'query'\n | 'action_derived'\n | 'transport_default';\n\nexport type HttpActionSource =\n | 'route_config'\n | 'custom_extractor'\n | 'header'\n | 'purpose_header_derived'\n | 'method_table';\n\nexport interface HttpPdlssInput {\n method: string;\n /** `X-Astra-Purpose` header value (agent's declaration). */\n astraPurpose?: string;\n /** `X-Astra-Action` header value (agent's declaration, symmetric with MCP). */\n astraAction?: string;\n /** Legacy `x-purpose` header — deprecated, removal scheduled for 4.0.0. */\n legacyPurpose?: string;\n /** Legacy `?purpose` query param (express only) — deprecated, removal 4.0.0. */\n queryPurpose?: string;\n /** Dashboard route config send-mapping (RouteAccessConfig.purpose/.action). */\n routePurpose?: string;\n routeAction?: string;\n /** Results of the merchant's extractPurpose/extractAction options. */\n customPurpose?: string;\n hasCustomPurposeExtractor?: boolean;\n customAction?: string;\n hasCustomActionExtractor?: boolean;\n}\n\nexport interface HttpPdlssResolution {\n purpose: string;\n action: string;\n purposeSource: HttpPurposeSource;\n actionSource: HttpActionSource;\n}\n\n/**\n * Normalize an `X-Astra-Purpose` header value.\n * - colon form `\"category:action\"` (legacy wire shape): take the prefix; the\n * remainder is free-text and exists in no vocabulary — discard it and let\n * the method table supply the action (matches pre-3.1.0 discard behavior).\n * - dotted form `\"shopping.purchase\"` (the shape older docs taught): prefix\n * becomes the purpose, the WHOLE token becomes an action candidate.\n * - bare token: purpose verbatim.\n */\nexport function normalizePurposeHeader(value: string): {\n purpose: string;\n actionCandidate?: string;\n} {\n const colon = value.indexOf(':');\n if (colon >= 0) {\n return { purpose: value.slice(0, colon) };\n }\n const dot = value.indexOf('.');\n if (dot > 0 && dot < value.length - 1) {\n return { purpose: value.slice(0, dot), actionCandidate: value };\n }\n return { purpose: value };\n}\n\n/**\n * Resolve the {purpose, action} pair for one HTTP request.\n *\n * Precedence (both axes):\n * action: route mapping → extractAction option → X-Astra-Action header →\n * candidate from a dotted X-Astra-Purpose → pinned method table\n * purpose: route mapping → extractPurpose option → X-Astra-Purpose →\n * legacy x-purpose / ?purpose → category prefix of the resolved\n * action → 'data'\n *\n * A configured custom extractor masks the header/legacy/query steps of its\n * axis (it replaces the default chain, as extractPurpose always has); when it\n * returns undefined the axis falls through to the table/prefix fallback.\n * The route mapping is merchant policy fetched from the dashboard — it\n * outranks agent-supplied headers the way MCP toolGates do.\n */\nexport function resolveHttpPdlss(input: HttpPdlssInput): HttpPdlssResolution {\n const fromHeader = input.astraPurpose ? normalizePurposeHeader(input.astraPurpose) : undefined;\n\n // ── Action axis ──\n let action: string;\n let actionSource: HttpActionSource;\n if (input.routeAction) {\n action = input.routeAction;\n actionSource = 'route_config';\n } else if (input.hasCustomActionExtractor && input.customAction) {\n action = input.customAction;\n actionSource = 'custom_extractor';\n } else if (!input.hasCustomActionExtractor && input.astraAction) {\n action = input.astraAction;\n actionSource = 'header';\n } else if (!input.hasCustomActionExtractor && fromHeader?.actionCandidate) {\n action = fromHeader.actionCandidate;\n actionSource = 'purpose_header_derived';\n } else {\n action = actionForHttpMethod(input.method);\n actionSource = 'method_table';\n }\n\n // ── Purpose axis ──\n let purpose: string | undefined;\n let purposeSource: HttpPurposeSource | undefined;\n if (input.routePurpose) {\n purpose = input.routePurpose;\n purposeSource = 'route_config';\n } else if (input.hasCustomPurposeExtractor) {\n if (input.customPurpose) {\n purpose = input.customPurpose;\n purposeSource = 'custom_extractor';\n }\n } else if (fromHeader) {\n purpose = fromHeader.purpose;\n purposeSource = 'header';\n } else if (input.legacyPurpose) {\n purpose = input.legacyPurpose;\n purposeSource = 'legacy_header';\n } else if (input.queryPurpose) {\n purpose = input.queryPurpose;\n purposeSource = 'query';\n }\n\n if (!purpose) {\n // Keep the pair coherent: an agent that sent only X-Astra-Action (or a\n // route that mapped only the action) gets that action's own category.\n const dot = action.indexOf('.');\n if (dot > 0) {\n purpose = action.slice(0, dot);\n purposeSource = 'action_derived';\n } else {\n purpose = DEFAULT_HTTP_PURPOSE;\n purposeSource = 'transport_default';\n }\n }\n\n return { purpose, action, purposeSource: purposeSource!, actionSource };\n}\n"],"mappings":";AAiBO,IAAM,2BAA6D;AAAA,EACxE,KAAK;AAAA,EACL,MAAM;AAAA,EACN,SAAS;AAAA,EACT,MAAM;AAAA,EACN,KAAK;AAAA,EACL,OAAO;AAAA,EACP,QAAQ;AACV;AAGO,IAAM,sBAAsB;AAG5B,IAAM,uBAAuB;AAE7B,SAAS,oBAAoB,QAAwB;AAC1D,SAAO,yBAAyB,OAAO,YAAY,CAAC,KAAK;AAC3D;AAsDO,SAAS,uBAAuB,OAGrC;AACA,QAAM,QAAQ,MAAM,QAAQ,GAAG;AAC/B,MAAI,SAAS,GAAG;AACd,WAAO,EAAE,SAAS,MAAM,MAAM,GAAG,KAAK,EAAE;AAAA,EAC1C;AACA,QAAM,MAAM,MAAM,QAAQ,GAAG;AAC7B,MAAI,MAAM,KAAK,MAAM,MAAM,SAAS,GAAG;AACrC,WAAO,EAAE,SAAS,MAAM,MAAM,GAAG,GAAG,GAAG,iBAAiB,MAAM;AAAA,EAChE;AACA,SAAO,EAAE,SAAS,MAAM;AAC1B;AAkBO,SAAS,iBAAiB,OAA4C;AAC3E,QAAM,aAAa,MAAM,eAAe,uBAAuB,MAAM,YAAY,IAAI;AAGrF,MAAI;AACJ,MAAI;AACJ,MAAI,MAAM,aAAa;AACrB,aAAS,MAAM;AACf,mBAAe;AAAA,EACjB,WAAW,MAAM,4BAA4B,MAAM,cAAc;AAC/D,aAAS,MAAM;AACf,mBAAe;AAAA,EACjB,WAAW,CAAC,MAAM,4BAA4B,MAAM,aAAa;AAC/D,aAAS,MAAM;AACf,mBAAe;AAAA,EACjB,WAAW,CAAC,MAAM,4BAA4B,YAAY,iBAAiB;AACzE,aAAS,WAAW;AACpB,mBAAe;AAAA,EACjB,OAAO;AACL,aAAS,oBAAoB,MAAM,MAAM;AACzC,mBAAe;AAAA,EACjB;AAGA,MAAI;AACJ,MAAI;AACJ,MAAI,MAAM,cAAc;AACtB,cAAU,MAAM;AAChB,oBAAgB;AAAA,EAClB,WAAW,MAAM,2BAA2B;AAC1C,QAAI,MAAM,eAAe;AACvB,gBAAU,MAAM;AAChB,sBAAgB;AAAA,IAClB;AAAA,EACF,WAAW,YAAY;AACrB,cAAU,WAAW;AACrB,oBAAgB;AAAA,EAClB,WAAW,MAAM,eAAe;AAC9B,cAAU,MAAM;AAChB,oBAAgB;AAAA,EAClB,WAAW,MAAM,cAAc;AAC7B,cAAU,MAAM;AAChB,oBAAgB;AAAA,EAClB;AAEA,MAAI,CAAC,SAAS;AAGZ,UAAM,MAAM,OAAO,QAAQ,GAAG;AAC9B,QAAI,MAAM,GAAG;AACX,gBAAU,OAAO,MAAM,GAAG,GAAG;AAC7B,sBAAgB;AAAA,IAClB,OAAO;AACL,gBAAU;AACV,sBAAgB;AAAA,IAClB;AAAA,EACF;AAEA,SAAO,EAAE,SAAS,QAAQ,eAA+B,aAAa;AACxE;","names":[]}
1
+ {"version":3,"sources":["../../src/adapters/http-pdlss.ts"],"sourcesContent":["/**\n * Shared purpose/action resolver for the HTTP adapters (express + nextjs) —\n * the vocabulary-unification layer.\n *\n * The PDLSS two-axis contract: `purpose` = bare category noun (`shopping`,\n * `data`, custom nouns like `trading`); `action` = dotted verb\n * (`shopping.search`) or an enumerated transport token. Transport verbs\n * (GET/POST/...) NEVER travel as PDLSS actions — they map through the pinned\n * method table below.\n *\n * SELF-CONTAINED on purpose: this published package cannot depend on the\n * private @astrasyncai/pdlss-vocab workspace package, so the constants are\n * duplicated here and pinned strictly equal by a monorepo CI test\n * (vocab-equality.test.ts). Change them in BOTH places or that test fails.\n */\n\n/** Pinned method→action derivation table. */\nexport const HTTP_METHOD_ACTION_TABLE: Readonly<Record<string, string>> = {\n GET: 'data.read',\n HEAD: 'data.read',\n OPTIONS: 'data.read',\n POST: 'data.write',\n PUT: 'data.write',\n PATCH: 'data.write',\n DELETE: 'data.delete',\n};\n\n/** Unknown methods are treated as mutating until proven otherwise. */\nexport const DEFAULT_HTTP_ACTION = 'data.write';\n\n/** Generic category emitted when nothing supplies a domain purpose. */\nexport const DEFAULT_HTTP_PURPOSE = 'data';\n\nexport function actionForHttpMethod(method: string): string {\n return HTTP_METHOD_ACTION_TABLE[method.toUpperCase()] ?? DEFAULT_HTTP_ACTION;\n}\n\nexport type HttpPurposeSource =\n | 'route_config'\n | 'custom_extractor'\n | 'header'\n | 'legacy_header'\n | 'query'\n | 'action_derived'\n | 'transport_default';\n\nexport type HttpActionSource =\n | 'route_config'\n | 'custom_extractor'\n | 'header'\n | 'purpose_header_derived'\n | 'method_table';\n\nexport interface HttpPdlssInput {\n method: string;\n /** `X-Astra-Purpose` header value (agent's declaration). */\n astraPurpose?: string;\n /** `X-Astra-Action` header value (agent's declaration, symmetric with MCP). */\n astraAction?: string;\n /** Legacy `x-purpose` header — deprecated, removal scheduled for 4.0.0. */\n legacyPurpose?: string;\n /** Legacy `?purpose` query param (express only) — deprecated, removal 4.0.0. */\n queryPurpose?: string;\n /** Dashboard route config send-mapping (RouteAccessConfig.purpose/.action). */\n routePurpose?: string;\n routeAction?: string;\n /** Results of the merchant's extractPurpose/extractAction options. */\n customPurpose?: string;\n hasCustomPurposeExtractor?: boolean;\n customAction?: string;\n hasCustomActionExtractor?: boolean;\n}\n\nexport interface HttpPdlssResolution {\n purpose: string;\n action: string;\n purposeSource: HttpPurposeSource;\n actionSource: HttpActionSource;\n}\n\n/**\n * Normalize an `X-Astra-Purpose` header value.\n * - colon form `\"category:action\"` (legacy wire shape): take the prefix; the\n * remainder is free-text and exists in no vocabulary — discard it and let\n * the method table supply the action (matches pre-3.1.0 discard behavior).\n * - dotted form `\"shopping.purchase\"` (the shape older docs taught): prefix\n * becomes the purpose, the WHOLE token becomes an action candidate.\n * - bare token: purpose verbatim.\n */\nexport function normalizePurposeHeader(value: string): {\n purpose: string;\n actionCandidate?: string;\n} {\n const colon = value.indexOf(':');\n if (colon >= 0) {\n return { purpose: value.slice(0, colon) };\n }\n const dot = value.indexOf('.');\n if (dot > 0 && dot < value.length - 1) {\n return { purpose: value.slice(0, dot), actionCandidate: value };\n }\n return { purpose: value };\n}\n\n/**\n * Resolve the {purpose, action} pair for one HTTP request.\n *\n * Precedence (both axes):\n * action: route mapping → extractAction option → X-Astra-Action header →\n * candidate from a dotted X-Astra-Purpose → pinned method table\n * purpose: route mapping → extractPurpose option → X-Astra-Purpose →\n * legacy x-purpose / ?purpose → category prefix of the resolved\n * action → 'data'\n *\n * A configured custom extractor masks the header/legacy/query steps of its\n * axis (it replaces the default chain, as extractPurpose always has); when it\n * returns undefined the axis falls through to the table/prefix fallback.\n * The route mapping is merchant policy fetched from the dashboard — it\n * outranks agent-supplied headers the way MCP toolGates do.\n */\nexport function resolveHttpPdlss(input: HttpPdlssInput): HttpPdlssResolution {\n const fromHeader = input.astraPurpose ? normalizePurposeHeader(input.astraPurpose) : undefined;\n\n // ── Action axis ──\n let action: string;\n let actionSource: HttpActionSource;\n if (input.routeAction) {\n action = input.routeAction;\n actionSource = 'route_config';\n } else if (input.hasCustomActionExtractor && input.customAction) {\n action = input.customAction;\n actionSource = 'custom_extractor';\n } else if (!input.hasCustomActionExtractor && input.astraAction) {\n action = input.astraAction;\n actionSource = 'header';\n } else if (!input.hasCustomActionExtractor && fromHeader?.actionCandidate) {\n action = fromHeader.actionCandidate;\n actionSource = 'purpose_header_derived';\n } else {\n action = actionForHttpMethod(input.method);\n actionSource = 'method_table';\n }\n\n // ── Purpose axis ──\n let purpose: string | undefined;\n let purposeSource: HttpPurposeSource | undefined;\n if (input.routePurpose) {\n purpose = input.routePurpose;\n purposeSource = 'route_config';\n } else if (input.hasCustomPurposeExtractor) {\n if (input.customPurpose) {\n purpose = input.customPurpose;\n purposeSource = 'custom_extractor';\n }\n } else if (fromHeader) {\n purpose = fromHeader.purpose;\n purposeSource = 'header';\n } else if (input.legacyPurpose) {\n purpose = input.legacyPurpose;\n purposeSource = 'legacy_header';\n } else if (input.queryPurpose) {\n purpose = input.queryPurpose;\n purposeSource = 'query';\n }\n\n if (!purpose) {\n // Keep the pair coherent: an agent that sent only X-Astra-Action (or a\n // route that mapped only the action) gets that action's own category.\n const dot = action.indexOf('.');\n if (dot > 0) {\n purpose = action.slice(0, dot);\n purposeSource = 'action_derived';\n } else {\n purpose = DEFAULT_HTTP_PURPOSE;\n purposeSource = 'transport_default';\n }\n }\n\n return { purpose, action, purposeSource: purposeSource!, actionSource };\n}\n\n/**\n * 5.19.0 — the coarse purpose source sent to verify-access as\n * `purposeSource`. Only `'route_config'` changes server behaviour (checked\n * against the endpoint's stored rules); the rest are attribution.\n */\nexport function wirePurposeSource(\n source: HttpPurposeSource\n): 'route_config' | 'header' | 'body' | 'default' {\n switch (source) {\n case 'route_config':\n return 'route_config';\n case 'header':\n case 'legacy_header':\n case 'query':\n return 'header';\n case 'custom_extractor':\n return 'body';\n default:\n return 'default';\n }\n}\n"],"mappings":";AAiBO,IAAM,2BAA6D;AAAA,EACxE,KAAK;AAAA,EACL,MAAM;AAAA,EACN,SAAS;AAAA,EACT,MAAM;AAAA,EACN,KAAK;AAAA,EACL,OAAO;AAAA,EACP,QAAQ;AACV;AAGO,IAAM,sBAAsB;AAG5B,IAAM,uBAAuB;AAE7B,SAAS,oBAAoB,QAAwB;AAC1D,SAAO,yBAAyB,OAAO,YAAY,CAAC,KAAK;AAC3D;AAsDO,SAAS,uBAAuB,OAGrC;AACA,QAAM,QAAQ,MAAM,QAAQ,GAAG;AAC/B,MAAI,SAAS,GAAG;AACd,WAAO,EAAE,SAAS,MAAM,MAAM,GAAG,KAAK,EAAE;AAAA,EAC1C;AACA,QAAM,MAAM,MAAM,QAAQ,GAAG;AAC7B,MAAI,MAAM,KAAK,MAAM,MAAM,SAAS,GAAG;AACrC,WAAO,EAAE,SAAS,MAAM,MAAM,GAAG,GAAG,GAAG,iBAAiB,MAAM;AAAA,EAChE;AACA,SAAO,EAAE,SAAS,MAAM;AAC1B;AAkBO,SAAS,iBAAiB,OAA4C;AAC3E,QAAM,aAAa,MAAM,eAAe,uBAAuB,MAAM,YAAY,IAAI;AAGrF,MAAI;AACJ,MAAI;AACJ,MAAI,MAAM,aAAa;AACrB,aAAS,MAAM;AACf,mBAAe;AAAA,EACjB,WAAW,MAAM,4BAA4B,MAAM,cAAc;AAC/D,aAAS,MAAM;AACf,mBAAe;AAAA,EACjB,WAAW,CAAC,MAAM,4BAA4B,MAAM,aAAa;AAC/D,aAAS,MAAM;AACf,mBAAe;AAAA,EACjB,WAAW,CAAC,MAAM,4BAA4B,YAAY,iBAAiB;AACzE,aAAS,WAAW;AACpB,mBAAe;AAAA,EACjB,OAAO;AACL,aAAS,oBAAoB,MAAM,MAAM;AACzC,mBAAe;AAAA,EACjB;AAGA,MAAI;AACJ,MAAI;AACJ,MAAI,MAAM,cAAc;AACtB,cAAU,MAAM;AAChB,oBAAgB;AAAA,EAClB,WAAW,MAAM,2BAA2B;AAC1C,QAAI,MAAM,eAAe;AACvB,gBAAU,MAAM;AAChB,sBAAgB;AAAA,IAClB;AAAA,EACF,WAAW,YAAY;AACrB,cAAU,WAAW;AACrB,oBAAgB;AAAA,EAClB,WAAW,MAAM,eAAe;AAC9B,cAAU,MAAM;AAChB,oBAAgB;AAAA,EAClB,WAAW,MAAM,cAAc;AAC7B,cAAU,MAAM;AAChB,oBAAgB;AAAA,EAClB;AAEA,MAAI,CAAC,SAAS;AAGZ,UAAM,MAAM,OAAO,QAAQ,GAAG;AAC9B,QAAI,MAAM,GAAG;AACX,gBAAU,OAAO,MAAM,GAAG,GAAG;AAC7B,sBAAgB;AAAA,IAClB,OAAO;AACL,gBAAU;AACV,sBAAgB;AAAA,IAClB;AAAA,EACF;AAEA,SAAO,EAAE,SAAS,QAAQ,eAA+B,aAAa;AACxE;AAOO,SAAS,kBACd,QACgD;AAChD,UAAQ,QAAQ;AAAA,IACd,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT;AACE,aAAO;AAAA,EACX;AACF;","names":[]}
@@ -7,6 +7,49 @@ interface KitRequestContext {
7
7
  */
8
8
  callerId?: string;
9
9
  }
10
+ /**
11
+ * 5.19.0: what an approval-event resume did. `terminal` outcomes are final
12
+ * for the event (the dispatcher answers 2xx and marks the event seen);
13
+ * non-terminal ones (`busy`, `still_held`) answer 5xx so the platform
14
+ * retries.
15
+ */
16
+ type ResumeOutcome = 'completed' | 'pending' | 'payment_failed' | 'cart_invalid' | 'not_authorized' | 'declined' | 'already_completed' | 'canceled' | 'not_held' | 'stale' | 'not_found' | 'busy' | 'still_held';
17
+ interface ResumeResult<S> {
18
+ outcome: ResumeOutcome;
19
+ terminal: boolean;
20
+ session: S | null;
21
+ }
22
+ /** 5.19.0: the approval event AstraSync POSTs to your endpoint's Events URL. */
23
+ interface ApprovalEvent {
24
+ /** Stable across retries — dedupe on it. */
25
+ eventId: string;
26
+ type: 'step_up.approved' | 'step_up.denied' | 'step_up.expired' | 'step_up.lapsed';
27
+ approvalId: string;
28
+ /** The cart id the confirm carried (the platform's, else the kit's session id). */
29
+ checkoutSessionId: string;
30
+ /** The kit session id the confirm carried in `metadata`. */
31
+ kitSessionId?: string;
32
+ protocol?: 'ucp' | 'acp';
33
+ counterpartyId: string;
34
+ agentAstraId: string;
35
+ transactionValue: number;
36
+ currency: string;
37
+ decidedAt: string;
38
+ /** Approved only: the approval can be redeemed until this instant (ISO). */
39
+ redeemBy?: string;
40
+ }
41
+ /** A kit the approval-event dispatcher can route to (both checkout kits). */
42
+ interface ApprovalEventKit {
43
+ readonly protocol: 'ucp' | 'acp';
44
+ /** Apply one verified event to the kit session it names. */
45
+ applyApprovalEvent(event: ApprovalEvent): Promise<ResumeResult<unknown>>;
46
+ }
47
+
48
+ /** Where the dispatcher remembers events it finished (dedupe). */
49
+ interface SeenEvents {
50
+ has(eventId: string): Promise<boolean>;
51
+ add(eventId: string): Promise<void>;
52
+ }
10
53
 
11
54
  /**
12
55
  * 5.18.0 — mount a fetch-style checkout kit (`createUcpCheckoutKit`,
@@ -24,11 +67,28 @@ interface KitRequestContext {
24
67
  * When the AstraSync middleware ran first, the caller's verified ASTRA-id
25
68
  * (`req.agentVerification.agent.astraId`) scopes sessions and idempotency
26
69
  * keys; override with `callerId`.
70
+ *
71
+ * 5.19.0:
72
+ * - the request URL the kit sees uses the kit's `publicOrigin` (UCP default:
73
+ * the origin of `profile.endpoint`), so RFC 9421 `@target-uri` /
74
+ * `@authority` and the forwarded agent artifacts match what the agent
75
+ * signed behind a TLS-terminating proxy. Without one (ACP default), the
76
+ * origin is `X-Forwarded-Proto` / `X-Forwarded-Host` when Express
77
+ * `trust proxy` is set, else `req.protocol` + Host.
78
+ * - `approvalEventsPath` + `webhookSecret` mount the owner-approval events
79
+ * receiver on the same handler (`handleApprovalEvents`); or mount
80
+ * `approvalEventsHandler({ secret, kits: [ucpKit, acpKit] })` on its own
81
+ * path to serve both kits from one Events URL.
27
82
  */
28
83
 
29
84
  /** Anything with a fetch-style `handle` (both checkout kits). */
30
85
  interface FetchKit {
31
86
  handle(req: Request, ctx?: KitRequestContext): Promise<Response>;
87
+ /** 5.19.0: the public origin to rebuild request URLs with (both kits expose it). */
88
+ readonly publicOrigin?: string;
89
+ /** 5.19.0: present on both kits — makes the kit an approval-event target. */
90
+ readonly protocol?: 'ucp' | 'acp';
91
+ applyApprovalEvent?: ApprovalEventKit['applyApprovalEvent'];
32
92
  }
33
93
  /** The parts of an Express request this adapter reads. */
34
94
  interface KitExpressRequest {
@@ -40,6 +100,10 @@ interface KitExpressRequest {
40
100
  body?: unknown;
41
101
  rawBody?: unknown;
42
102
  get?(name: string): string | undefined;
103
+ /** Express app (to read the `trust proxy` setting). */
104
+ app?: {
105
+ get(name: string): unknown;
106
+ };
43
107
  agentVerification?: {
44
108
  identityVerified?: boolean;
45
109
  agent?: {
@@ -56,8 +120,36 @@ interface KitExpressResponse {
56
120
  interface ExpressHandlerOptions {
57
121
  /** The caller's verified ASTRA-id (default: from `req.agentVerification` when identity-verified). */
58
122
  callerId?: (req: KitExpressRequest) => string | undefined;
123
+ /**
124
+ * 5.19.0: serve owner-approval events on this path (POST; matched against
125
+ * the full request path or the path under the mount point), e.g.
126
+ * `'/astrasync/events'`. Set the endpoint's Events URL to it. Needs
127
+ * `webhookSecret`.
128
+ */
129
+ approvalEventsPath?: string;
130
+ /** 5.19.0: the endpoint's webhook secret — or `[current, previous]` during a rotation. */
131
+ webhookSecret?: string | readonly string[];
132
+ /** 5.19.0: the kits events may name (default: this kit). */
133
+ approvalEventKits?: ApprovalEventKit[];
134
+ /** 5.19.0: dedupe store for event ids (default: process-local memory). */
135
+ seenEvents?: SeenEvents | ((eventId: string) => Promise<boolean>);
59
136
  }
137
+ interface ApprovalEventsHandlerOptions {
138
+ /** The endpoint's webhook secret — or `[current, previous]` during a rotation. */
139
+ secret: string | readonly string[];
140
+ /** The kits events may name, e.g. `[ucpKit, acpKit]`. */
141
+ kits: ApprovalEventKit[];
142
+ /** Dedupe store (default: process-local memory). */
143
+ seen?: SeenEvents | ((eventId: string) => Promise<boolean>);
144
+ }
145
+ /**
146
+ * 5.19.0: an Express handler for the endpoint's Events URL, serving both
147
+ * kits: `app.post('/astrasync/events', express.raw({ type: '*\/*' }),
148
+ * approvalEventsHandler({ secret, kits: [ucpKit, acpKit] }))`. Answers 2xx
149
+ * when done, 401 on a bad signature, 503 to have the platform retry.
150
+ */
151
+ declare function approvalEventsHandler(options: ApprovalEventsHandlerOptions): (req: KitExpressRequest, res: KitExpressResponse, next: (err?: unknown) => void) => Promise<void>;
60
152
  /** An Express handler that runs a checkout kit and writes its Response back. */
61
153
  declare function expressHandler(kit: FetchKit, options?: ExpressHandlerOptions): (req: KitExpressRequest, res: KitExpressResponse, next: (err?: unknown) => void) => Promise<void>;
62
154
 
63
- export { type ExpressHandlerOptions, type FetchKit, type KitExpressRequest, type KitExpressResponse, expressHandler };
155
+ export { type ApprovalEventsHandlerOptions, type ExpressHandlerOptions, type FetchKit, type KitExpressRequest, type KitExpressResponse, approvalEventsHandler, expressHandler };
@@ -7,6 +7,49 @@ interface KitRequestContext {
7
7
  */
8
8
  callerId?: string;
9
9
  }
10
+ /**
11
+ * 5.19.0: what an approval-event resume did. `terminal` outcomes are final
12
+ * for the event (the dispatcher answers 2xx and marks the event seen);
13
+ * non-terminal ones (`busy`, `still_held`) answer 5xx so the platform
14
+ * retries.
15
+ */
16
+ type ResumeOutcome = 'completed' | 'pending' | 'payment_failed' | 'cart_invalid' | 'not_authorized' | 'declined' | 'already_completed' | 'canceled' | 'not_held' | 'stale' | 'not_found' | 'busy' | 'still_held';
17
+ interface ResumeResult<S> {
18
+ outcome: ResumeOutcome;
19
+ terminal: boolean;
20
+ session: S | null;
21
+ }
22
+ /** 5.19.0: the approval event AstraSync POSTs to your endpoint's Events URL. */
23
+ interface ApprovalEvent {
24
+ /** Stable across retries — dedupe on it. */
25
+ eventId: string;
26
+ type: 'step_up.approved' | 'step_up.denied' | 'step_up.expired' | 'step_up.lapsed';
27
+ approvalId: string;
28
+ /** The cart id the confirm carried (the platform's, else the kit's session id). */
29
+ checkoutSessionId: string;
30
+ /** The kit session id the confirm carried in `metadata`. */
31
+ kitSessionId?: string;
32
+ protocol?: 'ucp' | 'acp';
33
+ counterpartyId: string;
34
+ agentAstraId: string;
35
+ transactionValue: number;
36
+ currency: string;
37
+ decidedAt: string;
38
+ /** Approved only: the approval can be redeemed until this instant (ISO). */
39
+ redeemBy?: string;
40
+ }
41
+ /** A kit the approval-event dispatcher can route to (both checkout kits). */
42
+ interface ApprovalEventKit {
43
+ readonly protocol: 'ucp' | 'acp';
44
+ /** Apply one verified event to the kit session it names. */
45
+ applyApprovalEvent(event: ApprovalEvent): Promise<ResumeResult<unknown>>;
46
+ }
47
+
48
+ /** Where the dispatcher remembers events it finished (dedupe). */
49
+ interface SeenEvents {
50
+ has(eventId: string): Promise<boolean>;
51
+ add(eventId: string): Promise<void>;
52
+ }
10
53
 
11
54
  /**
12
55
  * 5.18.0 — mount a fetch-style checkout kit (`createUcpCheckoutKit`,
@@ -24,11 +67,28 @@ interface KitRequestContext {
24
67
  * When the AstraSync middleware ran first, the caller's verified ASTRA-id
25
68
  * (`req.agentVerification.agent.astraId`) scopes sessions and idempotency
26
69
  * keys; override with `callerId`.
70
+ *
71
+ * 5.19.0:
72
+ * - the request URL the kit sees uses the kit's `publicOrigin` (UCP default:
73
+ * the origin of `profile.endpoint`), so RFC 9421 `@target-uri` /
74
+ * `@authority` and the forwarded agent artifacts match what the agent
75
+ * signed behind a TLS-terminating proxy. Without one (ACP default), the
76
+ * origin is `X-Forwarded-Proto` / `X-Forwarded-Host` when Express
77
+ * `trust proxy` is set, else `req.protocol` + Host.
78
+ * - `approvalEventsPath` + `webhookSecret` mount the owner-approval events
79
+ * receiver on the same handler (`handleApprovalEvents`); or mount
80
+ * `approvalEventsHandler({ secret, kits: [ucpKit, acpKit] })` on its own
81
+ * path to serve both kits from one Events URL.
27
82
  */
28
83
 
29
84
  /** Anything with a fetch-style `handle` (both checkout kits). */
30
85
  interface FetchKit {
31
86
  handle(req: Request, ctx?: KitRequestContext): Promise<Response>;
87
+ /** 5.19.0: the public origin to rebuild request URLs with (both kits expose it). */
88
+ readonly publicOrigin?: string;
89
+ /** 5.19.0: present on both kits — makes the kit an approval-event target. */
90
+ readonly protocol?: 'ucp' | 'acp';
91
+ applyApprovalEvent?: ApprovalEventKit['applyApprovalEvent'];
32
92
  }
33
93
  /** The parts of an Express request this adapter reads. */
34
94
  interface KitExpressRequest {
@@ -40,6 +100,10 @@ interface KitExpressRequest {
40
100
  body?: unknown;
41
101
  rawBody?: unknown;
42
102
  get?(name: string): string | undefined;
103
+ /** Express app (to read the `trust proxy` setting). */
104
+ app?: {
105
+ get(name: string): unknown;
106
+ };
43
107
  agentVerification?: {
44
108
  identityVerified?: boolean;
45
109
  agent?: {
@@ -56,8 +120,36 @@ interface KitExpressResponse {
56
120
  interface ExpressHandlerOptions {
57
121
  /** The caller's verified ASTRA-id (default: from `req.agentVerification` when identity-verified). */
58
122
  callerId?: (req: KitExpressRequest) => string | undefined;
123
+ /**
124
+ * 5.19.0: serve owner-approval events on this path (POST; matched against
125
+ * the full request path or the path under the mount point), e.g.
126
+ * `'/astrasync/events'`. Set the endpoint's Events URL to it. Needs
127
+ * `webhookSecret`.
128
+ */
129
+ approvalEventsPath?: string;
130
+ /** 5.19.0: the endpoint's webhook secret — or `[current, previous]` during a rotation. */
131
+ webhookSecret?: string | readonly string[];
132
+ /** 5.19.0: the kits events may name (default: this kit). */
133
+ approvalEventKits?: ApprovalEventKit[];
134
+ /** 5.19.0: dedupe store for event ids (default: process-local memory). */
135
+ seenEvents?: SeenEvents | ((eventId: string) => Promise<boolean>);
59
136
  }
137
+ interface ApprovalEventsHandlerOptions {
138
+ /** The endpoint's webhook secret — or `[current, previous]` during a rotation. */
139
+ secret: string | readonly string[];
140
+ /** The kits events may name, e.g. `[ucpKit, acpKit]`. */
141
+ kits: ApprovalEventKit[];
142
+ /** Dedupe store (default: process-local memory). */
143
+ seen?: SeenEvents | ((eventId: string) => Promise<boolean>);
144
+ }
145
+ /**
146
+ * 5.19.0: an Express handler for the endpoint's Events URL, serving both
147
+ * kits: `app.post('/astrasync/events', express.raw({ type: '*\/*' }),
148
+ * approvalEventsHandler({ secret, kits: [ucpKit, acpKit] }))`. Answers 2xx
149
+ * when done, 401 on a bad signature, 503 to have the platform retry.
150
+ */
151
+ declare function approvalEventsHandler(options: ApprovalEventsHandlerOptions): (req: KitExpressRequest, res: KitExpressResponse, next: (err?: unknown) => void) => Promise<void>;
60
152
  /** An Express handler that runs a checkout kit and writes its Response back. */
61
153
  declare function expressHandler(kit: FetchKit, options?: ExpressHandlerOptions): (req: KitExpressRequest, res: KitExpressResponse, next: (err?: unknown) => void) => Promise<void>;
62
154
 
63
- export { type ExpressHandlerOptions, type FetchKit, type KitExpressRequest, type KitExpressResponse, expressHandler };
155
+ export { type ApprovalEventsHandlerOptions, type ExpressHandlerOptions, type FetchKit, type KitExpressRequest, type KitExpressResponse, approvalEventsHandler, expressHandler };
@@ -20,10 +20,14 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/adapters/kit-express.ts
21
21
  var kit_express_exports = {};
22
22
  __export(kit_express_exports, {
23
+ approvalEventsHandler: () => approvalEventsHandler,
23
24
  expressHandler: () => expressHandler
24
25
  });
25
26
  module.exports = __toCommonJS(kit_express_exports);
26
27
 
28
+ // src/http-signatures/sign.ts
29
+ var import_http_message_signatures = require("http-message-signatures");
30
+
27
31
  // src/ucp/kit-shared.ts
28
32
  var warned = /* @__PURE__ */ new Set();
29
33
  function warnOnce(key, message) {
@@ -32,6 +36,145 @@ function warnOnce(key, message) {
32
36
  console.warn(message);
33
37
  }
34
38
 
39
+ // src/webhooks.ts
40
+ var import_crypto = require("crypto");
41
+ var DEFAULT_TOLERANCE_SECONDS = 300;
42
+ function parseSignatureHeader(header) {
43
+ let t;
44
+ const signatures = [];
45
+ for (const segment of header.split(",")) {
46
+ const eq = segment.indexOf("=");
47
+ if (eq < 0) continue;
48
+ const k = segment.slice(0, eq).trim();
49
+ const v = segment.slice(eq + 1).trim();
50
+ if (k === "t") t = v;
51
+ else if ((k === "v1" || k === "v1old") && v) signatures.push(v);
52
+ }
53
+ return { t, signatures };
54
+ }
55
+ function computeHmac(secret, signedPayload) {
56
+ return (0, import_crypto.createHmac)("sha256", secret).update(signedPayload, "utf8").digest("hex");
57
+ }
58
+ function constantTimeEquals(a, b) {
59
+ const aBuf = Buffer.from(a, "utf8");
60
+ const bBuf = Buffer.from(b, "utf8");
61
+ if (aBuf.length !== bBuf.length) return false;
62
+ return (0, import_crypto.timingSafeEqual)(aBuf, bBuf);
63
+ }
64
+ function verifyAstraSyncWebhook(rawBody, headers, secret, options = {}) {
65
+ const secrets = (Array.isArray(secret) ? secret : [secret]).filter(
66
+ (x) => typeof x === "string" && x.length > 0
67
+ );
68
+ if (!secrets.length) return { ok: false, reason: "no_secret_provided" };
69
+ const rawHeader = headers["x-astrasync-signature"] ?? headers["X-Astrasync-Signature"] ?? headers["X-AstraSync-Signature"] ?? headers["X-ASTRASYNC-SIGNATURE"];
70
+ if (!rawHeader) return { ok: false, reason: "missing_signature_header" };
71
+ const headerValue = Array.isArray(rawHeader) ? rawHeader[0] : rawHeader;
72
+ const { t: timestamp, signatures } = parseSignatureHeader(headerValue);
73
+ if (!timestamp || !signatures.length) {
74
+ return { ok: false, reason: "malformed_signature_header" };
75
+ }
76
+ const tolerance = options.toleranceSeconds ?? DEFAULT_TOLERANCE_SECONDS;
77
+ const now = options.nowMs ?? Date.now();
78
+ const tsSeconds = Number(timestamp);
79
+ if (!Number.isFinite(tsSeconds)) {
80
+ return { ok: false, reason: "invalid_timestamp" };
81
+ }
82
+ const ageSeconds = Math.abs(now / 1e3 - tsSeconds);
83
+ if (ageSeconds > tolerance) {
84
+ return { ok: false, reason: "timestamp_outside_tolerance" };
85
+ }
86
+ for (const key of secrets) {
87
+ const expectedSignature = computeHmac(key, `${timestamp}.${rawBody}`);
88
+ if (signatures.some((received) => constantTimeEquals(expectedSignature, received))) {
89
+ return { ok: true };
90
+ }
91
+ }
92
+ return { ok: false, reason: "signature_mismatch" };
93
+ }
94
+
95
+ // src/ucp/approval-events.ts
96
+ var MemorySeenEvents = class {
97
+ constructor(max = 1e4) {
98
+ this.max = max;
99
+ this.ids = /* @__PURE__ */ new Set();
100
+ }
101
+ async has(eventId) {
102
+ return this.ids.has(eventId);
103
+ }
104
+ async add(eventId) {
105
+ if (this.ids.size >= this.max) {
106
+ const first2 = this.ids.values().next().value;
107
+ if (first2 !== void 0) this.ids.delete(first2);
108
+ }
109
+ this.ids.add(eventId);
110
+ }
111
+ };
112
+ var defaultSeen = new MemorySeenEvents();
113
+ var TYPES = /* @__PURE__ */ new Set(["step_up.approved", "step_up.denied", "step_up.expired", "step_up.lapsed"]);
114
+ function toHeaderRecord(headers) {
115
+ if (typeof Headers !== "undefined" && headers instanceof Headers) {
116
+ const out2 = {};
117
+ headers.forEach((v, k) => out2[k.toLowerCase()] = v);
118
+ return out2;
119
+ }
120
+ const out = {};
121
+ for (const [k, v] of Object.entries(headers)) out[k.toLowerCase()] = v;
122
+ return out;
123
+ }
124
+ async function handleApprovalEvents(rawBody, headers, secret, opts) {
125
+ const raw = typeof rawBody === "string" ? rawBody : Buffer.from(rawBody).toString("utf8");
126
+ const sig = verifyAstraSyncWebhook(raw, toHeaderRecord(headers), secret, {
127
+ ...opts.toleranceSeconds !== void 0 && { toleranceSeconds: opts.toleranceSeconds },
128
+ ...opts.nowMs !== void 0 && { nowMs: opts.nowMs }
129
+ });
130
+ if (!sig.ok) return { status: 401, body: { ok: false, error: sig.reason } };
131
+ let event;
132
+ try {
133
+ event = JSON.parse(raw);
134
+ } catch {
135
+ return { status: 400, body: { ok: false, error: "invalid_json" } };
136
+ }
137
+ if (!event || typeof event.eventId !== "string" || typeof event.type !== "string") {
138
+ return { status: 400, body: { ok: false, error: "invalid_event" } };
139
+ }
140
+ if (!TYPES.has(event.type)) {
141
+ opts.onEvent?.(event, "ignored");
142
+ return { status: 200, body: { ok: true, eventId: event.eventId, outcome: "ignored" } };
143
+ }
144
+ const seen = opts.seen ?? defaultSeen;
145
+ const has = typeof seen === "function" ? seen : (id) => seen.has(id);
146
+ try {
147
+ if (await has(event.eventId)) {
148
+ opts.onEvent?.(event, "duplicate");
149
+ return { status: 200, body: { ok: true, eventId: event.eventId, outcome: "duplicate" } };
150
+ }
151
+ const candidates = event.protocol ? opts.kits.filter((k) => k.protocol === event.protocol) : opts.kits;
152
+ let last = "not_found";
153
+ let terminal = true;
154
+ for (const kit of candidates) {
155
+ const r = await kit.applyApprovalEvent(event);
156
+ last = r.outcome;
157
+ terminal = r.terminal;
158
+ if (r.outcome !== "not_found") break;
159
+ }
160
+ opts.onEvent?.(event, last);
161
+ if (!terminal) {
162
+ return { status: 503, body: { ok: false, eventId: event.eventId, outcome: last } };
163
+ }
164
+ if (typeof seen !== "function") await seen.add(event.eventId);
165
+ return { status: 200, body: { ok: true, eventId: event.eventId, outcome: last } };
166
+ } catch (err) {
167
+ return {
168
+ status: 503,
169
+ body: {
170
+ ok: false,
171
+ eventId: event.eventId,
172
+ error: err instanceof Error ? err.message : "transient_error"
173
+ }
174
+ };
175
+ }
176
+ }
177
+
35
178
  // src/adapters/kit-express.ts
36
179
  var bytes = (b) => b.buffer.slice(b.byteOffset, b.byteOffset + b.byteLength);
37
180
  function rawBodyOf(req) {
@@ -51,11 +194,46 @@ function rawBodyOf(req) {
51
194
  }
52
195
  return void 0;
53
196
  }
197
+ var first = (v) => (Array.isArray(v) ? v[0] : v)?.split(",")[0]?.trim() || void 0;
198
+ function originOf(req, publicOrigin) {
199
+ if (publicOrigin) return publicOrigin.replace(/\/+$/, "");
200
+ const trustProxy = Boolean(req.app?.get("trust proxy"));
201
+ const host = trustProxy && first(req.headers["x-forwarded-host"]) || req.get?.("host") || req.headers.host || "localhost";
202
+ const proto = trustProxy && first(req.headers["x-forwarded-proto"]) || req.protocol || "http";
203
+ return `${proto}://${host}`;
204
+ }
205
+ var isKit = (k) => typeof k.applyApprovalEvent === "function" && (k.protocol === "ucp" || k.protocol === "acp");
206
+ async function writeEvents(req, res, secret, kits, seen) {
207
+ const raw = rawBodyOf(req);
208
+ const text = raw === void 0 ? "" : typeof raw === "string" ? raw : Buffer.from(raw).toString("utf8");
209
+ const out = secret ? await handleApprovalEvents(text, req.headers, secret, { kits, ...seen && { seen } }) : { status: 500, body: { ok: false, error: "webhook_secret_not_configured" } };
210
+ res.status(out.status);
211
+ res.setHeader("content-type", "application/json");
212
+ res.end(JSON.stringify(out.body));
213
+ }
214
+ function approvalEventsHandler(options) {
215
+ return async (req, res, next) => {
216
+ try {
217
+ await writeEvents(req, res, options.secret, options.kits, options.seen);
218
+ } catch (err) {
219
+ next(err);
220
+ }
221
+ };
222
+ }
54
223
  function expressHandler(kit, options = {}) {
55
224
  return async (req, res, next) => {
56
225
  try {
57
- const host = req.get?.("host") ?? req.headers.host ?? "localhost";
58
- const url = `${req.protocol ?? "http"}://${host}${req.originalUrl ?? req.url}`;
226
+ if (options.approvalEventsPath && req.method === "POST") {
227
+ const p = options.approvalEventsPath.replace(/\/+$/, "");
228
+ const full = (req.originalUrl ?? req.url).split("?")[0].replace(/\/+$/, "");
229
+ const local = req.url.split("?")[0].replace(/\/+$/, "");
230
+ if (full === p || local === p) {
231
+ const kits = options.approvalEventKits ?? (isKit(kit) ? [kit] : []);
232
+ await writeEvents(req, res, options.webhookSecret, kits, options.seenEvents);
233
+ return;
234
+ }
235
+ }
236
+ const url = `${originOf(req, kit.publicOrigin)}${req.originalUrl ?? req.url}`;
59
237
  const headers = new Headers();
60
238
  for (const [k, v] of Object.entries(req.headers)) {
61
239
  if (v === void 0) continue;
@@ -83,6 +261,7 @@ function expressHandler(kit, options = {}) {
83
261
  }
84
262
  // Annotate the CommonJS export names for ESM import in node:
85
263
  0 && (module.exports = {
264
+ approvalEventsHandler,
86
265
  expressHandler
87
266
  });
88
267
  //# sourceMappingURL=kit-express.js.map