@open-mercato/core 0.10.1-develop.7707.1.61845a3ef7 → 0.10.1-develop.7709.1.046124599c

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.
@@ -10,6 +10,8 @@ import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
10
10
  import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
11
11
  import { CustomerInteraction } from "../../../data/entities.js";
12
12
  import { TERMINAL_INTERACTION_STATUS_LIST } from "../../../lib/interactionStatus.js";
13
+ import { applyEmailVisibilityFilter } from "../../../lib/visibilityFilter.js";
14
+ import { listGrantsForViewer, listSharedChannelIds } from "../../../lib/conversationShares.js";
13
15
  import { createLogger } from "@open-mercato/shared/lib/logger";
14
16
  const logger = createLogger("customers");
15
17
  const querySchema = z.object({
@@ -82,15 +84,27 @@ async function GET(req) {
82
84
  if (organizationIds?.length === 0) {
83
85
  return NextResponse.json({ ok: true, result: { hasConflicts: false, conflicts: [] } });
84
86
  }
85
- const checkUserId = query.userId ?? auth.userId;
87
+ const viewerUserId = auth.isApiKey ? null : auth.sub ?? null;
88
+ const checkUserId = query.userId ?? (auth.isApiKey ? auth.userId : viewerUserId);
86
89
  const em = container.resolve("em").fork();
87
90
  const kysely = em.getKysely();
91
+ const emailShareScope = { tenantId: auth.tenantId, organizationId: rbacOrganizationId };
92
+ const [sharedConversations, sharedChannelIds] = await Promise.all([
93
+ listGrantsForViewer(em, emailShareScope, viewerUserId),
94
+ listSharedChannelIds(em, emailShareScope, viewerUserId)
95
+ ]);
88
96
  let baseQuery = kysely.selectFrom("customer_interactions").select(["id", "scheduled_at", "duration_minutes", "interaction_type"]).where("tenant_id", "=", auth.tenantId).where("status", "not in", [...TERMINAL_INTERACTION_STATUS_LIST]).where("scheduled_at", "is not", null).where("deleted_at", "is", null);
89
97
  if (organizationIds?.length === 1) {
90
98
  baseQuery = baseQuery.where("organization_id", "=", organizationIds[0]);
91
99
  } else if (organizationIds && organizationIds.length > 1) {
92
100
  baseQuery = baseQuery.where("organization_id", "in", organizationIds);
93
101
  }
102
+ baseQuery = applyEmailVisibilityFilter(baseQuery, {
103
+ currentUserId: viewerUserId,
104
+ userFeatures: void 0,
105
+ sharedConversations,
106
+ sharedChannelIds
107
+ });
94
108
  if (checkUserId) {
95
109
  baseQuery = baseQuery.where(
96
110
  (eb) => eb.or([
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/customers/api/interactions/conflicts/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { sql } from 'kysely'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { resolveOrganizationScopeFilter } from '@open-mercato/core/modules/directory/utils/organizationScopeFilter'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { CustomerInteraction } from '../../../data/entities'\nimport { TERMINAL_INTERACTION_STATUS_LIST } from '../../../lib/interactionStatus'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('customers')\n\nconst querySchema = z.object({\n date: z.string().regex(/^\\d{4}-\\d{2}-\\d{2}$/),\n startTime: z.string().regex(/^\\d{2}:\\d{2}$/),\n duration: z.coerce.number().int().min(1).max(1440),\n excludeId: z.string().uuid().optional(),\n userId: z.string().uuid().optional(),\n timezoneOffsetMinutes: z.coerce.number().int().min(-900).max(900).optional(),\n})\n\nconst conflictItemSchema = z.object({\n id: z.string(),\n title: z.string().nullable(),\n startTime: z.string(),\n endTime: z.string(),\n type: z.string(),\n})\n\nconst responseSchema = z.object({\n ok: z.literal(true),\n result: z.object({\n hasConflicts: z.boolean(),\n conflicts: z.array(conflictItemSchema),\n }),\n})\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['customers.interactions.view'] },\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Customers',\n methods: {\n GET: {\n summary: 'Detect scheduling conflicts',\n description: 'Checks for overlapping planned interactions within the requested time window.',\n query: querySchema,\n responses: [\n {\n status: 200,\n description: 'Conflict detection result',\n schema: responseSchema,\n },\n ],\n },\n },\n}\n\nexport async function GET(req: Request) {\n const { translate } = await resolveTranslations()\n try {\n const queryUrl = new URL(req.url)\n const query = querySchema.parse(Object.fromEntries(queryUrl.searchParams))\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n\n if (!auth || !auth.tenantId) {\n throw new CrudHttpError(401, {\n error: translate('customers.errors.unauthorized', 'Unauthorized'),\n })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const { organizationIds, rbacOrganizationId } = resolveOrganizationScopeFilter(scope, auth)\n\n const offsetMinutes = query.timezoneOffsetMinutes ?? 0\n const offsetSign = offsetMinutes >= 0 ? '+' : '-'\n const absMinutes = Math.abs(offsetMinutes)\n const offsetHours = String(Math.floor(absMinutes / 60)).padStart(2, '0')\n const offsetMins = String(absMinutes % 60).padStart(2, '0')\n const offsetSuffix = `${offsetSign}${offsetHours}:${offsetMins}`\n const windowStart = new Date(`${query.date}T${query.startTime}:00${offsetSuffix}`)\n const windowEnd = new Date(windowStart.getTime() + query.duration * 60_000)\n\n if (Number.isNaN(windowStart.getTime()) || Number.isNaN(windowEnd.getTime())) {\n throw new CrudHttpError(400, { error: translate('customers.errors.invalid_date_time', 'Invalid date/time') })\n }\n if (organizationIds?.length === 0) {\n return NextResponse.json({ ok: true, result: { hasConflicts: false, conflicts: [] } })\n }\n\n const checkUserId = query.userId ?? auth.userId\n const em = (container.resolve('em') as EntityManager).fork()\n const kysely = em.getKysely<any>()\n\n let baseQuery = (kysely as any)\n .selectFrom('customer_interactions')\n .select(['id', 'scheduled_at', 'duration_minutes', 'interaction_type'])\n .where('tenant_id', '=', auth.tenantId)\n .where('status', 'not in', [...TERMINAL_INTERACTION_STATUS_LIST])\n .where('scheduled_at', 'is not', null)\n .where('deleted_at', 'is', null)\n\n if (organizationIds?.length === 1) {\n baseQuery = baseQuery.where('organization_id', '=', organizationIds[0])\n } else if (organizationIds && organizationIds.length > 1) {\n baseQuery = baseQuery.where('organization_id', 'in', organizationIds)\n }\n\n if (checkUserId) {\n baseQuery = baseQuery.where((eb: any) =>\n eb.or([\n eb('author_user_id', '=', checkUserId),\n eb('owner_user_id', '=', checkUserId),\n ])\n )\n }\n\n if (query.excludeId) {\n baseQuery = baseQuery.where('id', '!=', query.excludeId)\n }\n\n // Overlap condition: existing.start < windowEnd AND existing.end > windowStart\n // end = scheduled_at + duration_minutes (default 30 if null)\n baseQuery = baseQuery.where((eb: any) =>\n eb.and([\n eb('scheduled_at', '<', windowEnd.toISOString()),\n eb(sql`(scheduled_at + make_interval(mins => COALESCE(duration_minutes, 30)))`, '>', windowStart.toISOString()),\n ])\n )\n\n // Raw SELECT: reads only unencrypted columns (id, scheduled_at, duration_minutes, interaction_type); title is excluded to avoid ciphertext leakage and is resolved below via findWithDecryption.\n const rows = await baseQuery\n .orderBy('scheduled_at', 'asc')\n .limit(10)\n .execute() as Array<{\n id: string\n scheduled_at: string | Date\n duration_minutes: number | null\n interaction_type: string\n }>\n\n const decryptionScope = {\n tenantId: auth.tenantId ?? null,\n organizationId: rbacOrganizationId,\n }\n const conflictIds = rows.map((row) => row.id)\n const interactionFilter: Record<string, unknown> = {\n id: { $in: conflictIds },\n tenantId: auth.tenantId,\n deletedAt: null,\n }\n if (organizationIds?.length === 1) {\n interactionFilter.organizationId = organizationIds[0]\n } else if (organizationIds && organizationIds.length > 1) {\n interactionFilter.organizationId = { $in: organizationIds }\n }\n const decryptedInteractions = conflictIds.length > 0\n ? await findWithDecryption(\n em,\n CustomerInteraction,\n interactionFilter as any,\n undefined,\n decryptionScope,\n )\n : []\n const titleById = new Map<string, string | null>()\n for (const record of decryptedInteractions) {\n titleById.set((record as any).id, ((record as any).title ?? null) as string | null)\n }\n\n const conflicts = rows.map((row) => {\n const start = new Date(row.scheduled_at)\n const durationMin = row.duration_minutes ?? 30\n const end = new Date(start.getTime() + durationMin * 60_000)\n return {\n id: row.id,\n title: titleById.has(row.id) ? titleById.get(row.id) ?? null : null,\n startTime: start.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false }),\n endTime: end.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false }),\n type: row.interaction_type,\n }\n })\n\n return NextResponse.json({\n ok: true,\n result: { hasConflicts: conflicts.length > 0, conflicts },\n })\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n logger.error('GET failed', { component: 'interactions/conflicts', err })\n return NextResponse.json({ error: translate('customers.errors.internal', 'Internal server error') }, { status: 500 })\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { sql } from 'kysely'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { resolveOrganizationScopeFilter } from '@open-mercato/core/modules/directory/utils/organizationScopeFilter'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { CustomerInteraction } from '../../../data/entities'\nimport { TERMINAL_INTERACTION_STATUS_LIST } from '../../../lib/interactionStatus'\nimport { applyEmailVisibilityFilter } from '../../../lib/visibilityFilter'\nimport { listGrantsForViewer, listSharedChannelIds } from '../../../lib/conversationShares'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('customers')\n\nconst querySchema = z.object({\n date: z.string().regex(/^\\d{4}-\\d{2}-\\d{2}$/),\n startTime: z.string().regex(/^\\d{2}:\\d{2}$/),\n duration: z.coerce.number().int().min(1).max(1440),\n excludeId: z.string().uuid().optional(),\n userId: z.string().uuid().optional(),\n timezoneOffsetMinutes: z.coerce.number().int().min(-900).max(900).optional(),\n})\n\nconst conflictItemSchema = z.object({\n id: z.string(),\n title: z.string().nullable(),\n startTime: z.string(),\n endTime: z.string(),\n type: z.string(),\n})\n\nconst responseSchema = z.object({\n ok: z.literal(true),\n result: z.object({\n hasConflicts: z.boolean(),\n conflicts: z.array(conflictItemSchema),\n }),\n})\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['customers.interactions.view'] },\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Customers',\n methods: {\n GET: {\n summary: 'Detect scheduling conflicts',\n description: 'Checks for overlapping planned interactions within the requested time window.',\n query: querySchema,\n responses: [\n {\n status: 200,\n description: 'Conflict detection result',\n schema: responseSchema,\n },\n ],\n },\n },\n}\n\nexport async function GET(req: Request) {\n const { translate } = await resolveTranslations()\n try {\n const queryUrl = new URL(req.url)\n const query = querySchema.parse(Object.fromEntries(queryUrl.searchParams))\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n\n if (!auth || !auth.tenantId) {\n throw new CrudHttpError(401, {\n error: translate('customers.errors.unauthorized', 'Unauthorized'),\n })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const { organizationIds, rbacOrganizationId } = resolveOrganizationScopeFilter(scope, auth)\n\n const offsetMinutes = query.timezoneOffsetMinutes ?? 0\n const offsetSign = offsetMinutes >= 0 ? '+' : '-'\n const absMinutes = Math.abs(offsetMinutes)\n const offsetHours = String(Math.floor(absMinutes / 60)).padStart(2, '0')\n const offsetMins = String(absMinutes % 60).padStart(2, '0')\n const offsetSuffix = `${offsetSign}${offsetHours}:${offsetMins}`\n const windowStart = new Date(`${query.date}T${query.startTime}:00${offsetSuffix}`)\n const windowEnd = new Date(windowStart.getTime() + query.duration * 60_000)\n\n if (Number.isNaN(windowStart.getTime()) || Number.isNaN(windowEnd.getTime())) {\n throw new CrudHttpError(400, { error: translate('customers.errors.invalid_date_time', 'Invalid date/time') })\n }\n if (organizationIds?.length === 0) {\n return NextResponse.json({ ok: true, result: { hasConflicts: false, conflicts: [] } })\n }\n\n const viewerUserId = auth.isApiKey ? null : (auth.sub ?? null)\n const checkUserId = query.userId ?? (auth.isApiKey ? auth.userId : viewerUserId)\n const em = (container.resolve('em') as EntityManager).fork()\n const kysely = em.getKysely<any>()\n const emailShareScope = { tenantId: auth.tenantId, organizationId: rbacOrganizationId }\n const [sharedConversations, sharedChannelIds] = await Promise.all([\n listGrantsForViewer(em, emailShareScope, viewerUserId),\n listSharedChannelIds(em, emailShareScope, viewerUserId),\n ])\n\n let baseQuery = (kysely as any)\n .selectFrom('customer_interactions')\n .select(['id', 'scheduled_at', 'duration_minutes', 'interaction_type'])\n .where('tenant_id', '=', auth.tenantId)\n .where('status', 'not in', [...TERMINAL_INTERACTION_STATUS_LIST])\n .where('scheduled_at', 'is not', null)\n .where('deleted_at', 'is', null)\n\n if (organizationIds?.length === 1) {\n baseQuery = baseQuery.where('organization_id', '=', organizationIds[0])\n } else if (organizationIds && organizationIds.length > 1) {\n baseQuery = baseQuery.where('organization_id', 'in', organizationIds)\n }\n\n baseQuery = applyEmailVisibilityFilter(baseQuery, {\n currentUserId: viewerUserId,\n userFeatures: undefined,\n sharedConversations,\n sharedChannelIds,\n })\n\n if (checkUserId) {\n baseQuery = baseQuery.where((eb: any) =>\n eb.or([\n eb('author_user_id', '=', checkUserId),\n eb('owner_user_id', '=', checkUserId),\n ])\n )\n }\n\n if (query.excludeId) {\n baseQuery = baseQuery.where('id', '!=', query.excludeId)\n }\n\n // Overlap condition: existing.start < windowEnd AND existing.end > windowStart\n // end = scheduled_at + duration_minutes (default 30 if null)\n baseQuery = baseQuery.where((eb: any) =>\n eb.and([\n eb('scheduled_at', '<', windowEnd.toISOString()),\n eb(sql`(scheduled_at + make_interval(mins => COALESCE(duration_minutes, 30)))`, '>', windowStart.toISOString()),\n ])\n )\n\n // Raw SELECT: reads only unencrypted columns (id, scheduled_at, duration_minutes, interaction_type); title is excluded to avoid ciphertext leakage and is resolved below via findWithDecryption.\n const rows = await baseQuery\n .orderBy('scheduled_at', 'asc')\n .limit(10)\n .execute() as Array<{\n id: string\n scheduled_at: string | Date\n duration_minutes: number | null\n interaction_type: string\n }>\n\n const decryptionScope = {\n tenantId: auth.tenantId ?? null,\n organizationId: rbacOrganizationId,\n }\n const conflictIds = rows.map((row) => row.id)\n const interactionFilter: Record<string, unknown> = {\n id: { $in: conflictIds },\n tenantId: auth.tenantId,\n deletedAt: null,\n }\n if (organizationIds?.length === 1) {\n interactionFilter.organizationId = organizationIds[0]\n } else if (organizationIds && organizationIds.length > 1) {\n interactionFilter.organizationId = { $in: organizationIds }\n }\n const decryptedInteractions = conflictIds.length > 0\n ? await findWithDecryption(\n em,\n CustomerInteraction,\n interactionFilter as any,\n undefined,\n decryptionScope,\n )\n : []\n const titleById = new Map<string, string | null>()\n for (const record of decryptedInteractions) {\n titleById.set((record as any).id, ((record as any).title ?? null) as string | null)\n }\n\n const conflicts = rows.map((row) => {\n const start = new Date(row.scheduled_at)\n const durationMin = row.duration_minutes ?? 30\n const end = new Date(start.getTime() + durationMin * 60_000)\n return {\n id: row.id,\n title: titleById.has(row.id) ? titleById.get(row.id) ?? null : null,\n startTime: start.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false }),\n endTime: end.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false }),\n type: row.interaction_type,\n }\n })\n\n return NextResponse.json({\n ok: true,\n result: { hasConflicts: conflicts.length > 0, conflicts },\n })\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n logger.error('GET failed', { component: 'interactions/conflicts', err })\n return NextResponse.json({ error: translate('customers.errors.internal', 'Internal server error') }, { status: 500 })\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -170,7 +170,7 @@ async function enqueueActivity(em, activity, context) {
170
170
  workflowContext,
171
171
  stepContext,
172
172
  retryPolicy: activity.retryPolicy,
173
- timeoutMs: activity.timeoutMs,
173
+ timeoutMs: resolveActivityTimeoutMs(activity),
174
174
  tenantId: workflowInstance.tenantId,
175
175
  organizationId: workflowInstance.organizationId,
176
176
  userId: context.userId
@@ -458,7 +458,8 @@ async function executeActivityByType(em, container, activity, context, signal) {
458
458
  return await entry.execute(interpolatedConfig, context, {
459
459
  em,
460
460
  container,
461
- signal
461
+ signal,
462
+ timeoutMs: resolveActivityTimeoutMs(activity)
462
463
  });
463
464
  }
464
465
  async function executeSendEmail(config, context, container, signal) {
@@ -669,6 +670,34 @@ async function resolveDictionaryEntryId(em, dictionaryKey, value, tenantId, orga
669
670
  return null;
670
671
  }
671
672
  }
673
+ const WEBHOOK_TIMEOUT_MS = 3e4;
674
+ const WEBHOOK_MAX_RESPONSE_BYTES = 1024 * 1024;
675
+ async function readWebhookResponse(response, signal) {
676
+ const declaredLength = Number(response.headers.get("content-length"));
677
+ if (declaredLength > WEBHOOK_MAX_RESPONSE_BYTES) {
678
+ throw new Error(`[internal] CALL_WEBHOOK response exceeds ${WEBHOOK_MAX_RESPONSE_BYTES} bytes`);
679
+ }
680
+ if (!response.body) return "";
681
+ const reader = response.body.getReader();
682
+ const chunks = [];
683
+ let totalBytes = 0;
684
+ try {
685
+ for (; ; ) {
686
+ throwIfAborted(signal);
687
+ const { done, value } = await raceAbortable(reader.read(), signal);
688
+ if (done) break;
689
+ totalBytes += value.byteLength;
690
+ if (totalBytes > WEBHOOK_MAX_RESPONSE_BYTES) {
691
+ throw new Error(`[internal] CALL_WEBHOOK response exceeds ${WEBHOOK_MAX_RESPONSE_BYTES} bytes`);
692
+ }
693
+ chunks.push(value);
694
+ }
695
+ return new TextDecoder().decode(Buffer.concat(chunks, totalBytes));
696
+ } finally {
697
+ void reader.cancel().catch(() => void 0);
698
+ reader.releaseLock();
699
+ }
700
+ }
672
701
  async function executeCallWebhook(config, context, deps = {}) {
673
702
  const parsed = callWebhookConfigSchema.safeParse(config);
674
703
  if (!parsed.success) {
@@ -678,58 +707,56 @@ async function executeCallWebhook(config, context, deps = {}) {
678
707
  const { url, method, headers: rawHeaders, body } = parsed.data;
679
708
  const headers = rawHeaders ?? {};
680
709
  const allowPrivate = deps.allowPrivate ?? isAllowPrivateWorkflowWebhookUrlsEnabled();
681
- let response;
682
- try {
683
- response = await safeOutboundFetch(
684
- url,
685
- {
686
- method,
687
- headers: {
688
- "Content-Type": "application/json",
689
- ...headers
710
+ return executeWithTimeout(async (deadlineSignal) => {
711
+ const signal = deps.signal ? AbortSignal.any([deadlineSignal, deps.signal]) : deadlineSignal;
712
+ throwIfAborted(signal);
713
+ let response;
714
+ try {
715
+ response = await raceAbortable(safeOutboundFetch(
716
+ url,
717
+ {
718
+ method,
719
+ headers: {
720
+ "Content-Type": "application/json",
721
+ ...headers
722
+ },
723
+ body: body !== void 0 && body !== null ? JSON.stringify(body) : void 0,
724
+ redirect: "manual",
725
+ signal
690
726
  },
691
- body: body !== void 0 && body !== null ? JSON.stringify(body) : void 0,
692
- redirect: "manual",
693
- signal: deps.signal
694
- },
695
- {
696
- subject: "Workflow webhook URL",
697
- allowPrivate,
698
- lookupHost: deps.lookupHost,
699
- fetchImpl: deps.fetchImpl
727
+ {
728
+ subject: "Workflow webhook URL",
729
+ allowPrivate,
730
+ lookupHost: deps.lookupHost,
731
+ fetchImpl: deps.fetchImpl
732
+ }
733
+ ), signal);
734
+ if (response.status >= 300 && response.status < 400) {
735
+ const location = response.headers.get("location");
736
+ throw new Error(
737
+ `CALL_WEBHOOK refused to follow redirect ${response.status} to ${location ?? "(no Location header)"}`
738
+ );
700
739
  }
701
- );
702
- } catch (error) {
703
- if (error instanceof UnsafeOutboundUrlError) {
704
- throw new Error(
705
- `CALL_WEBHOOK rejected unsafe URL (reason=${error.reason}): ${error.message}`
706
- );
740
+ const text = await readWebhookResponse(response, signal);
741
+ const contentType = response.headers.get("content-type");
742
+ const result = contentType?.includes("application/json") ? JSON.parse(text) : text;
743
+ if (!response.ok) {
744
+ throw new Error(
745
+ `Webhook request failed with status ${response.status}: ${JSON.stringify(result)}`
746
+ );
747
+ }
748
+ return { status: response.status, statusText: response.statusText, result };
749
+ } catch (error) {
750
+ if (error instanceof UnsafeOutboundUrlError) {
751
+ throw new Error(
752
+ `CALL_WEBHOOK rejected unsafe URL (reason=${error.reason}): ${error.message}`
753
+ );
754
+ }
755
+ throw error;
756
+ } finally {
757
+ void response?.body?.cancel().catch(() => void 0);
707
758
  }
708
- throw error;
709
- }
710
- if (response.status >= 300 && response.status < 400) {
711
- const location = response.headers.get("location");
712
- throw new Error(
713
- `CALL_WEBHOOK refused to follow redirect ${response.status} to ${location ?? "(no Location header)"}`
714
- );
715
- }
716
- let result;
717
- const contentType = response.headers.get("content-type");
718
- if (contentType && contentType.includes("application/json")) {
719
- result = await response.json();
720
- } else {
721
- result = await response.text();
722
- }
723
- if (!response.ok) {
724
- throw new Error(
725
- `Webhook request failed with status ${response.status}: ${JSON.stringify(result)}`
726
- );
727
- }
728
- return {
729
- status: response.status,
730
- statusText: response.statusText,
731
- result
732
- };
759
+ }, deps.timeoutMs && deps.timeoutMs > 0 ? deps.timeoutMs : WEBHOOK_TIMEOUT_MS);
733
760
  }
734
761
  async function executeFunction(config, context, container, signal) {
735
762
  const { functionName, args = {} } = config;