(opts: {
toolName: string;
args: Record<string, unknown>;
response: Record<string, unknown>;
requestIdempotencyKey?: string;
/** Caller-uniqueness key for webhook idempotency. Pass the same value the
* request-side idempotency store uses for this caller (legacy dispatch
* passes `scopedPrincipal(auth, accountScope)`; the framework path passes
* `auth` directly except for `static:public` where it scopes by account).
* Two distinct callers MUST produce distinct strings here, otherwise
* receivers that dedupe across tenants on `idempotency_key` may drop one
* caller's webhook as a duplicate of another's. Empty strings are rejected
* fail-fast — they would silently degrade scoping to "no partitioning". */
principal: string;
})
| 159 | * adapter (`framework-server.ts`) so both paths emit byte-identical envelopes. |
| 160 | */ |
| 161 | export function maybeEmitCompletionWebhook(opts: { |
| 162 | toolName: string; |
| 163 | args: Record<string, unknown>; |
| 164 | response: Record<string, unknown>; |
| 165 | requestIdempotencyKey?: string; |
| 166 | /** Caller-uniqueness key for webhook idempotency. Pass the same value the |
| 167 | * request-side idempotency store uses for this caller (legacy dispatch |
| 168 | * passes `scopedPrincipal(auth, accountScope)`; the framework path passes |
| 169 | * `auth` directly except for `static:public` where it scopes by account). |
| 170 | * Two distinct callers MUST produce distinct strings here, otherwise |
| 171 | * receivers that dedupe across tenants on `idempotency_key` may drop one |
| 172 | * caller's webhook as a duplicate of another's. Empty strings are rejected |
| 173 | * fail-fast — they would silently degrade scoping to "no partitioning". */ |
| 174 | principal: string; |
| 175 | }): void { |
| 176 | if (!opts.principal) { |
| 177 | throw new Error('maybeEmitCompletionWebhook: principal must be a non-empty string (callers must pass the same caller-uniqueness key used for the request-side idempotency cache)'); |
| 178 | } |
| 179 | const webhookUrl = extractWebhookUrl(opts.args); |
| 180 | if (!webhookUrl || !(opts.toolName in TOOL_TO_TASK_TYPE)) return; |
| 181 | const tool = opts.toolName as WebhookEmittingTool; |
| 182 | |
| 183 | const emitter = getWebhookEmitter(); |
| 184 | const registeredOperationId = extractRegisteredOperationId(opts.args); |
| 185 | const deliveryOperationId = registeredOperationId |
| 186 | ? deriveRegisteredWebhookDeliveryOperationId(opts.toolName, registeredOperationId, opts.principal) |
| 187 | : deriveWebhookOperationId(opts.toolName, opts.response, opts.requestIdempotencyKey, opts.principal); |
| 188 | const payloadOperationId = registeredOperationId ?? deliveryOperationId; |
| 189 | const webhookTaskId = (opts.response.task_id as string | undefined) |
| 190 | ?? `tsk_${payloadOperationId.slice(0, 32).replace(/[^A-Za-z0-9_.:-]/g, '_')}`; |
| 191 | const payload: Record<string, unknown> = { |
| 192 | operation_id: payloadOperationId, |
| 193 | task_id: webhookTaskId, |
| 194 | task_type: TOOL_TO_TASK_TYPE[tool], |
| 195 | protocol: TOOL_TO_PROTOCOL[tool], |
| 196 | status: 'completed', |
| 197 | timestamp: new Date().toISOString(), |
| 198 | result: opts.response, |
| 199 | }; |
| 200 | void emitter.emit({ url: webhookUrl, payload, operation_id: deliveryOperationId }) |
| 201 | .catch(err => logger.warn({ err, tool: opts.toolName, url: webhookUrl }, 'Webhook emission failed')); |
| 202 | } |
| 203 | |
| 204 | const ENV_KEY = 'WEBHOOK_SIGNING_KEY_JWK'; |
| 205 | const KMS_WEBHOOK_ENV = 'GCP_KMS_WEBHOOK_KEY_VERSION'; |
no test coverage detected