@niadra/sdk 0.9.1 → 0.10.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +69 -0
- package/dist/ai-sdk.d.cts +3 -3
- package/dist/ai-sdk.d.ts +3 -3
- package/dist/anthropic.d.cts +4 -4
- package/dist/anthropic.d.ts +4 -4
- package/dist/bedrock.d.cts +4 -4
- package/dist/bedrock.d.ts +4 -4
- package/dist/cli.js +331 -96
- package/dist/cli.js.map +1 -1
- package/dist/{client-CWMxmJHs.d.cts → client-7mkrdRI0.d.cts} +192 -7
- package/dist/{client-CWMxmJHs.d.ts → client-7mkrdRI0.d.ts} +192 -7
- package/dist/cloudflare-agents.d.cts +3 -3
- package/dist/cloudflare-agents.d.ts +3 -3
- package/dist/elevenlabs.cjs +7 -5
- package/dist/elevenlabs.cjs.map +1 -1
- package/dist/elevenlabs.d.cts +5 -5
- package/dist/elevenlabs.d.ts +5 -5
- package/dist/elevenlabs.js +7 -5
- package/dist/elevenlabs.js.map +1 -1
- package/dist/genkit.d.cts +3 -3
- package/dist/genkit.d.ts +3 -3
- package/dist/google-adk.cjs +7 -5
- package/dist/google-adk.cjs.map +1 -1
- package/dist/google-adk.d.cts +3 -3
- package/dist/google-adk.d.ts +3 -3
- package/dist/google-adk.js +7 -5
- package/dist/google-adk.js.map +1 -1
- package/dist/google-genai.d.cts +4 -4
- package/dist/google-genai.d.ts +4 -4
- package/dist/index.cjs +341 -98
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -5
- package/dist/index.d.ts +13 -5
- package/dist/index.js +341 -98
- package/dist/index.js.map +1 -1
- package/dist/{intercept-CGngQI8B.d.cts → intercept-CGQhwAom.d.cts} +1 -1
- package/dist/{intercept-Bv0nLpSw.d.ts → intercept-CT0vglsd.d.ts} +1 -1
- package/dist/langchain.cjs.map +1 -1
- package/dist/langchain.d.cts +3 -3
- package/dist/langchain.d.ts +3 -3
- package/dist/langchain.js.map +1 -1
- package/dist/livekit.cjs +10 -2
- package/dist/livekit.cjs.map +1 -1
- package/dist/livekit.d.cts +3 -3
- package/dist/livekit.d.ts +3 -3
- package/dist/livekit.js +10 -2
- package/dist/livekit.js.map +1 -1
- package/dist/llamaindex.d.cts +3 -3
- package/dist/llamaindex.d.ts +3 -3
- package/dist/mastra.d.cts +3 -3
- package/dist/mastra.d.ts +3 -3
- package/dist/openai-agents.d.cts +3 -3
- package/dist/openai-agents.d.ts +3 -3
- package/dist/retell.cjs +7 -5
- package/dist/retell.cjs.map +1 -1
- package/dist/retell.d.cts +5 -5
- package/dist/retell.d.ts +5 -5
- package/dist/retell.js +7 -5
- package/dist/retell.js.map +1 -1
- package/dist/{shared-B4Ef2Wpg.d.ts → shared-Bf84jr3Y.d.ts} +1 -1
- package/dist/{shared-Cq8mz-7m.d.cts → shared-CxAwOcZI.d.cts} +1 -1
- package/dist/strands.d.cts +3 -3
- package/dist/strands.d.ts +3 -3
- package/dist/twilio.cjs +10 -2
- package/dist/twilio.cjs.map +1 -1
- package/dist/twilio.d.cts +4 -4
- package/dist/twilio.d.ts +4 -4
- package/dist/twilio.js +10 -2
- package/dist/twilio.js.map +1 -1
- package/dist/vapi.cjs +7 -5
- package/dist/vapi.cjs.map +1 -1
- package/dist/vapi.d.cts +5 -5
- package/dist/vapi.d.ts +5 -5
- package/dist/vapi.js +7 -5
- package/dist/vapi.js.map +1 -1
- package/dist/voltagent.d.cts +3 -3
- package/dist/voltagent.d.ts +3 -3
- package/dist/{webhook-jTx_SegV.d.cts → webhook-DE3HsGkb.d.cts} +1 -1
- package/dist/{webhook-z2TjUzjv.d.ts → webhook-QxYgxE6_.d.ts} +1 -1
- package/dist/whatsapp.cjs +10 -2
- package/dist/whatsapp.cjs.map +1 -1
- package/dist/whatsapp.d.cts +2 -2
- package/dist/whatsapp.d.ts +2 -2
- package/dist/whatsapp.js +10 -2
- package/dist/whatsapp.js.map +1 -1
- package/package.json +1 -1
package/dist/whatsapp.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/handles.ts","../src/integrations/shared.ts","../src/integrations/webhook.ts","../src/integrations/whatsapp.ts"],"names":[],"mappings":"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","file":"whatsapp.cjs","sourcesContent":["import { NiadraValidationError } from \"./errors.js\";\nimport type { Handle, ObjectRef } from \"./types/common.js\";\nimport type { SubjectKind } from \"./types/vocabulary.js\";\n\ninterface HandleOptions {\n /** Overrides the subject kind the server would infer from the handle type. */\n subjectKind?: SubjectKind;\n}\n\nfunction build(type: Handle[\"type\"], value: string, scope?: string, options?: HandleOptions): Handle {\n const handle: Handle = { type, value };\n if (scope !== undefined) handle.scope = scope;\n if (options?.subjectKind !== undefined) handle.subject_kind = options.subjectKind;\n return handle;\n}\n\n/**\n * Builders for the handle types the API accepts. They only assemble the object; the value is\n * normalized and validated on the server, which is the single source of truth for matching.\n *\n * @example\n * niadra.context({ subject: handles.phone(\"+5511987654321\"), conversation_id: \"wa-8812\" })\n */\nexport const handles = {\n /** A phone number in E.164 form, such as `+5511987654321`. */\n phone: (e164: string, options?: HandleOptions): Handle => build(\"phone_e164\", e164, undefined, options),\n email: (address: string, options?: HandleOptions): Handle => build(\"email\", address, undefined, options),\n /** The WhatsApp id (`wa_id`) the Cloud API reports for a contact. */\n waId: (value: string, options?: HandleOptions): Handle => build(\"wa_id\", value, undefined, options),\n waJid: (value: string, options?: HandleOptions): Handle => build(\"wa_jid\", value, undefined, options),\n waLid: (value: string, options?: HandleOptions): Handle => build(\"wa_lid\", value, undefined, options),\n /** A business-scoped WhatsApp user id; `businessAccount` is the WhatsApp Business account it belongs to. */\n waBsuid: (value: string, businessAccount: string, options?: HandleOptions): Handle =>\n build(\"wa_bsuid\", value, businessAccount, options),\n /** The user id in your own app or site. */\n appUserId: (value: string, options?: HandleOptions): Handle => build(\"app_user_id\", value, undefined, options),\n /** The id of a person or organization in a system of record; `system` names that system, such as `crm`. */\n systemId: (value: string, system: string, options?: HandleOptions): Handle =>\n build(\"system_id\", value, system, options),\n /** An HMAC of a national document number; `country` is its ISO 3166-1 alpha-2 code. */\n govIdHmac: (value: string, country: string, options?: HandleOptions): Handle =>\n build(\"gov_id_hmac\", value, country, options),\n /** An HMAC of a company registry number. Identifies an organization. */\n orgRegistryHmac: (value: string, country: string): Handle =>\n build(\"org_registry_hmac\", value, country, { subjectKind: \"account\" }),\n /** An e-mail domain, such as `acme.com`. Identifies an organization. */\n emailDomain: (domain: string): Handle => build(\"email_domain\", domain, undefined, { subjectKind: \"account\" }),\n /** An anonymous visitor or device id, before the person is known. */\n anonId: (value: string, options?: HandleOptions): Handle => build(\"anon_id\", value, undefined, options),\n} as const;\n\n/**\n * Accepts an `ObjectRef` or the `type:namespace:id` shorthand. The id may itself contain\n * colons (`invoice:erp:2026:0823`); only the first two separate the parts.\n */\nexport function toObjectRef(object: ObjectRef | string): ObjectRef {\n if (typeof object !== \"string\") return object;\n const first = object.indexOf(\":\");\n const second = first < 0 ? -1 : object.indexOf(\":\", first + 1);\n if (first <= 0 || second <= first + 1 || second === object.length - 1) {\n throw new NiadraValidationError(\"object must look like `type:namespace:id`\");\n }\n return { type: object.slice(0, first), namespace: object.slice(first + 1, second), id: object.slice(second + 1) };\n}\n","/**\n * What every framework adapter shares: the five primitives (context before the model call, the\n * turns, the tools, the verification and the handoff) wired the same way, and fail-open.\n *\n * Nothing here throws into the framework: a failed read gives an empty context, a failed write\n * is logged without content, and the agent keeps working without memory.\n */\n\nimport { recordedTool } from \"../capture/tool.js\";\nimport type { Played } from \"../replay/playback.js\";\nimport type { AgentMemoryParams } from \"../agent-memory.js\";\nimport type { TurnParams } from \"../agent-session.js\";\nimport type { CallCapture, TurnFrame } from \"../capture/frame.js\";\nimport type { ContextOptions, ContextResult } from \"../context.js\";\nimport { emptyResult } from \"../context.js\";\nimport type { Conversation, TurnOptions } from \"../conversation.js\";\nimport type { Logger } from \"../logger.js\";\nimport type { Task } from \"../task.js\";\nimport type { BoundTools } from \"../tools.js\";\nimport type { Handle } from \"../types/common.js\";\nimport type { Verification, VerifyMethod } from \"../types/vocabulary.js\";\n\n/** A customer conversation or an internal agent's task. */\nexport type Session = Conversation | Task;\n\n/** A session, or a function that finds the one the current call belongs to (`null` passes the call through). */\nexport type SessionResolver<S extends Session = Session> = S | (() => S | null | undefined);\n\n/** What the framework proved about the person, recorded with `verify()` before the first context read. */\nexport interface Proof {\n method: VerifyMethod;\n level: Verification;\n /** Defaults to the session's subject. */\n handle?: Handle;\n}\n\n/** A proof, or a function that reads it from the framework when the first context is needed. */\nexport type ProofSource =\n | Proof\n | null\n | undefined\n | (() => Proof | null | undefined | PromiseLike<Proof | null | undefined>);\n\n/**\n * The agent's own working notes: `true` puts the block before the customer's context and offers\n * `search_agent_memory`; `{ write: true }` also offers `remember` (for a key with the\n * `agent_memory:write` scope); the other fields go to `agentMemory()`.\n */\nexport type AgentMemoryOption = boolean | (AgentMemoryParams & { write?: boolean });\n\n/** What goes into the prompt for one model call. */\nexport interface Read {\n context: ContextResult;\n /** The agent's own notes, or an empty string. */\n memory: string;\n /** What goes after the instructions: the notes, then the customer's pack. */\n prefix: string;\n /** What goes at the end: live turns, the turn's slots and deltas. */\n suffix: string;\n}\n\n/** A tool as every framework needs it: the name, the text for the model, the JSON Schema and the call. */\ninterface ToolSpec {\n name: string;\n description: string;\n /** JSON Schema of the arguments. None of them names the customer. */\n parameters: Record<string, unknown>;\n /** Runs the call and returns the text for the model. Never rejects in fail-open mode. */\n execute(args: string | Record<string, unknown>): Promise<string>;\n}\n\nconst ATTESTED: Record<string, Verification> = { A: \"V2\", B: \"V1\", C: \"V1\" };\n\n/**\n * The proof a carrier's STIR/SHAKEN attestation gives a phone call: `A` proves V2, `B` and `C`\n * prove V1, anything else proves nothing (`null`). Accepts the bare letter or the forms carriers\n * and platforms send, such as Twilio's `TN-Validation-Passed-A`.\n */\nexport function attestationProof(attestation: string | null | undefined): Proof | null {\n if (!attestation) return null;\n const value = attestation.trim().toUpperCase();\n if (value.includes(\"FAILED\") || value.startsWith(\"NO-\")) return null;\n const letter = value.length === 1 ? value : /PASSED-([ABC])$/.exec(value)?.[1];\n const level = letter ? ATTESTED[letter] : undefined;\n return level ? { method: \"network_attestation\", level } : null;\n}\n\nexport function resolveSession<S extends Session>(source: SessionResolver<S>): S | null {\n if (typeof source !== \"function\") return source;\n try {\n return source() ?? null;\n } catch {\n return null;\n }\n}\n\nfunction isConversation(session: Session): session is Conversation {\n return \"customer\" in session && typeof session.customer === \"function\";\n}\n\n/** A phone number as an E.164 handle, or `null` when it does not look like one. */\nexport function phoneHandle(value: string | null | undefined): Handle | null {\n const raw = value?.trim().replace(/^tel:/i, \"\").replace(/[\\s().-]/g, \"\");\n if (!raw) return null;\n const e164 = raw.startsWith(\"+\") ? raw : `+${raw}`;\n return /^\\+[1-9]\\d{6,14}$/.test(e164) ? { type: \"phone_e164\", value: e164 } : null;\n}\n\nexport function errorName(error: unknown): string {\n return error instanceof Error ? error.name : typeof error;\n}\n\n/**\n * One session as an adapter drives it. Keeps the verification to a single call made before the\n * first context read, and turns every failure into a log line.\n */\nexport class Bridge {\n private proving: Promise<void> | null = null;\n /** `begin()` started the first read of a call: the first `context()` waits for it with `ready()`. */\n private begun = false;\n private opened = false;\n\n constructor(\n readonly session: Session,\n private readonly proof?: ProofSource,\n private readonly memory: AgentMemoryOption = false,\n ) {}\n\n /**\n * The context and, when enabled, the agent's notes, read side by side within the same budget.\n * Never rejects.\n */\n async read(options: ContextOptions = {}): Promise<Read> {\n const memoryParams = this.memory === false ? null : this.memory === true ? {} : withoutWrite(this.memory);\n const [context, memory] = await Promise.all([\n this.context(options),\n memoryParams ? this.agentMemory(memoryParams, options) : Promise.resolve(\"\"),\n ]);\n return { context, memory, prefix: [memory, context.text].filter(Boolean).join(\"\\n\\n\"), suffix: context.suffix };\n }\n\n get logger(): Logger {\n return this.session.logger;\n }\n\n /**\n * Starts the first read of a call now, in the background, after the proof when one was given (the\n * pack depends on the level it proves): at call start, so the read runs while the call is set up.\n * The first `context()` then waits for it within `timeouts.contextVoiceStart`. Never throws.\n */\n begin(): void {\n const session = this.session;\n if (!isConversation(session)) return;\n const start = (): void => {\n this.safe(\"start the first read\", () => {\n // False off the voice read path (a chat view, the cache off): reads go on as before.\n this.begun = session.begin();\n });\n };\n if (this.proof === undefined || this.proof === null) start();\n else void this.verifyOnce().then(start);\n }\n\n /** Verifies once (when a proof was given), then reads the context. Never rejects. */\n async context(options: ContextOptions = {}): Promise<ContextResult> {\n await this.verifyOnce();\n try {\n const session = this.session;\n if (this.begun && !this.opened && isConversation(session)) {\n this.opened = true;\n // The call's first read, begun at call start: waited for within its own, longer budget.\n await session.ready(options.timeout === undefined ? {} : { timeout: options.timeout });\n }\n return await this.session.context(options);\n } catch (error) {\n this.logger.warn(`could not read context (${errorName(error)})`);\n return emptyResult(null);\n }\n }\n\n /** Records that `context` went into the prompt, unless it was empty for a reason other than the holdout. */\n injected(context: ContextResult): void {\n if (!context.text && !context.suffix && context.response?.path !== \"holdout\") return;\n this.safe(\"mark the injection\", () => {\n this.session.markInjected(context);\n });\n }\n\n customer(text: string, options: TurnOptions = {}): void {\n const session = this.session;\n if (!text.trim() || !isConversation(session)) return;\n this.safe(\"record the customer's turn\", () => session.customer(text, options));\n }\n\n /** Sends the customer's turn so far, while they speak. In the background; never throws. */\n prefetch(text: string): void {\n const session = this.session;\n if (!text.trim() || !isConversation(session)) return;\n this.safe(\"prefetch the turn\", () => session.prefetch(text));\n }\n\n agent(text: string, options: TurnOptions = {}): void {\n if (!text.trim()) return;\n this.safe(\"record the agent's turn\", () => this.session.agent(text, options));\n }\n\n /** Records a transfer. Tasks have no transfer; the call is ignored for them. */\n async handoff(target: \"agent\" | \"human\", reason?: string): Promise<void> {\n const session = this.session;\n if (!isConversation(session)) return;\n try {\n await session.handoff(reason ? { target, reason } : { target });\n } catch (error) {\n this.logger.warn(`could not record the handoff (${errorName(error)})`);\n }\n }\n\n async end(): Promise<void> {\n try {\n await this.session.end();\n } catch (error) {\n this.logger.warn(`could not end the session (${errorName(error)})`);\n }\n }\n\n /** The navigation kit as plain specs (with the agent's memory tools when enabled), or none for a task about an object only. */\n tools(): ToolSpec[] {\n let bound: BoundTools | null;\n try {\n const memory = this.memory;\n bound = this.session.tools(memory === false ? {} : { agentMemory: true, writeAgentMemory: memory !== true && memory.write === true });\n } catch (error) {\n this.logger.warn(`could not build the tools (${errorName(error)})`);\n return [];\n }\n return bound ? toolSpecs(bound) : [];\n }\n\n private async agentMemory(params: AgentMemoryParams, options: ContextOptions): Promise<string> {\n try {\n const result = await this.session.agentMemory(params, options.timeout === undefined ? {} : { timeout: options.timeout });\n return result.text;\n } catch (error) {\n this.logger.warn(`could not read the agent's memory (${errorName(error)})`);\n return \"\";\n }\n }\n\n private async verifyOnce(): Promise<void> {\n if (this.proof === undefined || this.proof === null) return;\n this.proving ??= this.runVerify();\n await this.proving;\n }\n\n private async runVerify(): Promise<void> {\n try {\n const source = this.proof;\n const proof = typeof source === \"function\" ? await source() : source;\n if (!proof) return;\n await this.session.verify(proof);\n } catch (error) {\n this.logger.warn(`could not record the verification (${errorName(error)})`);\n }\n }\n\n private safe(what: string, run: () => unknown): void {\n try {\n run();\n } catch (error) {\n this.logger.warn(`could not ${what} (${errorName(error)})`);\n }\n }\n}\n\nfunction withoutWrite(option: AgentMemoryParams & { write?: boolean }): AgentMemoryParams {\n const { write: _, ...params } = option;\n return params;\n}\n\n/** The bound kit as specs. The customer stays inside `bound`; no parameter names them. */\nfunction toolSpecs(bound: BoundTools): ToolSpec[] {\n return bound.definitions.map(({ function: definition }) => ({\n name: definition.name,\n description: definition.description,\n parameters: definition.parameters,\n execute: (args) => bound.call(definition.name, args),\n }));\n}\n\n/** The text of a message content in the shapes model APIs use: a string, or a list of text parts. */\nexport function textOf(content: unknown): string {\n if (typeof content === \"string\") return content;\n if (!Array.isArray(content)) return \"\";\n const parts: string[] = [];\n for (const part of content as unknown[]) {\n if (typeof part === \"string\") parts.push(part);\n else if (isRecord(part) && typeof part.text === \"string\" && (part.type === undefined || part.type === \"text\")) {\n parts.push(part.text);\n }\n }\n return parts.join(\"\");\n}\n\nexport function isRecord(value: unknown): value is Record<PropertyKey, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\n/** A non-negative integer, or `null`. */\nexport function count(value: unknown): number | null {\n return typeof value === \"number\" && Number.isInteger(value) && value >= 0 ? value : null;\n}\n\n/**\n * The turn records an adapter keeps from its framework's callbacks, keyed by the framework's own ids (a\n * run, a request, a tool call id). A run starting while the session has a turn in progress (yours, from\n * `conversation.turn()`, or a replay's) records into it; otherwise the adapter opens its own turn and closes\n * it when the run ends. A tool call recorded here is taken over by a `tool()` wrapper running inside it, so\n * the call is recorded once. Every method does nothing when turns are off and never throws.\n */\nexport class TurnHooks {\n /** Runs held open at most; past it the oldest run's turn closes, incomplete (a run whose end never came). */\n static readonly MAX_OPEN = 100;\n private readonly frames = new Map<string, { frame: TurnFrame; own: boolean }>();\n private readonly calls = new Map<string, CallCapture>();\n\n constructor(\n readonly adapter: string,\n readonly enabled: boolean,\n ) {}\n\n /** The turn of the run `key`: the session's turn in progress, or a new one of its own. */\n open(session: Session, key: string, params: TurnParams = {}): TurnFrame | undefined {\n if (!this.enabled) return undefined;\n const held = this.frames.get(key);\n if (held !== undefined) return held.frame;\n try {\n const active = session.activeTurn();\n if (active !== undefined && !this.owns(active)) {\n this.frames.set(key, { frame: active, own: false });\n return active;\n }\n const frame = session.openTurn({ ...params, adapter: this.adapter });\n this.frames.set(key, { frame, own: true });\n if (this.frames.size > TurnHooks.MAX_OPEN) {\n const [oldest] = this.frames.keys();\n if (oldest !== undefined) {\n this.frames.get(oldest)?.frame.incomplete();\n this.close(oldest);\n }\n }\n return frame;\n } catch (error) {\n session.logger.warn(`could not open the turn (${errorName(error)})`);\n return undefined;\n }\n }\n\n /** The turn of `key`, else the session's turn in progress. */\n frame(session: Session, key?: string): TurnFrame | undefined {\n if (!this.enabled) return undefined;\n const held = key === undefined ? undefined : this.frames.get(key);\n const frame = held?.frame ?? session.activeTurn();\n return frame === undefined || frame.closed ? undefined : frame;\n }\n\n has(key: string): boolean {\n return this.frames.has(key);\n }\n\n private owns(frame: TurnFrame): boolean {\n for (const held of this.frames.values()) if (held.own && held.frame === frame) return true;\n return false;\n }\n\n /** Ends the run `key`: a turn the adapter opened closes; the calls it left open failed. */\n close(key: string, error?: unknown): void {\n const held = this.frames.get(key);\n if (held === undefined) return;\n this.frames.delete(key);\n for (const [callKey, call] of this.calls) {\n if (call.frame !== held.frame) continue;\n this.calls.delete(callKey);\n if (held.own) call.failed(error ?? \"error\");\n }\n if (held.own) held.frame.close(error);\n }\n\n /** A tool call starting, keyed by `callKey`; `callId` is the provider's id of the call, when there is one. */\n toolStart(session: Session, callKey: string, name: string, args: unknown, options: { frameKey?: string; callId?: string | null } = {}): void {\n const frame = this.frame(session, options.frameKey);\n if (frame === undefined) return;\n try {\n this.calls.set(callKey, frame.toolCall(name, args, { callId: options.callId ?? null, adoptable: true }));\n } catch {\n frame.incomplete();\n }\n }\n\n /**\n * In a replay, how a framework tool the adapter stands in for answers: the recorded result of a call with the\n * same arguments, or empty (a divergence), never the live tool. `null` outside a replay, and for a function\n * wrapped with `tool()`, which answers itself.\n */\n replayed(session: Session, name: string, args: unknown, fn?: unknown, frameKey?: string): Played | null {\n const frame = this.enabled ? this.frame(session, frameKey) : session.activeTurn();\n if (frame?.playback == null || (fn !== undefined && recordedTool(fn) !== null)) return null;\n return frame.playback.answer(name, args, false);\n }\n\n toolEnd(callKey: string, result?: unknown, error?: unknown): void {\n const call = this.calls.get(callKey);\n if (call === undefined) return;\n this.calls.delete(callKey);\n try {\n if (error !== undefined) call.failed(error);\n else call.result(result);\n } catch {\n call.frame.incomplete();\n }\n }\n\n /** A model call that ended, with its tokens when the framework reported them. */\n model(\n session: Session,\n model: string | null | undefined,\n tokens: { in?: number | null; out?: number | null; cached?: number | null } = {},\n frameKey?: string,\n ): void {\n const frame = this.frame(session, frameKey);\n if (frame === undefined) return;\n try {\n frame.modelCall(model, { tokensIn: tokens.in ?? null, tokensOut: tokens.out ?? null, tokensCached: tokens.cached ?? 0 });\n } catch {\n frame.incomplete();\n }\n }\n}\n","/**\n * Helpers for the webhook adapters (ElevenLabs, Vapi, WhatsApp, Twilio): signatures with Web\n * Crypto and header lookup. Only web APIs, so the handlers run on Node 20+, Deno, Bun, Cloudflare\n * Workers and the Vercel Edge Runtime alike.\n */\n\nimport type { Handle } from \"../types/common.js\";\nimport type { ContextStamp } from \"../types/events.js\";\nimport type { Verification } from \"../types/vocabulary.js\";\n\n/**\n * What a call's first webhook learned, for the webhooks that come later in the same call (tool\n * calls, the end-of-call report): who the caller is, the level they proved and the context stamp.\n */\nexport interface CallRecord {\n subject: Handle;\n verification: Verification;\n stamp?: ContextStamp | null;\n}\n\n/**\n * Where the webhook adapters keep a `CallRecord` between requests, by the platform's call id.\n * The default lives in this process's memory; on serverless or several instances, pass one backed\n * by your key-value store.\n */\nexport interface CallStore {\n get(id: string): CallRecord | undefined | PromiseLike<CallRecord | undefined>;\n set(id: string, record: CallRecord): void | PromiseLike<void>;\n}\n\n/** An in-memory `CallStore` that keeps the most recent `max` calls. */\nexport function memoryCallStore(max = 10_000): CallStore {\n const calls = new Map<string, CallRecord>();\n return {\n get: (id) => calls.get(id),\n set: (id, record) => {\n calls.delete(id);\n calls.set(id, record);\n if (calls.size > max) {\n const oldest = calls.keys().next().value;\n if (oldest !== undefined) calls.delete(oldest);\n }\n },\n };\n}\n\n/** Request headers as a `Headers` object or a plain record (any case). */\nexport type HeadersLike = Headers | Record<string, string | string[] | undefined>;\n\n/** What a handler answers: the HTTP status and a JSON-serializable body (`null` for no body). */\nexport interface WebhookResponse<B = unknown> {\n status: number;\n body: B;\n}\n\nexport function header(headers: HeadersLike, name: string): string | undefined {\n if (typeof (headers as Headers).get === \"function\") return (headers as Headers).get(name) ?? undefined;\n const wanted = name.toLowerCase();\n for (const [key, value] of Object.entries(headers as Record<string, string | string[] | undefined>)) {\n if (key.toLowerCase() !== wanted) continue;\n return Array.isArray(value) ? value[0] : value;\n }\n return undefined;\n}\n\nfunction subtle(): SubtleCrypto {\n const crypto = (globalThis as { crypto?: Crypto }).crypto;\n if (!crypto?.subtle) throw new Error(\"Web Crypto is not available on this runtime\");\n return crypto.subtle;\n}\n\nconst encoder = new TextEncoder();\n\nexport async function hmac(hash: \"SHA-1\" | \"SHA-256\", secret: string, message: string): Promise<Uint8Array> {\n const key = await subtle().importKey(\"raw\", encoder.encode(secret), { name: \"HMAC\", hash }, false, [\"sign\"]);\n return new Uint8Array(await subtle().sign(\"HMAC\", key, encoder.encode(message)));\n}\n\nexport function hex(bytes: Uint8Array): string {\n let out = \"\";\n for (const byte of bytes) out += byte.toString(16).padStart(2, \"0\");\n return out;\n}\n\nexport function base64(bytes: Uint8Array): string {\n let binary = \"\";\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary);\n}\n\n/** Compares two strings in time that depends only on their length. */\nexport function safeEqual(a: string, b: string): boolean {\n const left = encoder.encode(a);\n const right = encoder.encode(b);\n if (left.length !== right.length) return false;\n let diff = 0;\n for (let i = 0; i < left.length; i++) diff |= (left[i] ?? 0) ^ (right[i] ?? 0);\n return diff === 0;\n}\n\n/** The raw body as text, whatever form the framework hands it in. */\nexport function bodyText(body: string | Uint8Array | ArrayBuffer): string {\n if (typeof body === \"string\") return body;\n return new TextDecoder().decode(body instanceof Uint8Array ? body : new Uint8Array(body));\n}\n\nexport function parseJson(text: string): unknown {\n try {\n return JSON.parse(text) as unknown;\n } catch {\n return undefined;\n }\n}\n","/**\n * Niadra for the WhatsApp Cloud API (Meta). Translates only: it checks and reads Meta's webhook,\n * gives you each inbound message with the customer's handle and the turn ready to record, and\n * records the agent's turn from the send API's answer. It never sends a message.\n *\n * @example\n * const { status, messages } = await readWhatsApp(await request.text(), request.headers, { appSecret });\n * for (const inbound of messages) {\n * const convo = niadra.conversation({ subject: inbound.subject, channel: \"whatsapp\", conversation_id: yourThreadId });\n * recordInbound(convo, inbound);\n * const ctx = await convo.context();\n * ...\n * recordOutbound(convo, reply, await sendResponse.json());\n * }\n */\n\nimport type { Conversation } from \"../conversation.js\";\nimport { handles } from \"../handles.js\";\nimport type { Handle } from \"../types/common.js\";\nimport type { Content } from \"../types/events.js\";\nimport { errorName, isRecord } from \"./shared.js\";\nimport { bodyText, header, hex, hmac, parseJson, safeEqual } from \"./webhook.js\";\nimport type { HeadersLike, WebhookResponse } from \"./webhook.js\";\n\nexport type { HeadersLike, WebhookResponse } from \"./webhook.js\";\n\n/** A media attachment as Meta reports it. Download it with the Graph API, then `niadra.uploadMedia()`. */\nexport interface WhatsAppMedia {\n /** Meta's media id. */\n id: string;\n mimeType?: string | undefined;\n filename?: string | undefined;\n}\n\n/** One inbound message, read from the webhook. */\nexport interface WhatsAppInbound {\n /** The customer, as `wa_id`. */\n subject: Handle;\n waId: string;\n /** The name on the customer's WhatsApp profile, when Meta sends it. */\n profileName?: string | undefined;\n /** Your business number that received the message. */\n phoneNumberId: string;\n /** Meta's message id (`wamid...`): the idempotency key of the turn. */\n messageId: string;\n /** Meta's message type: `text`, `image`, `audio`, `document`, `interactive`, ... */\n type: string;\n /** The text of the message, the caption of media, or the title of a button or list reply. */\n text: string;\n media?: WhatsAppMedia | undefined;\n occurredAt?: Date | undefined;\n /** The message this one replies to, when it is a reply. */\n replyTo?: string | undefined;\n}\n\nexport interface ReadResult extends WebhookResponse<null> {\n /** The inbound messages, empty for status updates and for requests that fail the signature. */\n messages: WhatsAppInbound[];\n}\n\nconst MEDIA = new Set([\"image\", \"audio\", \"video\", \"document\", \"sticker\", \"voice\"]);\nconst CONTENT: Record<string, Content[\"type\"]> = { image: \"image\", sticker: \"image\", audio: \"audio\", voice: \"audio\", video: \"file\", document: \"file\" };\n\n/** Checks `X-Hub-Signature-256: sha256=<hex HMAC-SHA256 of the raw body with the app secret>`. */\nexport async function validWhatsAppSignature(rawBody: string | Uint8Array | ArrayBuffer, signature: string | undefined, appSecret: string): Promise<boolean> {\n if (!signature || !appSecret || !signature.startsWith(\"sha256=\")) return false;\n const expected = `sha256=${hex(await hmac(\"SHA-256\", appSecret, bodyText(rawBody)))}`;\n return safeEqual(signature.toLowerCase(), expected);\n}\n\n/**\n * Answers Meta's subscription check (`GET` with `hub.mode`, `hub.verify_token`, `hub.challenge`):\n * the challenge when the token matches, 403 otherwise.\n */\nexport function whatsAppChallenge(query: URLSearchParams | Record<string, string | undefined>, verifyToken: string): WebhookResponse<string> {\n const get = (name: string): string | undefined =>\n query instanceof URLSearchParams ? (query.get(name) ?? undefined) : query[name];\n const token = get(\"hub.verify_token\");\n const challenge = get(\"hub.challenge\");\n if (get(\"hub.mode\") === \"subscribe\" && token !== undefined && verifyToken && safeEqual(token, verifyToken) && challenge) {\n return { status: 200, body: challenge };\n }\n return { status: 403, body: \"\" };\n}\n\n/**\n * Checks the signature and reads every inbound message of a webhook delivery. Answer Meta with\n * `status` at once (200, or 401 for a bad signature); a delivery that fails is not read.\n */\nexport async function readWhatsApp(\n rawBody: string | Uint8Array | ArrayBuffer,\n headers: HeadersLike,\n options: { appSecret: string },\n): Promise<ReadResult> {\n const text = bodyText(rawBody);\n if (!(await validWhatsAppSignature(text, header(headers, \"x-hub-signature-256\"), options.appSecret))) {\n return { status: 401, body: null, messages: [] };\n }\n return { status: 200, body: null, messages: parseWhatsApp(parseJson(text)) };\n}\n\n/** Reads the inbound messages of an already verified webhook payload. */\nexport function parseWhatsApp(payload: unknown): WhatsAppInbound[] {\n const messages: WhatsAppInbound[] = [];\n const entries = isRecord(payload) && Array.isArray(payload.entry) ? (payload.entry as unknown[]) : [];\n for (const entry of entries) {\n const changes = isRecord(entry) && Array.isArray(entry.changes) ? (entry.changes as unknown[]) : [];\n for (const change of changes) {\n const value = isRecord(change) && isRecord(change.value) ? change.value : null;\n if (!value || !Array.isArray(value.messages)) continue;\n const phoneNumberId = isRecord(value.metadata) ? str(value.metadata.phone_number_id) : undefined;\n const names = new Map<string, string>();\n for (const contact of Array.isArray(value.contacts) ? (value.contacts as unknown[]) : []) {\n const waId = isRecord(contact) ? str(contact.wa_id) : undefined;\n const name = isRecord(contact) && isRecord(contact.profile) ? str(contact.profile.name) : undefined;\n if (waId && name) names.set(waId, name);\n }\n for (const message of value.messages as unknown[]) {\n const inbound = isRecord(message) && phoneNumberId ? readMessage(message, phoneNumberId, names) : null;\n if (inbound) messages.push(inbound);\n }\n }\n }\n return messages;\n}\n\n/**\n * Records an inbound message as the customer's turn, with Meta's message id as the idempotency key,\n * so a redelivered webhook is harmless. For media, pass what `niadra.uploadMedia()` returned.\n */\nexport function recordInbound(\n conversation: Conversation,\n inbound: WhatsAppInbound,\n media?: { media_ref: string; media_sha256?: string | undefined },\n): string | null {\n try {\n const kind = CONTENT[inbound.type];\n if (media && kind) {\n return conversation.track({\n speaker: \"customer\",\n idempotency_key: inbound.messageId,\n ...(inbound.occurredAt ? { occurred_at: inbound.occurredAt } : {}),\n content: {\n type: kind,\n media_ref: media.media_ref,\n ...(media.media_sha256 ? { media_sha256: media.media_sha256 } : {}),\n ...(inbound.text ? { text: inbound.text } : {}),\n },\n });\n }\n if (!inbound.text) return null;\n return conversation.customer(inbound.text, {\n idempotency_key: inbound.messageId,\n ...(inbound.occurredAt ? { occurred_at: inbound.occurredAt } : {}),\n });\n } catch (error) {\n conversation.logger.warn(`could not record the WhatsApp message (${errorName(error)})`);\n return null;\n }\n}\n\n/**\n * Records the agent's turn after the send API accepted it, with the `wamid` Meta returned as the\n * idempotency key. `response` is the JSON of `POST /{phone_number_id}/messages`.\n */\nexport function recordOutbound(conversation: Conversation, text: string, response?: unknown): string | null {\n try {\n const messages = isRecord(response) && Array.isArray(response.messages) ? (response.messages as unknown[]) : [];\n const id = isRecord(messages[0]) ? str(messages[0].id) : undefined;\n return conversation.agent(text, id ? { idempotency_key: id } : {});\n } catch (error) {\n conversation.logger.warn(`could not record the agent's WhatsApp message (${errorName(error)})`);\n return null;\n }\n}\n\nfunction readMessage(message: Record<PropertyKey, unknown>, phoneNumberId: string, names: Map<string, string>): WhatsAppInbound | null {\n const waId = str(message.from);\n const id = str(message.id);\n const type = str(message.type) ?? \"unknown\";\n if (!waId || !id) return null;\n let text = \"\";\n let media: WhatsAppMedia | undefined;\n const body = message[type];\n if (type === \"text\" && isRecord(body)) text = str(body.body) ?? \"\";\n else if (type === \"button\" && isRecord(body)) text = str(body.text) ?? \"\";\n else if (type === \"interactive\" && isRecord(body)) {\n const reply = isRecord(body.button_reply) ? body.button_reply : isRecord(body.list_reply) ? body.list_reply : null;\n text = reply ? (str(reply.title) ?? \"\") : \"\";\n } else if (MEDIA.has(type) && isRecord(body)) {\n const mediaId = str(body.id);\n text = str(body.caption) ?? \"\";\n if (mediaId) media = { id: mediaId, mimeType: str(body.mime_type), filename: str(body.filename) };\n }\n const seconds = Number(message.timestamp);\n const context = isRecord(message.context) ? str(message.context.id) : undefined;\n return {\n subject: handles.waId(waId),\n waId,\n profileName: names.get(waId),\n phoneNumberId,\n messageId: id,\n type,\n text,\n media,\n occurredAt: Number.isFinite(seconds) && seconds > 0 ? new Date(seconds * 1000) : undefined,\n replyTo: context,\n };\n}\n\nfunction str(value: unknown): string | undefined {\n return typeof value === \"string\" && value.length > 0 ? value : undefined;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/handles.ts","../src/integrations/shared.ts","../src/integrations/webhook.ts","../src/integrations/whatsapp.ts"],"names":[],"mappings":"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","file":"whatsapp.cjs","sourcesContent":["import { NiadraValidationError } from \"./errors.js\";\nimport type { Handle, ObjectRef } from \"./types/common.js\";\nimport type { SubjectKind } from \"./types/vocabulary.js\";\n\ninterface HandleOptions {\n /** Overrides the subject kind the server would infer from the handle type. */\n subjectKind?: SubjectKind;\n}\n\nfunction build(type: Handle[\"type\"], value: string, scope?: string, options?: HandleOptions): Handle {\n const handle: Handle = { type, value };\n if (scope !== undefined) handle.scope = scope;\n if (options?.subjectKind !== undefined) handle.subject_kind = options.subjectKind;\n return handle;\n}\n\n/**\n * Builders for the handle types the API accepts. They only assemble the object; the value is\n * normalized and validated on the server, which is the single source of truth for matching.\n *\n * @example\n * niadra.context({ subject: handles.phone(\"+5511987654321\"), conversation_id: \"wa-8812\" })\n */\nexport const handles = {\n /** A phone number in E.164 form, such as `+5511987654321`. */\n phone: (e164: string, options?: HandleOptions): Handle => build(\"phone_e164\", e164, undefined, options),\n email: (address: string, options?: HandleOptions): Handle => build(\"email\", address, undefined, options),\n /** The WhatsApp id (`wa_id`) the Cloud API reports for a contact. */\n waId: (value: string, options?: HandleOptions): Handle => build(\"wa_id\", value, undefined, options),\n waJid: (value: string, options?: HandleOptions): Handle => build(\"wa_jid\", value, undefined, options),\n waLid: (value: string, options?: HandleOptions): Handle => build(\"wa_lid\", value, undefined, options),\n /** A business-scoped WhatsApp user id; `businessAccount` is the WhatsApp Business account it belongs to. */\n waBsuid: (value: string, businessAccount: string, options?: HandleOptions): Handle =>\n build(\"wa_bsuid\", value, businessAccount, options),\n /** The user id in your own app or site. */\n appUserId: (value: string, options?: HandleOptions): Handle => build(\"app_user_id\", value, undefined, options),\n /** The id of a person or organization in a system of record; `system` names that system, such as `crm`. */\n systemId: (value: string, system: string, options?: HandleOptions): Handle =>\n build(\"system_id\", value, system, options),\n /**\n * A person's national document, such as a CPF: `govIdHmac(\"529.982.247-25\", \"BR\")`. Send the number\n * itself, never a hash of it: the server checks its check digits, then keeps only a keyed hash with your\n * space's secret and an encrypted copy, and shows it masked. `country` is its ISO 3166-1 alpha-2 code.\n */\n govIdHmac: (value: string, country: string, options?: HandleOptions): Handle =>\n build(\"gov_id_hmac\", value, country, options),\n /**\n * A company's registry number, such as a CNPJ: `orgRegistryHmac(\"11.222.333/0001-81\", \"BR\")`. As with\n * `govIdHmac`, send the number itself; the server checks it and keeps only a keyed hash. Identifies an\n * organization.\n */\n orgRegistryHmac: (value: string, country: string): Handle =>\n build(\"org_registry_hmac\", value, country, { subjectKind: \"account\" }),\n /** An e-mail domain, such as `acme.com`. Identifies an organization. */\n emailDomain: (domain: string): Handle => build(\"email_domain\", domain, undefined, { subjectKind: \"account\" }),\n /** An anonymous visitor or device id, before the person is known. */\n anonId: (value: string, options?: HandleOptions): Handle => build(\"anon_id\", value, undefined, options),\n} as const;\n\n/**\n * Accepts an `ObjectRef` or the `type:namespace:id` shorthand. The id may itself contain\n * colons (`invoice:erp:2026:0823`); only the first two separate the parts.\n */\nexport function toObjectRef(object: ObjectRef | string): ObjectRef {\n if (typeof object !== \"string\") return object;\n const first = object.indexOf(\":\");\n const second = first < 0 ? -1 : object.indexOf(\":\", first + 1);\n if (first <= 0 || second <= first + 1 || second === object.length - 1) {\n throw new NiadraValidationError(\"object must look like `type:namespace:id`\");\n }\n return { type: object.slice(0, first), namespace: object.slice(first + 1, second), id: object.slice(second + 1) };\n}\n","/**\n * What every framework adapter shares: the five primitives (context before the model call, the\n * turns, the tools, the verification and the handoff) wired the same way, and fail-open.\n *\n * Nothing here throws into the framework: a failed read gives an empty context, a failed write\n * is logged without content, and the agent keeps working without memory.\n */\n\nimport { recordedTool } from \"../capture/tool.js\";\nimport type { Played } from \"../replay/playback.js\";\nimport type { AgentMemoryParams } from \"../agent-memory.js\";\nimport type { TurnParams } from \"../agent-session.js\";\nimport type { CallCapture, TurnFrame } from \"../capture/frame.js\";\nimport type { ContextOptions, ContextResult } from \"../context.js\";\nimport { emptyResult } from \"../context.js\";\nimport type { Conversation, TurnOptions } from \"../conversation.js\";\nimport type { Logger } from \"../logger.js\";\nimport type { Task } from \"../task.js\";\nimport type { BoundTools } from \"../tools.js\";\nimport type { Handle } from \"../types/common.js\";\nimport type { Verification, VerifyMethod } from \"../types/vocabulary.js\";\n\n/** A customer conversation or an internal agent's task. */\nexport type Session = Conversation | Task;\n\n/** A session, or a function that finds the one the current call belongs to (`null` passes the call through). */\nexport type SessionResolver<S extends Session = Session> = S | (() => S | null | undefined);\n\n/** What the framework proved about the person, recorded with `verify()` before the first context read. */\nexport interface Proof {\n method: VerifyMethod;\n level: Verification;\n /** Defaults to the session's subject. */\n handle?: Handle;\n}\n\n/** A proof, or a function that reads it from the framework when the first context is needed. */\nexport type ProofSource =\n | Proof\n | null\n | undefined\n | (() => Proof | null | undefined | PromiseLike<Proof | null | undefined>);\n\n/**\n * The agent's own working notes: `true` puts the block before the customer's context and offers\n * `search_agent_memory`; `{ write: true }` also offers `remember` (for a key with the\n * `agent_memory:write` scope); the other fields go to `agentMemory()`.\n */\nexport type AgentMemoryOption = boolean | (AgentMemoryParams & { write?: boolean });\n\n/** What goes into the prompt for one model call. */\nexport interface Read {\n context: ContextResult;\n /** The agent's own notes, or an empty string. */\n memory: string;\n /** What goes after the instructions: the notes, then the customer's pack. */\n prefix: string;\n /** What goes at the end: live turns, the turn's slots and deltas. */\n suffix: string;\n}\n\n/** A tool as every framework needs it: the name, the text for the model, the JSON Schema and the call. */\ninterface ToolSpec {\n name: string;\n description: string;\n /** JSON Schema of the arguments. None of them names the customer. */\n parameters: Record<string, unknown>;\n /** Runs the call and returns the text for the model. Never rejects in fail-open mode. */\n execute(args: string | Record<string, unknown>): Promise<string>;\n}\n\nconst ATTESTED: Record<string, Verification> = { A: \"V2\", B: \"V1\", C: \"V1\" };\n\n/**\n * The proof a carrier's STIR/SHAKEN attestation gives a phone call: `A` proves V2, `B` and `C`\n * prove V1, anything else proves nothing (`null`). Accepts the bare letter or the forms carriers\n * and platforms send, such as Twilio's `TN-Validation-Passed-A`.\n */\nexport function attestationProof(attestation: string | null | undefined): Proof | null {\n if (!attestation) return null;\n const value = attestation.trim().toUpperCase();\n if (value.includes(\"FAILED\") || value.startsWith(\"NO-\")) return null;\n const letter = value.length === 1 ? value : /PASSED-([ABC])$/.exec(value)?.[1];\n const level = letter ? ATTESTED[letter] : undefined;\n return level ? { method: \"network_attestation\", level } : null;\n}\n\nexport function resolveSession<S extends Session>(source: SessionResolver<S>): S | null {\n if (typeof source !== \"function\") return source;\n try {\n return source() ?? null;\n } catch {\n return null;\n }\n}\n\nfunction isConversation(session: Session): session is Conversation {\n return \"customer\" in session && typeof session.customer === \"function\";\n}\n\n/** A phone number as an E.164 handle, or `null` when it does not look like one. */\nexport function phoneHandle(value: string | null | undefined): Handle | null {\n const raw = value?.trim().replace(/^tel:/i, \"\").replace(/[\\s().-]/g, \"\");\n if (!raw) return null;\n const e164 = raw.startsWith(\"+\") ? raw : `+${raw}`;\n return /^\\+[1-9]\\d{6,14}$/.test(e164) ? { type: \"phone_e164\", value: e164 } : null;\n}\n\nexport function errorName(error: unknown): string {\n return error instanceof Error ? error.name : typeof error;\n}\n\n/**\n * One session as an adapter drives it. Keeps the verification to a single call made before the\n * first context read, and turns every failure into a log line.\n */\nexport class Bridge {\n private proving: Promise<void> | null = null;\n /** `begin()` started the first read of a call: the first `context()` waits for it with `ready()`. */\n private begun = false;\n private opened = false;\n\n constructor(\n readonly session: Session,\n private readonly proof?: ProofSource,\n private readonly memory: AgentMemoryOption = false,\n ) {}\n\n /**\n * The context and, when enabled, the agent's notes, read side by side within the same budget.\n * Never rejects.\n */\n async read(options: ContextOptions = {}): Promise<Read> {\n const memoryParams = this.memory === false ? null : this.memory === true ? {} : withoutWrite(this.memory);\n const [context, memory] = await Promise.all([\n this.context(options),\n memoryParams ? this.agentMemory(memoryParams, options) : Promise.resolve(\"\"),\n ]);\n return { context, memory, prefix: [memory, context.text].filter(Boolean).join(\"\\n\\n\"), suffix: context.suffix };\n }\n\n get logger(): Logger {\n return this.session.logger;\n }\n\n /**\n * Starts the first read of a call now, in the background, after the proof when one was given (the\n * pack depends on the level it proves): at call start, so the read runs while the call is set up.\n * The first `context()` then waits for it within `timeouts.contextVoiceStart`. Never throws.\n */\n begin(): void {\n const session = this.session;\n if (!isConversation(session)) return;\n const start = (): void => {\n this.safe(\"start the first read\", () => {\n // False off the voice read path (a chat view, the cache off): reads go on as before.\n this.begun = session.begin();\n });\n };\n if (this.proof === undefined || this.proof === null) start();\n else void this.verifyOnce().then(start);\n }\n\n /** Verifies once (when a proof was given), then reads the context. Never rejects. */\n async context(options: ContextOptions = {}): Promise<ContextResult> {\n await this.verifyOnce();\n try {\n const session = this.session;\n if (this.begun && !this.opened && isConversation(session)) {\n this.opened = true;\n // The call's first read, begun at call start: waited for within its own, longer budget.\n await session.ready(options.timeout === undefined ? {} : { timeout: options.timeout });\n }\n return await this.session.context(options);\n } catch (error) {\n this.logger.warn(`could not read context (${errorName(error)})`);\n return emptyResult(null);\n }\n }\n\n /** Records that `context` went into the prompt, unless it was empty for a reason other than the holdout. */\n injected(context: ContextResult): void {\n if (!context.text && !context.suffix && context.response?.path !== \"holdout\") return;\n this.safe(\"mark the injection\", () => {\n this.session.markInjected(context);\n });\n }\n\n customer(text: string, options: TurnOptions = {}): void {\n const session = this.session;\n if (!text.trim() || !isConversation(session)) return;\n this.safe(\"record the customer's turn\", () => session.customer(text, options));\n }\n\n /** Sends the customer's turn so far, while they speak. In the background; never throws. */\n prefetch(text: string): void {\n const session = this.session;\n if (!text.trim() || !isConversation(session)) return;\n this.safe(\"prefetch the turn\", () => session.prefetch(text));\n }\n\n agent(text: string, options: TurnOptions = {}): void {\n if (!text.trim()) return;\n this.safe(\"record the agent's turn\", () => this.session.agent(text, options));\n }\n\n /** Records a transfer. Tasks have no transfer; the call is ignored for them. */\n async handoff(target: \"agent\" | \"human\", reason?: string): Promise<void> {\n const session = this.session;\n if (!isConversation(session)) return;\n try {\n await session.handoff(reason ? { target, reason } : { target });\n } catch (error) {\n this.logger.warn(`could not record the handoff (${errorName(error)})`);\n }\n }\n\n async end(): Promise<void> {\n try {\n await this.session.end();\n } catch (error) {\n this.logger.warn(`could not end the session (${errorName(error)})`);\n }\n }\n\n /** The navigation kit as plain specs (with the agent's memory tools when enabled), or none for a task about an object only. */\n tools(): ToolSpec[] {\n let bound: BoundTools | null;\n try {\n const memory = this.memory;\n bound = this.session.tools(memory === false ? {} : { agentMemory: true, writeAgentMemory: memory !== true && memory.write === true });\n } catch (error) {\n this.logger.warn(`could not build the tools (${errorName(error)})`);\n return [];\n }\n return bound ? toolSpecs(bound) : [];\n }\n\n private async agentMemory(params: AgentMemoryParams, options: ContextOptions): Promise<string> {\n try {\n const result = await this.session.agentMemory(params, options.timeout === undefined ? {} : { timeout: options.timeout });\n return result.text;\n } catch (error) {\n this.logger.warn(`could not read the agent's memory (${errorName(error)})`);\n return \"\";\n }\n }\n\n private async verifyOnce(): Promise<void> {\n if (this.proof === undefined || this.proof === null) return;\n this.proving ??= this.runVerify();\n await this.proving;\n }\n\n private async runVerify(): Promise<void> {\n try {\n const source = this.proof;\n const proof = typeof source === \"function\" ? await source() : source;\n if (!proof) return;\n await this.session.verify(proof);\n } catch (error) {\n this.logger.warn(`could not record the verification (${errorName(error)})`);\n }\n }\n\n private safe(what: string, run: () => unknown): void {\n try {\n run();\n } catch (error) {\n this.logger.warn(`could not ${what} (${errorName(error)})`);\n }\n }\n}\n\nfunction withoutWrite(option: AgentMemoryParams & { write?: boolean }): AgentMemoryParams {\n const { write: _, ...params } = option;\n return params;\n}\n\n/** The bound kit as specs. The customer stays inside `bound`; no parameter names them. */\nfunction toolSpecs(bound: BoundTools): ToolSpec[] {\n return bound.definitions.map(({ function: definition }) => ({\n name: definition.name,\n description: definition.description,\n parameters: definition.parameters,\n execute: (args) => bound.call(definition.name, args),\n }));\n}\n\n/** The text of a message content in the shapes model APIs use: a string, or a list of text parts. */\nexport function textOf(content: unknown): string {\n if (typeof content === \"string\") return content;\n if (!Array.isArray(content)) return \"\";\n const parts: string[] = [];\n for (const part of content as unknown[]) {\n if (typeof part === \"string\") parts.push(part);\n else if (isRecord(part) && typeof part.text === \"string\" && (part.type === undefined || part.type === \"text\")) {\n parts.push(part.text);\n }\n }\n return parts.join(\"\");\n}\n\nexport function isRecord(value: unknown): value is Record<PropertyKey, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\n/** A non-negative integer, or `null`. */\nexport function count(value: unknown): number | null {\n return typeof value === \"number\" && Number.isInteger(value) && value >= 0 ? value : null;\n}\n\n/**\n * The turn records an adapter keeps from its framework's callbacks, keyed by the framework's own ids (a\n * run, a request, a tool call id). A run starting while the session has a turn in progress (yours, from\n * `conversation.turn()`, or a replay's) records into it; otherwise the adapter opens its own turn and closes\n * it when the run ends. A tool call recorded here is taken over by a `tool()` wrapper running inside it, so\n * the call is recorded once. Every method does nothing when turns are off and never throws.\n */\nexport class TurnHooks {\n /** Runs held open at most; past it the oldest run's turn closes, incomplete (a run whose end never came). */\n static readonly MAX_OPEN = 100;\n private readonly frames = new Map<string, { frame: TurnFrame; own: boolean }>();\n private readonly calls = new Map<string, CallCapture>();\n\n constructor(\n readonly adapter: string,\n readonly enabled: boolean,\n ) {}\n\n /** The turn of the run `key`: the session's turn in progress, or a new one of its own. */\n open(session: Session, key: string, params: TurnParams = {}): TurnFrame | undefined {\n if (!this.enabled) return undefined;\n const held = this.frames.get(key);\n if (held !== undefined) return held.frame;\n try {\n const active = session.activeTurn();\n if (active !== undefined && !this.owns(active)) {\n this.frames.set(key, { frame: active, own: false });\n return active;\n }\n const frame = session.openTurn({ ...params, adapter: this.adapter });\n this.frames.set(key, { frame, own: true });\n if (this.frames.size > TurnHooks.MAX_OPEN) {\n const [oldest] = this.frames.keys();\n if (oldest !== undefined) {\n this.frames.get(oldest)?.frame.incomplete();\n this.close(oldest);\n }\n }\n return frame;\n } catch (error) {\n session.logger.warn(`could not open the turn (${errorName(error)})`);\n return undefined;\n }\n }\n\n /** The turn of `key`, else the session's turn in progress. */\n frame(session: Session, key?: string): TurnFrame | undefined {\n if (!this.enabled) return undefined;\n const held = key === undefined ? undefined : this.frames.get(key);\n const frame = held?.frame ?? session.activeTurn();\n return frame === undefined || frame.closed ? undefined : frame;\n }\n\n has(key: string): boolean {\n return this.frames.has(key);\n }\n\n private owns(frame: TurnFrame): boolean {\n for (const held of this.frames.values()) if (held.own && held.frame === frame) return true;\n return false;\n }\n\n /** Ends the run `key`: a turn the adapter opened closes; the calls it left open failed. */\n close(key: string, error?: unknown): void {\n const held = this.frames.get(key);\n if (held === undefined) return;\n this.frames.delete(key);\n for (const [callKey, call] of this.calls) {\n if (call.frame !== held.frame) continue;\n this.calls.delete(callKey);\n if (held.own) call.failed(error ?? \"error\");\n }\n if (held.own) held.frame.close(error);\n }\n\n /** A tool call starting, keyed by `callKey`; `callId` is the provider's id of the call, when there is one. */\n toolStart(session: Session, callKey: string, name: string, args: unknown, options: { frameKey?: string; callId?: string | null } = {}): void {\n const frame = this.frame(session, options.frameKey);\n if (frame === undefined) return;\n try {\n this.calls.set(callKey, frame.toolCall(name, args, { callId: options.callId ?? null, adoptable: true }));\n } catch {\n frame.incomplete();\n }\n }\n\n /**\n * In a replay, how a framework tool the adapter stands in for answers: the recorded result of a call with the\n * same arguments, or empty (a divergence), never the live tool. `null` outside a replay, and for a function\n * wrapped with `tool()`, which answers itself.\n */\n replayed(session: Session, name: string, args: unknown, fn?: unknown, frameKey?: string): Played | null {\n const frame = this.enabled ? this.frame(session, frameKey) : session.activeTurn();\n if (frame?.playback == null || (fn !== undefined && recordedTool(fn) !== null)) return null;\n return frame.playback.answer(name, args, false);\n }\n\n toolEnd(callKey: string, result?: unknown, error?: unknown): void {\n const call = this.calls.get(callKey);\n if (call === undefined) return;\n this.calls.delete(callKey);\n try {\n if (error !== undefined) call.failed(error);\n else call.result(result);\n } catch {\n call.frame.incomplete();\n }\n }\n\n /** A model call that ended, with its tokens when the framework reported them. */\n model(\n session: Session,\n model: string | null | undefined,\n tokens: { in?: number | null; out?: number | null; cached?: number | null } = {},\n frameKey?: string,\n ): void {\n const frame = this.frame(session, frameKey);\n if (frame === undefined) return;\n try {\n frame.modelCall(model, { tokensIn: tokens.in ?? null, tokensOut: tokens.out ?? null, tokensCached: tokens.cached ?? 0 });\n } catch {\n frame.incomplete();\n }\n }\n}\n","/**\n * Helpers for the webhook adapters (ElevenLabs, Vapi, WhatsApp, Twilio): signatures with Web\n * Crypto and header lookup. Only web APIs, so the handlers run on Node 20+, Deno, Bun, Cloudflare\n * Workers and the Vercel Edge Runtime alike.\n */\n\nimport type { Handle } from \"../types/common.js\";\nimport type { ContextStamp } from \"../types/events.js\";\nimport type { Verification } from \"../types/vocabulary.js\";\n\n/**\n * What a call's first webhook learned, for the webhooks that come later in the same call (tool\n * calls, the end-of-call report): who the caller is, the level they proved and the context stamp.\n */\nexport interface CallRecord {\n subject: Handle;\n verification: Verification;\n stamp?: ContextStamp | null;\n}\n\n/**\n * Where the webhook adapters keep a `CallRecord` between requests, by the platform's call id.\n * The default lives in this process's memory; on serverless or several instances, pass one backed\n * by your key-value store.\n */\nexport interface CallStore {\n get(id: string): CallRecord | undefined | PromiseLike<CallRecord | undefined>;\n set(id: string, record: CallRecord): void | PromiseLike<void>;\n}\n\n/** An in-memory `CallStore` that keeps the most recent `max` calls. */\nexport function memoryCallStore(max = 10_000): CallStore {\n const calls = new Map<string, CallRecord>();\n return {\n get: (id) => calls.get(id),\n set: (id, record) => {\n calls.delete(id);\n calls.set(id, record);\n if (calls.size > max) {\n const oldest = calls.keys().next().value;\n if (oldest !== undefined) calls.delete(oldest);\n }\n },\n };\n}\n\n/** Request headers as a `Headers` object or a plain record (any case). */\nexport type HeadersLike = Headers | Record<string, string | string[] | undefined>;\n\n/** What a handler answers: the HTTP status and a JSON-serializable body (`null` for no body). */\nexport interface WebhookResponse<B = unknown> {\n status: number;\n body: B;\n}\n\nexport function header(headers: HeadersLike, name: string): string | undefined {\n if (typeof (headers as Headers).get === \"function\") return (headers as Headers).get(name) ?? undefined;\n const wanted = name.toLowerCase();\n for (const [key, value] of Object.entries(headers as Record<string, string | string[] | undefined>)) {\n if (key.toLowerCase() !== wanted) continue;\n return Array.isArray(value) ? value[0] : value;\n }\n return undefined;\n}\n\nfunction subtle(): SubtleCrypto {\n const crypto = (globalThis as { crypto?: Crypto }).crypto;\n if (!crypto?.subtle) throw new Error(\"Web Crypto is not available on this runtime\");\n return crypto.subtle;\n}\n\nconst encoder = new TextEncoder();\n\nexport async function hmac(hash: \"SHA-1\" | \"SHA-256\", secret: string, message: string): Promise<Uint8Array> {\n const key = await subtle().importKey(\"raw\", encoder.encode(secret), { name: \"HMAC\", hash }, false, [\"sign\"]);\n return new Uint8Array(await subtle().sign(\"HMAC\", key, encoder.encode(message)));\n}\n\nexport function hex(bytes: Uint8Array): string {\n let out = \"\";\n for (const byte of bytes) out += byte.toString(16).padStart(2, \"0\");\n return out;\n}\n\nexport function base64(bytes: Uint8Array): string {\n let binary = \"\";\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary);\n}\n\n/** Compares two strings in time that depends only on their length. */\nexport function safeEqual(a: string, b: string): boolean {\n const left = encoder.encode(a);\n const right = encoder.encode(b);\n if (left.length !== right.length) return false;\n let diff = 0;\n for (let i = 0; i < left.length; i++) diff |= (left[i] ?? 0) ^ (right[i] ?? 0);\n return diff === 0;\n}\n\n/** The raw body as text, whatever form the framework hands it in. */\nexport function bodyText(body: string | Uint8Array | ArrayBuffer): string {\n if (typeof body === \"string\") return body;\n return new TextDecoder().decode(body instanceof Uint8Array ? body : new Uint8Array(body));\n}\n\nexport function parseJson(text: string): unknown {\n try {\n return JSON.parse(text) as unknown;\n } catch {\n return undefined;\n }\n}\n","/**\n * Niadra for the WhatsApp Cloud API (Meta). Translates only: it checks and reads Meta's webhook,\n * gives you each inbound message with the customer's handle and the turn ready to record, and\n * records the agent's turn from the send API's answer. It never sends a message.\n *\n * @example\n * const { status, messages } = await readWhatsApp(await request.text(), request.headers, { appSecret });\n * for (const inbound of messages) {\n * const convo = niadra.conversation({ subject: inbound.subject, channel: \"whatsapp\", conversation_id: yourThreadId });\n * recordInbound(convo, inbound);\n * const ctx = await convo.context();\n * ...\n * recordOutbound(convo, reply, await sendResponse.json());\n * }\n */\n\nimport type { Conversation } from \"../conversation.js\";\nimport { handles } from \"../handles.js\";\nimport type { Handle } from \"../types/common.js\";\nimport type { Content } from \"../types/events.js\";\nimport { errorName, isRecord } from \"./shared.js\";\nimport { bodyText, header, hex, hmac, parseJson, safeEqual } from \"./webhook.js\";\nimport type { HeadersLike, WebhookResponse } from \"./webhook.js\";\n\nexport type { HeadersLike, WebhookResponse } from \"./webhook.js\";\n\n/** A media attachment as Meta reports it. Download it with the Graph API, then `niadra.uploadMedia()`. */\nexport interface WhatsAppMedia {\n /** Meta's media id. */\n id: string;\n mimeType?: string | undefined;\n filename?: string | undefined;\n}\n\n/** One inbound message, read from the webhook. */\nexport interface WhatsAppInbound {\n /** The customer, as `wa_id`. */\n subject: Handle;\n waId: string;\n /** The name on the customer's WhatsApp profile, when Meta sends it. */\n profileName?: string | undefined;\n /** Your business number that received the message. */\n phoneNumberId: string;\n /** Meta's message id (`wamid...`): the idempotency key of the turn. */\n messageId: string;\n /** Meta's message type: `text`, `image`, `audio`, `document`, `interactive`, ... */\n type: string;\n /** The text of the message, the caption of media, or the title of a button or list reply. */\n text: string;\n media?: WhatsAppMedia | undefined;\n occurredAt?: Date | undefined;\n /** The message this one replies to, when it is a reply. */\n replyTo?: string | undefined;\n}\n\nexport interface ReadResult extends WebhookResponse<null> {\n /** The inbound messages, empty for status updates and for requests that fail the signature. */\n messages: WhatsAppInbound[];\n}\n\nconst MEDIA = new Set([\"image\", \"audio\", \"video\", \"document\", \"sticker\", \"voice\"]);\nconst CONTENT: Record<string, Content[\"type\"]> = { image: \"image\", sticker: \"image\", audio: \"audio\", voice: \"audio\", video: \"file\", document: \"file\" };\n\n/** Checks `X-Hub-Signature-256: sha256=<hex HMAC-SHA256 of the raw body with the app secret>`. */\nexport async function validWhatsAppSignature(rawBody: string | Uint8Array | ArrayBuffer, signature: string | undefined, appSecret: string): Promise<boolean> {\n if (!signature || !appSecret || !signature.startsWith(\"sha256=\")) return false;\n const expected = `sha256=${hex(await hmac(\"SHA-256\", appSecret, bodyText(rawBody)))}`;\n return safeEqual(signature.toLowerCase(), expected);\n}\n\n/**\n * Answers Meta's subscription check (`GET` with `hub.mode`, `hub.verify_token`, `hub.challenge`):\n * the challenge when the token matches, 403 otherwise.\n */\nexport function whatsAppChallenge(query: URLSearchParams | Record<string, string | undefined>, verifyToken: string): WebhookResponse<string> {\n const get = (name: string): string | undefined =>\n query instanceof URLSearchParams ? (query.get(name) ?? undefined) : query[name];\n const token = get(\"hub.verify_token\");\n const challenge = get(\"hub.challenge\");\n if (get(\"hub.mode\") === \"subscribe\" && token !== undefined && verifyToken && safeEqual(token, verifyToken) && challenge) {\n return { status: 200, body: challenge };\n }\n return { status: 403, body: \"\" };\n}\n\n/**\n * Checks the signature and reads every inbound message of a webhook delivery. Answer Meta with\n * `status` at once (200, or 401 for a bad signature); a delivery that fails is not read.\n */\nexport async function readWhatsApp(\n rawBody: string | Uint8Array | ArrayBuffer,\n headers: HeadersLike,\n options: { appSecret: string },\n): Promise<ReadResult> {\n const text = bodyText(rawBody);\n if (!(await validWhatsAppSignature(text, header(headers, \"x-hub-signature-256\"), options.appSecret))) {\n return { status: 401, body: null, messages: [] };\n }\n return { status: 200, body: null, messages: parseWhatsApp(parseJson(text)) };\n}\n\n/** Reads the inbound messages of an already verified webhook payload. */\nexport function parseWhatsApp(payload: unknown): WhatsAppInbound[] {\n const messages: WhatsAppInbound[] = [];\n const entries = isRecord(payload) && Array.isArray(payload.entry) ? (payload.entry as unknown[]) : [];\n for (const entry of entries) {\n const changes = isRecord(entry) && Array.isArray(entry.changes) ? (entry.changes as unknown[]) : [];\n for (const change of changes) {\n const value = isRecord(change) && isRecord(change.value) ? change.value : null;\n if (!value || !Array.isArray(value.messages)) continue;\n const phoneNumberId = isRecord(value.metadata) ? str(value.metadata.phone_number_id) : undefined;\n const names = new Map<string, string>();\n for (const contact of Array.isArray(value.contacts) ? (value.contacts as unknown[]) : []) {\n const waId = isRecord(contact) ? str(contact.wa_id) : undefined;\n const name = isRecord(contact) && isRecord(contact.profile) ? str(contact.profile.name) : undefined;\n if (waId && name) names.set(waId, name);\n }\n for (const message of value.messages as unknown[]) {\n const inbound = isRecord(message) && phoneNumberId ? readMessage(message, phoneNumberId, names) : null;\n if (inbound) messages.push(inbound);\n }\n }\n }\n return messages;\n}\n\n/**\n * Records an inbound message as the customer's turn, with Meta's message id as the idempotency key,\n * so a redelivered webhook is harmless. For media, pass what `niadra.uploadMedia()` returned.\n */\nexport function recordInbound(\n conversation: Conversation,\n inbound: WhatsAppInbound,\n media?: { media_ref: string; media_sha256?: string | undefined },\n): string | null {\n try {\n const kind = CONTENT[inbound.type];\n if (media && kind) {\n return conversation.track({\n speaker: \"customer\",\n idempotency_key: inbound.messageId,\n ...(inbound.occurredAt ? { occurred_at: inbound.occurredAt } : {}),\n content: {\n type: kind,\n media_ref: media.media_ref,\n ...(media.media_sha256 ? { media_sha256: media.media_sha256 } : {}),\n ...(inbound.text ? { text: inbound.text } : {}),\n },\n });\n }\n if (!inbound.text) return null;\n return conversation.customer(inbound.text, {\n idempotency_key: inbound.messageId,\n ...(inbound.occurredAt ? { occurred_at: inbound.occurredAt } : {}),\n });\n } catch (error) {\n conversation.logger.warn(`could not record the WhatsApp message (${errorName(error)})`);\n return null;\n }\n}\n\n/**\n * Records the agent's turn after the send API accepted it, with the `wamid` Meta returned as the\n * idempotency key. `response` is the JSON of `POST /{phone_number_id}/messages`.\n */\nexport function recordOutbound(conversation: Conversation, text: string, response?: unknown): string | null {\n try {\n const messages = isRecord(response) && Array.isArray(response.messages) ? (response.messages as unknown[]) : [];\n const id = isRecord(messages[0]) ? str(messages[0].id) : undefined;\n return conversation.agent(text, id ? { idempotency_key: id } : {});\n } catch (error) {\n conversation.logger.warn(`could not record the agent's WhatsApp message (${errorName(error)})`);\n return null;\n }\n}\n\nfunction readMessage(message: Record<PropertyKey, unknown>, phoneNumberId: string, names: Map<string, string>): WhatsAppInbound | null {\n const waId = str(message.from);\n const id = str(message.id);\n const type = str(message.type) ?? \"unknown\";\n if (!waId || !id) return null;\n let text = \"\";\n let media: WhatsAppMedia | undefined;\n const body = message[type];\n if (type === \"text\" && isRecord(body)) text = str(body.body) ?? \"\";\n else if (type === \"button\" && isRecord(body)) text = str(body.text) ?? \"\";\n else if (type === \"interactive\" && isRecord(body)) {\n const reply = isRecord(body.button_reply) ? body.button_reply : isRecord(body.list_reply) ? body.list_reply : null;\n text = reply ? (str(reply.title) ?? \"\") : \"\";\n } else if (MEDIA.has(type) && isRecord(body)) {\n const mediaId = str(body.id);\n text = str(body.caption) ?? \"\";\n if (mediaId) media = { id: mediaId, mimeType: str(body.mime_type), filename: str(body.filename) };\n }\n const seconds = Number(message.timestamp);\n const context = isRecord(message.context) ? str(message.context.id) : undefined;\n return {\n subject: handles.waId(waId),\n waId,\n profileName: names.get(waId),\n phoneNumberId,\n messageId: id,\n type,\n text,\n media,\n occurredAt: Number.isFinite(seconds) && seconds > 0 ? new Date(seconds * 1000) : undefined,\n replyTo: context,\n };\n}\n\nfunction str(value: unknown): string | undefined {\n return typeof value === \"string\" && value.length > 0 ? value : undefined;\n}\n"]}
|
package/dist/whatsapp.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { g as Handle,
|
|
2
|
-
import { W as WebhookResponse, H as HeadersLike } from './webhook-
|
|
1
|
+
import { g as Handle, bx as Conversation } from './client-7mkrdRI0.cjs';
|
|
2
|
+
import { W as WebhookResponse, H as HeadersLike } from './webhook-DE3HsGkb.cjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Niadra for the WhatsApp Cloud API (Meta). Translates only: it checks and reads Meta's webhook,
|
package/dist/whatsapp.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { g as Handle,
|
|
2
|
-
import { W as WebhookResponse, H as HeadersLike } from './webhook-
|
|
1
|
+
import { g as Handle, bx as Conversation } from './client-7mkrdRI0.js';
|
|
2
|
+
import { W as WebhookResponse, H as HeadersLike } from './webhook-QxYgxE6_.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Niadra for the WhatsApp Cloud API (Meta). Translates only: it checks and reads Meta's webhook,
|
package/dist/whatsapp.js
CHANGED
|
@@ -19,9 +19,17 @@ var handles = {
|
|
|
19
19
|
appUserId: (value, options) => build("app_user_id", value, void 0, options),
|
|
20
20
|
/** The id of a person or organization in a system of record; `system` names that system, such as `crm`. */
|
|
21
21
|
systemId: (value, system, options) => build("system_id", value, system, options),
|
|
22
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* A person's national document, such as a CPF: `govIdHmac("529.982.247-25", "BR")`. Send the number
|
|
24
|
+
* itself, never a hash of it: the server checks its check digits, then keeps only a keyed hash with your
|
|
25
|
+
* space's secret and an encrypted copy, and shows it masked. `country` is its ISO 3166-1 alpha-2 code.
|
|
26
|
+
*/
|
|
23
27
|
govIdHmac: (value, country, options) => build("gov_id_hmac", value, country, options),
|
|
24
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* A company's registry number, such as a CNPJ: `orgRegistryHmac("11.222.333/0001-81", "BR")`. As with
|
|
30
|
+
* `govIdHmac`, send the number itself; the server checks it and keeps only a keyed hash. Identifies an
|
|
31
|
+
* organization.
|
|
32
|
+
*/
|
|
25
33
|
orgRegistryHmac: (value, country) => build("org_registry_hmac", value, country, { subjectKind: "account" }),
|
|
26
34
|
/** An e-mail domain, such as `acme.com`. Identifies an organization. */
|
|
27
35
|
emailDomain: (domain) => build("email_domain", domain, void 0, { subjectKind: "account" }),
|
package/dist/whatsapp.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/handles.ts","../src/integrations/shared.ts","../src/integrations/webhook.ts","../src/integrations/whatsapp.ts"],"names":[],"mappings":"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","file":"whatsapp.js","sourcesContent":["import { NiadraValidationError } from \"./errors.js\";\nimport type { Handle, ObjectRef } from \"./types/common.js\";\nimport type { SubjectKind } from \"./types/vocabulary.js\";\n\ninterface HandleOptions {\n /** Overrides the subject kind the server would infer from the handle type. */\n subjectKind?: SubjectKind;\n}\n\nfunction build(type: Handle[\"type\"], value: string, scope?: string, options?: HandleOptions): Handle {\n const handle: Handle = { type, value };\n if (scope !== undefined) handle.scope = scope;\n if (options?.subjectKind !== undefined) handle.subject_kind = options.subjectKind;\n return handle;\n}\n\n/**\n * Builders for the handle types the API accepts. They only assemble the object; the value is\n * normalized and validated on the server, which is the single source of truth for matching.\n *\n * @example\n * niadra.context({ subject: handles.phone(\"+5511987654321\"), conversation_id: \"wa-8812\" })\n */\nexport const handles = {\n /** A phone number in E.164 form, such as `+5511987654321`. */\n phone: (e164: string, options?: HandleOptions): Handle => build(\"phone_e164\", e164, undefined, options),\n email: (address: string, options?: HandleOptions): Handle => build(\"email\", address, undefined, options),\n /** The WhatsApp id (`wa_id`) the Cloud API reports for a contact. */\n waId: (value: string, options?: HandleOptions): Handle => build(\"wa_id\", value, undefined, options),\n waJid: (value: string, options?: HandleOptions): Handle => build(\"wa_jid\", value, undefined, options),\n waLid: (value: string, options?: HandleOptions): Handle => build(\"wa_lid\", value, undefined, options),\n /** A business-scoped WhatsApp user id; `businessAccount` is the WhatsApp Business account it belongs to. */\n waBsuid: (value: string, businessAccount: string, options?: HandleOptions): Handle =>\n build(\"wa_bsuid\", value, businessAccount, options),\n /** The user id in your own app or site. */\n appUserId: (value: string, options?: HandleOptions): Handle => build(\"app_user_id\", value, undefined, options),\n /** The id of a person or organization in a system of record; `system` names that system, such as `crm`. */\n systemId: (value: string, system: string, options?: HandleOptions): Handle =>\n build(\"system_id\", value, system, options),\n /** An HMAC of a national document number; `country` is its ISO 3166-1 alpha-2 code. */\n govIdHmac: (value: string, country: string, options?: HandleOptions): Handle =>\n build(\"gov_id_hmac\", value, country, options),\n /** An HMAC of a company registry number. Identifies an organization. */\n orgRegistryHmac: (value: string, country: string): Handle =>\n build(\"org_registry_hmac\", value, country, { subjectKind: \"account\" }),\n /** An e-mail domain, such as `acme.com`. Identifies an organization. */\n emailDomain: (domain: string): Handle => build(\"email_domain\", domain, undefined, { subjectKind: \"account\" }),\n /** An anonymous visitor or device id, before the person is known. */\n anonId: (value: string, options?: HandleOptions): Handle => build(\"anon_id\", value, undefined, options),\n} as const;\n\n/**\n * Accepts an `ObjectRef` or the `type:namespace:id` shorthand. The id may itself contain\n * colons (`invoice:erp:2026:0823`); only the first two separate the parts.\n */\nexport function toObjectRef(object: ObjectRef | string): ObjectRef {\n if (typeof object !== \"string\") return object;\n const first = object.indexOf(\":\");\n const second = first < 0 ? -1 : object.indexOf(\":\", first + 1);\n if (first <= 0 || second <= first + 1 || second === object.length - 1) {\n throw new NiadraValidationError(\"object must look like `type:namespace:id`\");\n }\n return { type: object.slice(0, first), namespace: object.slice(first + 1, second), id: object.slice(second + 1) };\n}\n","/**\n * What every framework adapter shares: the five primitives (context before the model call, the\n * turns, the tools, the verification and the handoff) wired the same way, and fail-open.\n *\n * Nothing here throws into the framework: a failed read gives an empty context, a failed write\n * is logged without content, and the agent keeps working without memory.\n */\n\nimport { recordedTool } from \"../capture/tool.js\";\nimport type { Played } from \"../replay/playback.js\";\nimport type { AgentMemoryParams } from \"../agent-memory.js\";\nimport type { TurnParams } from \"../agent-session.js\";\nimport type { CallCapture, TurnFrame } from \"../capture/frame.js\";\nimport type { ContextOptions, ContextResult } from \"../context.js\";\nimport { emptyResult } from \"../context.js\";\nimport type { Conversation, TurnOptions } from \"../conversation.js\";\nimport type { Logger } from \"../logger.js\";\nimport type { Task } from \"../task.js\";\nimport type { BoundTools } from \"../tools.js\";\nimport type { Handle } from \"../types/common.js\";\nimport type { Verification, VerifyMethod } from \"../types/vocabulary.js\";\n\n/** A customer conversation or an internal agent's task. */\nexport type Session = Conversation | Task;\n\n/** A session, or a function that finds the one the current call belongs to (`null` passes the call through). */\nexport type SessionResolver<S extends Session = Session> = S | (() => S | null | undefined);\n\n/** What the framework proved about the person, recorded with `verify()` before the first context read. */\nexport interface Proof {\n method: VerifyMethod;\n level: Verification;\n /** Defaults to the session's subject. */\n handle?: Handle;\n}\n\n/** A proof, or a function that reads it from the framework when the first context is needed. */\nexport type ProofSource =\n | Proof\n | null\n | undefined\n | (() => Proof | null | undefined | PromiseLike<Proof | null | undefined>);\n\n/**\n * The agent's own working notes: `true` puts the block before the customer's context and offers\n * `search_agent_memory`; `{ write: true }` also offers `remember` (for a key with the\n * `agent_memory:write` scope); the other fields go to `agentMemory()`.\n */\nexport type AgentMemoryOption = boolean | (AgentMemoryParams & { write?: boolean });\n\n/** What goes into the prompt for one model call. */\nexport interface Read {\n context: ContextResult;\n /** The agent's own notes, or an empty string. */\n memory: string;\n /** What goes after the instructions: the notes, then the customer's pack. */\n prefix: string;\n /** What goes at the end: live turns, the turn's slots and deltas. */\n suffix: string;\n}\n\n/** A tool as every framework needs it: the name, the text for the model, the JSON Schema and the call. */\ninterface ToolSpec {\n name: string;\n description: string;\n /** JSON Schema of the arguments. None of them names the customer. */\n parameters: Record<string, unknown>;\n /** Runs the call and returns the text for the model. Never rejects in fail-open mode. */\n execute(args: string | Record<string, unknown>): Promise<string>;\n}\n\nconst ATTESTED: Record<string, Verification> = { A: \"V2\", B: \"V1\", C: \"V1\" };\n\n/**\n * The proof a carrier's STIR/SHAKEN attestation gives a phone call: `A` proves V2, `B` and `C`\n * prove V1, anything else proves nothing (`null`). Accepts the bare letter or the forms carriers\n * and platforms send, such as Twilio's `TN-Validation-Passed-A`.\n */\nexport function attestationProof(attestation: string | null | undefined): Proof | null {\n if (!attestation) return null;\n const value = attestation.trim().toUpperCase();\n if (value.includes(\"FAILED\") || value.startsWith(\"NO-\")) return null;\n const letter = value.length === 1 ? value : /PASSED-([ABC])$/.exec(value)?.[1];\n const level = letter ? ATTESTED[letter] : undefined;\n return level ? { method: \"network_attestation\", level } : null;\n}\n\nexport function resolveSession<S extends Session>(source: SessionResolver<S>): S | null {\n if (typeof source !== \"function\") return source;\n try {\n return source() ?? null;\n } catch {\n return null;\n }\n}\n\nfunction isConversation(session: Session): session is Conversation {\n return \"customer\" in session && typeof session.customer === \"function\";\n}\n\n/** A phone number as an E.164 handle, or `null` when it does not look like one. */\nexport function phoneHandle(value: string | null | undefined): Handle | null {\n const raw = value?.trim().replace(/^tel:/i, \"\").replace(/[\\s().-]/g, \"\");\n if (!raw) return null;\n const e164 = raw.startsWith(\"+\") ? raw : `+${raw}`;\n return /^\\+[1-9]\\d{6,14}$/.test(e164) ? { type: \"phone_e164\", value: e164 } : null;\n}\n\nexport function errorName(error: unknown): string {\n return error instanceof Error ? error.name : typeof error;\n}\n\n/**\n * One session as an adapter drives it. Keeps the verification to a single call made before the\n * first context read, and turns every failure into a log line.\n */\nexport class Bridge {\n private proving: Promise<void> | null = null;\n /** `begin()` started the first read of a call: the first `context()` waits for it with `ready()`. */\n private begun = false;\n private opened = false;\n\n constructor(\n readonly session: Session,\n private readonly proof?: ProofSource,\n private readonly memory: AgentMemoryOption = false,\n ) {}\n\n /**\n * The context and, when enabled, the agent's notes, read side by side within the same budget.\n * Never rejects.\n */\n async read(options: ContextOptions = {}): Promise<Read> {\n const memoryParams = this.memory === false ? null : this.memory === true ? {} : withoutWrite(this.memory);\n const [context, memory] = await Promise.all([\n this.context(options),\n memoryParams ? this.agentMemory(memoryParams, options) : Promise.resolve(\"\"),\n ]);\n return { context, memory, prefix: [memory, context.text].filter(Boolean).join(\"\\n\\n\"), suffix: context.suffix };\n }\n\n get logger(): Logger {\n return this.session.logger;\n }\n\n /**\n * Starts the first read of a call now, in the background, after the proof when one was given (the\n * pack depends on the level it proves): at call start, so the read runs while the call is set up.\n * The first `context()` then waits for it within `timeouts.contextVoiceStart`. Never throws.\n */\n begin(): void {\n const session = this.session;\n if (!isConversation(session)) return;\n const start = (): void => {\n this.safe(\"start the first read\", () => {\n // False off the voice read path (a chat view, the cache off): reads go on as before.\n this.begun = session.begin();\n });\n };\n if (this.proof === undefined || this.proof === null) start();\n else void this.verifyOnce().then(start);\n }\n\n /** Verifies once (when a proof was given), then reads the context. Never rejects. */\n async context(options: ContextOptions = {}): Promise<ContextResult> {\n await this.verifyOnce();\n try {\n const session = this.session;\n if (this.begun && !this.opened && isConversation(session)) {\n this.opened = true;\n // The call's first read, begun at call start: waited for within its own, longer budget.\n await session.ready(options.timeout === undefined ? {} : { timeout: options.timeout });\n }\n return await this.session.context(options);\n } catch (error) {\n this.logger.warn(`could not read context (${errorName(error)})`);\n return emptyResult(null);\n }\n }\n\n /** Records that `context` went into the prompt, unless it was empty for a reason other than the holdout. */\n injected(context: ContextResult): void {\n if (!context.text && !context.suffix && context.response?.path !== \"holdout\") return;\n this.safe(\"mark the injection\", () => {\n this.session.markInjected(context);\n });\n }\n\n customer(text: string, options: TurnOptions = {}): void {\n const session = this.session;\n if (!text.trim() || !isConversation(session)) return;\n this.safe(\"record the customer's turn\", () => session.customer(text, options));\n }\n\n /** Sends the customer's turn so far, while they speak. In the background; never throws. */\n prefetch(text: string): void {\n const session = this.session;\n if (!text.trim() || !isConversation(session)) return;\n this.safe(\"prefetch the turn\", () => session.prefetch(text));\n }\n\n agent(text: string, options: TurnOptions = {}): void {\n if (!text.trim()) return;\n this.safe(\"record the agent's turn\", () => this.session.agent(text, options));\n }\n\n /** Records a transfer. Tasks have no transfer; the call is ignored for them. */\n async handoff(target: \"agent\" | \"human\", reason?: string): Promise<void> {\n const session = this.session;\n if (!isConversation(session)) return;\n try {\n await session.handoff(reason ? { target, reason } : { target });\n } catch (error) {\n this.logger.warn(`could not record the handoff (${errorName(error)})`);\n }\n }\n\n async end(): Promise<void> {\n try {\n await this.session.end();\n } catch (error) {\n this.logger.warn(`could not end the session (${errorName(error)})`);\n }\n }\n\n /** The navigation kit as plain specs (with the agent's memory tools when enabled), or none for a task about an object only. */\n tools(): ToolSpec[] {\n let bound: BoundTools | null;\n try {\n const memory = this.memory;\n bound = this.session.tools(memory === false ? {} : { agentMemory: true, writeAgentMemory: memory !== true && memory.write === true });\n } catch (error) {\n this.logger.warn(`could not build the tools (${errorName(error)})`);\n return [];\n }\n return bound ? toolSpecs(bound) : [];\n }\n\n private async agentMemory(params: AgentMemoryParams, options: ContextOptions): Promise<string> {\n try {\n const result = await this.session.agentMemory(params, options.timeout === undefined ? {} : { timeout: options.timeout });\n return result.text;\n } catch (error) {\n this.logger.warn(`could not read the agent's memory (${errorName(error)})`);\n return \"\";\n }\n }\n\n private async verifyOnce(): Promise<void> {\n if (this.proof === undefined || this.proof === null) return;\n this.proving ??= this.runVerify();\n await this.proving;\n }\n\n private async runVerify(): Promise<void> {\n try {\n const source = this.proof;\n const proof = typeof source === \"function\" ? await source() : source;\n if (!proof) return;\n await this.session.verify(proof);\n } catch (error) {\n this.logger.warn(`could not record the verification (${errorName(error)})`);\n }\n }\n\n private safe(what: string, run: () => unknown): void {\n try {\n run();\n } catch (error) {\n this.logger.warn(`could not ${what} (${errorName(error)})`);\n }\n }\n}\n\nfunction withoutWrite(option: AgentMemoryParams & { write?: boolean }): AgentMemoryParams {\n const { write: _, ...params } = option;\n return params;\n}\n\n/** The bound kit as specs. The customer stays inside `bound`; no parameter names them. */\nfunction toolSpecs(bound: BoundTools): ToolSpec[] {\n return bound.definitions.map(({ function: definition }) => ({\n name: definition.name,\n description: definition.description,\n parameters: definition.parameters,\n execute: (args) => bound.call(definition.name, args),\n }));\n}\n\n/** The text of a message content in the shapes model APIs use: a string, or a list of text parts. */\nexport function textOf(content: unknown): string {\n if (typeof content === \"string\") return content;\n if (!Array.isArray(content)) return \"\";\n const parts: string[] = [];\n for (const part of content as unknown[]) {\n if (typeof part === \"string\") parts.push(part);\n else if (isRecord(part) && typeof part.text === \"string\" && (part.type === undefined || part.type === \"text\")) {\n parts.push(part.text);\n }\n }\n return parts.join(\"\");\n}\n\nexport function isRecord(value: unknown): value is Record<PropertyKey, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\n/** A non-negative integer, or `null`. */\nexport function count(value: unknown): number | null {\n return typeof value === \"number\" && Number.isInteger(value) && value >= 0 ? value : null;\n}\n\n/**\n * The turn records an adapter keeps from its framework's callbacks, keyed by the framework's own ids (a\n * run, a request, a tool call id). A run starting while the session has a turn in progress (yours, from\n * `conversation.turn()`, or a replay's) records into it; otherwise the adapter opens its own turn and closes\n * it when the run ends. A tool call recorded here is taken over by a `tool()` wrapper running inside it, so\n * the call is recorded once. Every method does nothing when turns are off and never throws.\n */\nexport class TurnHooks {\n /** Runs held open at most; past it the oldest run's turn closes, incomplete (a run whose end never came). */\n static readonly MAX_OPEN = 100;\n private readonly frames = new Map<string, { frame: TurnFrame; own: boolean }>();\n private readonly calls = new Map<string, CallCapture>();\n\n constructor(\n readonly adapter: string,\n readonly enabled: boolean,\n ) {}\n\n /** The turn of the run `key`: the session's turn in progress, or a new one of its own. */\n open(session: Session, key: string, params: TurnParams = {}): TurnFrame | undefined {\n if (!this.enabled) return undefined;\n const held = this.frames.get(key);\n if (held !== undefined) return held.frame;\n try {\n const active = session.activeTurn();\n if (active !== undefined && !this.owns(active)) {\n this.frames.set(key, { frame: active, own: false });\n return active;\n }\n const frame = session.openTurn({ ...params, adapter: this.adapter });\n this.frames.set(key, { frame, own: true });\n if (this.frames.size > TurnHooks.MAX_OPEN) {\n const [oldest] = this.frames.keys();\n if (oldest !== undefined) {\n this.frames.get(oldest)?.frame.incomplete();\n this.close(oldest);\n }\n }\n return frame;\n } catch (error) {\n session.logger.warn(`could not open the turn (${errorName(error)})`);\n return undefined;\n }\n }\n\n /** The turn of `key`, else the session's turn in progress. */\n frame(session: Session, key?: string): TurnFrame | undefined {\n if (!this.enabled) return undefined;\n const held = key === undefined ? undefined : this.frames.get(key);\n const frame = held?.frame ?? session.activeTurn();\n return frame === undefined || frame.closed ? undefined : frame;\n }\n\n has(key: string): boolean {\n return this.frames.has(key);\n }\n\n private owns(frame: TurnFrame): boolean {\n for (const held of this.frames.values()) if (held.own && held.frame === frame) return true;\n return false;\n }\n\n /** Ends the run `key`: a turn the adapter opened closes; the calls it left open failed. */\n close(key: string, error?: unknown): void {\n const held = this.frames.get(key);\n if (held === undefined) return;\n this.frames.delete(key);\n for (const [callKey, call] of this.calls) {\n if (call.frame !== held.frame) continue;\n this.calls.delete(callKey);\n if (held.own) call.failed(error ?? \"error\");\n }\n if (held.own) held.frame.close(error);\n }\n\n /** A tool call starting, keyed by `callKey`; `callId` is the provider's id of the call, when there is one. */\n toolStart(session: Session, callKey: string, name: string, args: unknown, options: { frameKey?: string; callId?: string | null } = {}): void {\n const frame = this.frame(session, options.frameKey);\n if (frame === undefined) return;\n try {\n this.calls.set(callKey, frame.toolCall(name, args, { callId: options.callId ?? null, adoptable: true }));\n } catch {\n frame.incomplete();\n }\n }\n\n /**\n * In a replay, how a framework tool the adapter stands in for answers: the recorded result of a call with the\n * same arguments, or empty (a divergence), never the live tool. `null` outside a replay, and for a function\n * wrapped with `tool()`, which answers itself.\n */\n replayed(session: Session, name: string, args: unknown, fn?: unknown, frameKey?: string): Played | null {\n const frame = this.enabled ? this.frame(session, frameKey) : session.activeTurn();\n if (frame?.playback == null || (fn !== undefined && recordedTool(fn) !== null)) return null;\n return frame.playback.answer(name, args, false);\n }\n\n toolEnd(callKey: string, result?: unknown, error?: unknown): void {\n const call = this.calls.get(callKey);\n if (call === undefined) return;\n this.calls.delete(callKey);\n try {\n if (error !== undefined) call.failed(error);\n else call.result(result);\n } catch {\n call.frame.incomplete();\n }\n }\n\n /** A model call that ended, with its tokens when the framework reported them. */\n model(\n session: Session,\n model: string | null | undefined,\n tokens: { in?: number | null; out?: number | null; cached?: number | null } = {},\n frameKey?: string,\n ): void {\n const frame = this.frame(session, frameKey);\n if (frame === undefined) return;\n try {\n frame.modelCall(model, { tokensIn: tokens.in ?? null, tokensOut: tokens.out ?? null, tokensCached: tokens.cached ?? 0 });\n } catch {\n frame.incomplete();\n }\n }\n}\n","/**\n * Helpers for the webhook adapters (ElevenLabs, Vapi, WhatsApp, Twilio): signatures with Web\n * Crypto and header lookup. Only web APIs, so the handlers run on Node 20+, Deno, Bun, Cloudflare\n * Workers and the Vercel Edge Runtime alike.\n */\n\nimport type { Handle } from \"../types/common.js\";\nimport type { ContextStamp } from \"../types/events.js\";\nimport type { Verification } from \"../types/vocabulary.js\";\n\n/**\n * What a call's first webhook learned, for the webhooks that come later in the same call (tool\n * calls, the end-of-call report): who the caller is, the level they proved and the context stamp.\n */\nexport interface CallRecord {\n subject: Handle;\n verification: Verification;\n stamp?: ContextStamp | null;\n}\n\n/**\n * Where the webhook adapters keep a `CallRecord` between requests, by the platform's call id.\n * The default lives in this process's memory; on serverless or several instances, pass one backed\n * by your key-value store.\n */\nexport interface CallStore {\n get(id: string): CallRecord | undefined | PromiseLike<CallRecord | undefined>;\n set(id: string, record: CallRecord): void | PromiseLike<void>;\n}\n\n/** An in-memory `CallStore` that keeps the most recent `max` calls. */\nexport function memoryCallStore(max = 10_000): CallStore {\n const calls = new Map<string, CallRecord>();\n return {\n get: (id) => calls.get(id),\n set: (id, record) => {\n calls.delete(id);\n calls.set(id, record);\n if (calls.size > max) {\n const oldest = calls.keys().next().value;\n if (oldest !== undefined) calls.delete(oldest);\n }\n },\n };\n}\n\n/** Request headers as a `Headers` object or a plain record (any case). */\nexport type HeadersLike = Headers | Record<string, string | string[] | undefined>;\n\n/** What a handler answers: the HTTP status and a JSON-serializable body (`null` for no body). */\nexport interface WebhookResponse<B = unknown> {\n status: number;\n body: B;\n}\n\nexport function header(headers: HeadersLike, name: string): string | undefined {\n if (typeof (headers as Headers).get === \"function\") return (headers as Headers).get(name) ?? undefined;\n const wanted = name.toLowerCase();\n for (const [key, value] of Object.entries(headers as Record<string, string | string[] | undefined>)) {\n if (key.toLowerCase() !== wanted) continue;\n return Array.isArray(value) ? value[0] : value;\n }\n return undefined;\n}\n\nfunction subtle(): SubtleCrypto {\n const crypto = (globalThis as { crypto?: Crypto }).crypto;\n if (!crypto?.subtle) throw new Error(\"Web Crypto is not available on this runtime\");\n return crypto.subtle;\n}\n\nconst encoder = new TextEncoder();\n\nexport async function hmac(hash: \"SHA-1\" | \"SHA-256\", secret: string, message: string): Promise<Uint8Array> {\n const key = await subtle().importKey(\"raw\", encoder.encode(secret), { name: \"HMAC\", hash }, false, [\"sign\"]);\n return new Uint8Array(await subtle().sign(\"HMAC\", key, encoder.encode(message)));\n}\n\nexport function hex(bytes: Uint8Array): string {\n let out = \"\";\n for (const byte of bytes) out += byte.toString(16).padStart(2, \"0\");\n return out;\n}\n\nexport function base64(bytes: Uint8Array): string {\n let binary = \"\";\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary);\n}\n\n/** Compares two strings in time that depends only on their length. */\nexport function safeEqual(a: string, b: string): boolean {\n const left = encoder.encode(a);\n const right = encoder.encode(b);\n if (left.length !== right.length) return false;\n let diff = 0;\n for (let i = 0; i < left.length; i++) diff |= (left[i] ?? 0) ^ (right[i] ?? 0);\n return diff === 0;\n}\n\n/** The raw body as text, whatever form the framework hands it in. */\nexport function bodyText(body: string | Uint8Array | ArrayBuffer): string {\n if (typeof body === \"string\") return body;\n return new TextDecoder().decode(body instanceof Uint8Array ? body : new Uint8Array(body));\n}\n\nexport function parseJson(text: string): unknown {\n try {\n return JSON.parse(text) as unknown;\n } catch {\n return undefined;\n }\n}\n","/**\n * Niadra for the WhatsApp Cloud API (Meta). Translates only: it checks and reads Meta's webhook,\n * gives you each inbound message with the customer's handle and the turn ready to record, and\n * records the agent's turn from the send API's answer. It never sends a message.\n *\n * @example\n * const { status, messages } = await readWhatsApp(await request.text(), request.headers, { appSecret });\n * for (const inbound of messages) {\n * const convo = niadra.conversation({ subject: inbound.subject, channel: \"whatsapp\", conversation_id: yourThreadId });\n * recordInbound(convo, inbound);\n * const ctx = await convo.context();\n * ...\n * recordOutbound(convo, reply, await sendResponse.json());\n * }\n */\n\nimport type { Conversation } from \"../conversation.js\";\nimport { handles } from \"../handles.js\";\nimport type { Handle } from \"../types/common.js\";\nimport type { Content } from \"../types/events.js\";\nimport { errorName, isRecord } from \"./shared.js\";\nimport { bodyText, header, hex, hmac, parseJson, safeEqual } from \"./webhook.js\";\nimport type { HeadersLike, WebhookResponse } from \"./webhook.js\";\n\nexport type { HeadersLike, WebhookResponse } from \"./webhook.js\";\n\n/** A media attachment as Meta reports it. Download it with the Graph API, then `niadra.uploadMedia()`. */\nexport interface WhatsAppMedia {\n /** Meta's media id. */\n id: string;\n mimeType?: string | undefined;\n filename?: string | undefined;\n}\n\n/** One inbound message, read from the webhook. */\nexport interface WhatsAppInbound {\n /** The customer, as `wa_id`. */\n subject: Handle;\n waId: string;\n /** The name on the customer's WhatsApp profile, when Meta sends it. */\n profileName?: string | undefined;\n /** Your business number that received the message. */\n phoneNumberId: string;\n /** Meta's message id (`wamid...`): the idempotency key of the turn. */\n messageId: string;\n /** Meta's message type: `text`, `image`, `audio`, `document`, `interactive`, ... */\n type: string;\n /** The text of the message, the caption of media, or the title of a button or list reply. */\n text: string;\n media?: WhatsAppMedia | undefined;\n occurredAt?: Date | undefined;\n /** The message this one replies to, when it is a reply. */\n replyTo?: string | undefined;\n}\n\nexport interface ReadResult extends WebhookResponse<null> {\n /** The inbound messages, empty for status updates and for requests that fail the signature. */\n messages: WhatsAppInbound[];\n}\n\nconst MEDIA = new Set([\"image\", \"audio\", \"video\", \"document\", \"sticker\", \"voice\"]);\nconst CONTENT: Record<string, Content[\"type\"]> = { image: \"image\", sticker: \"image\", audio: \"audio\", voice: \"audio\", video: \"file\", document: \"file\" };\n\n/** Checks `X-Hub-Signature-256: sha256=<hex HMAC-SHA256 of the raw body with the app secret>`. */\nexport async function validWhatsAppSignature(rawBody: string | Uint8Array | ArrayBuffer, signature: string | undefined, appSecret: string): Promise<boolean> {\n if (!signature || !appSecret || !signature.startsWith(\"sha256=\")) return false;\n const expected = `sha256=${hex(await hmac(\"SHA-256\", appSecret, bodyText(rawBody)))}`;\n return safeEqual(signature.toLowerCase(), expected);\n}\n\n/**\n * Answers Meta's subscription check (`GET` with `hub.mode`, `hub.verify_token`, `hub.challenge`):\n * the challenge when the token matches, 403 otherwise.\n */\nexport function whatsAppChallenge(query: URLSearchParams | Record<string, string | undefined>, verifyToken: string): WebhookResponse<string> {\n const get = (name: string): string | undefined =>\n query instanceof URLSearchParams ? (query.get(name) ?? undefined) : query[name];\n const token = get(\"hub.verify_token\");\n const challenge = get(\"hub.challenge\");\n if (get(\"hub.mode\") === \"subscribe\" && token !== undefined && verifyToken && safeEqual(token, verifyToken) && challenge) {\n return { status: 200, body: challenge };\n }\n return { status: 403, body: \"\" };\n}\n\n/**\n * Checks the signature and reads every inbound message of a webhook delivery. Answer Meta with\n * `status` at once (200, or 401 for a bad signature); a delivery that fails is not read.\n */\nexport async function readWhatsApp(\n rawBody: string | Uint8Array | ArrayBuffer,\n headers: HeadersLike,\n options: { appSecret: string },\n): Promise<ReadResult> {\n const text = bodyText(rawBody);\n if (!(await validWhatsAppSignature(text, header(headers, \"x-hub-signature-256\"), options.appSecret))) {\n return { status: 401, body: null, messages: [] };\n }\n return { status: 200, body: null, messages: parseWhatsApp(parseJson(text)) };\n}\n\n/** Reads the inbound messages of an already verified webhook payload. */\nexport function parseWhatsApp(payload: unknown): WhatsAppInbound[] {\n const messages: WhatsAppInbound[] = [];\n const entries = isRecord(payload) && Array.isArray(payload.entry) ? (payload.entry as unknown[]) : [];\n for (const entry of entries) {\n const changes = isRecord(entry) && Array.isArray(entry.changes) ? (entry.changes as unknown[]) : [];\n for (const change of changes) {\n const value = isRecord(change) && isRecord(change.value) ? change.value : null;\n if (!value || !Array.isArray(value.messages)) continue;\n const phoneNumberId = isRecord(value.metadata) ? str(value.metadata.phone_number_id) : undefined;\n const names = new Map<string, string>();\n for (const contact of Array.isArray(value.contacts) ? (value.contacts as unknown[]) : []) {\n const waId = isRecord(contact) ? str(contact.wa_id) : undefined;\n const name = isRecord(contact) && isRecord(contact.profile) ? str(contact.profile.name) : undefined;\n if (waId && name) names.set(waId, name);\n }\n for (const message of value.messages as unknown[]) {\n const inbound = isRecord(message) && phoneNumberId ? readMessage(message, phoneNumberId, names) : null;\n if (inbound) messages.push(inbound);\n }\n }\n }\n return messages;\n}\n\n/**\n * Records an inbound message as the customer's turn, with Meta's message id as the idempotency key,\n * so a redelivered webhook is harmless. For media, pass what `niadra.uploadMedia()` returned.\n */\nexport function recordInbound(\n conversation: Conversation,\n inbound: WhatsAppInbound,\n media?: { media_ref: string; media_sha256?: string | undefined },\n): string | null {\n try {\n const kind = CONTENT[inbound.type];\n if (media && kind) {\n return conversation.track({\n speaker: \"customer\",\n idempotency_key: inbound.messageId,\n ...(inbound.occurredAt ? { occurred_at: inbound.occurredAt } : {}),\n content: {\n type: kind,\n media_ref: media.media_ref,\n ...(media.media_sha256 ? { media_sha256: media.media_sha256 } : {}),\n ...(inbound.text ? { text: inbound.text } : {}),\n },\n });\n }\n if (!inbound.text) return null;\n return conversation.customer(inbound.text, {\n idempotency_key: inbound.messageId,\n ...(inbound.occurredAt ? { occurred_at: inbound.occurredAt } : {}),\n });\n } catch (error) {\n conversation.logger.warn(`could not record the WhatsApp message (${errorName(error)})`);\n return null;\n }\n}\n\n/**\n * Records the agent's turn after the send API accepted it, with the `wamid` Meta returned as the\n * idempotency key. `response` is the JSON of `POST /{phone_number_id}/messages`.\n */\nexport function recordOutbound(conversation: Conversation, text: string, response?: unknown): string | null {\n try {\n const messages = isRecord(response) && Array.isArray(response.messages) ? (response.messages as unknown[]) : [];\n const id = isRecord(messages[0]) ? str(messages[0].id) : undefined;\n return conversation.agent(text, id ? { idempotency_key: id } : {});\n } catch (error) {\n conversation.logger.warn(`could not record the agent's WhatsApp message (${errorName(error)})`);\n return null;\n }\n}\n\nfunction readMessage(message: Record<PropertyKey, unknown>, phoneNumberId: string, names: Map<string, string>): WhatsAppInbound | null {\n const waId = str(message.from);\n const id = str(message.id);\n const type = str(message.type) ?? \"unknown\";\n if (!waId || !id) return null;\n let text = \"\";\n let media: WhatsAppMedia | undefined;\n const body = message[type];\n if (type === \"text\" && isRecord(body)) text = str(body.body) ?? \"\";\n else if (type === \"button\" && isRecord(body)) text = str(body.text) ?? \"\";\n else if (type === \"interactive\" && isRecord(body)) {\n const reply = isRecord(body.button_reply) ? body.button_reply : isRecord(body.list_reply) ? body.list_reply : null;\n text = reply ? (str(reply.title) ?? \"\") : \"\";\n } else if (MEDIA.has(type) && isRecord(body)) {\n const mediaId = str(body.id);\n text = str(body.caption) ?? \"\";\n if (mediaId) media = { id: mediaId, mimeType: str(body.mime_type), filename: str(body.filename) };\n }\n const seconds = Number(message.timestamp);\n const context = isRecord(message.context) ? str(message.context.id) : undefined;\n return {\n subject: handles.waId(waId),\n waId,\n profileName: names.get(waId),\n phoneNumberId,\n messageId: id,\n type,\n text,\n media,\n occurredAt: Number.isFinite(seconds) && seconds > 0 ? new Date(seconds * 1000) : undefined,\n replyTo: context,\n };\n}\n\nfunction str(value: unknown): string | undefined {\n return typeof value === \"string\" && value.length > 0 ? value : undefined;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/handles.ts","../src/integrations/shared.ts","../src/integrations/webhook.ts","../src/integrations/whatsapp.ts"],"names":[],"mappings":"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","file":"whatsapp.js","sourcesContent":["import { NiadraValidationError } from \"./errors.js\";\nimport type { Handle, ObjectRef } from \"./types/common.js\";\nimport type { SubjectKind } from \"./types/vocabulary.js\";\n\ninterface HandleOptions {\n /** Overrides the subject kind the server would infer from the handle type. */\n subjectKind?: SubjectKind;\n}\n\nfunction build(type: Handle[\"type\"], value: string, scope?: string, options?: HandleOptions): Handle {\n const handle: Handle = { type, value };\n if (scope !== undefined) handle.scope = scope;\n if (options?.subjectKind !== undefined) handle.subject_kind = options.subjectKind;\n return handle;\n}\n\n/**\n * Builders for the handle types the API accepts. They only assemble the object; the value is\n * normalized and validated on the server, which is the single source of truth for matching.\n *\n * @example\n * niadra.context({ subject: handles.phone(\"+5511987654321\"), conversation_id: \"wa-8812\" })\n */\nexport const handles = {\n /** A phone number in E.164 form, such as `+5511987654321`. */\n phone: (e164: string, options?: HandleOptions): Handle => build(\"phone_e164\", e164, undefined, options),\n email: (address: string, options?: HandleOptions): Handle => build(\"email\", address, undefined, options),\n /** The WhatsApp id (`wa_id`) the Cloud API reports for a contact. */\n waId: (value: string, options?: HandleOptions): Handle => build(\"wa_id\", value, undefined, options),\n waJid: (value: string, options?: HandleOptions): Handle => build(\"wa_jid\", value, undefined, options),\n waLid: (value: string, options?: HandleOptions): Handle => build(\"wa_lid\", value, undefined, options),\n /** A business-scoped WhatsApp user id; `businessAccount` is the WhatsApp Business account it belongs to. */\n waBsuid: (value: string, businessAccount: string, options?: HandleOptions): Handle =>\n build(\"wa_bsuid\", value, businessAccount, options),\n /** The user id in your own app or site. */\n appUserId: (value: string, options?: HandleOptions): Handle => build(\"app_user_id\", value, undefined, options),\n /** The id of a person or organization in a system of record; `system` names that system, such as `crm`. */\n systemId: (value: string, system: string, options?: HandleOptions): Handle =>\n build(\"system_id\", value, system, options),\n /**\n * A person's national document, such as a CPF: `govIdHmac(\"529.982.247-25\", \"BR\")`. Send the number\n * itself, never a hash of it: the server checks its check digits, then keeps only a keyed hash with your\n * space's secret and an encrypted copy, and shows it masked. `country` is its ISO 3166-1 alpha-2 code.\n */\n govIdHmac: (value: string, country: string, options?: HandleOptions): Handle =>\n build(\"gov_id_hmac\", value, country, options),\n /**\n * A company's registry number, such as a CNPJ: `orgRegistryHmac(\"11.222.333/0001-81\", \"BR\")`. As with\n * `govIdHmac`, send the number itself; the server checks it and keeps only a keyed hash. Identifies an\n * organization.\n */\n orgRegistryHmac: (value: string, country: string): Handle =>\n build(\"org_registry_hmac\", value, country, { subjectKind: \"account\" }),\n /** An e-mail domain, such as `acme.com`. Identifies an organization. */\n emailDomain: (domain: string): Handle => build(\"email_domain\", domain, undefined, { subjectKind: \"account\" }),\n /** An anonymous visitor or device id, before the person is known. */\n anonId: (value: string, options?: HandleOptions): Handle => build(\"anon_id\", value, undefined, options),\n} as const;\n\n/**\n * Accepts an `ObjectRef` or the `type:namespace:id` shorthand. The id may itself contain\n * colons (`invoice:erp:2026:0823`); only the first two separate the parts.\n */\nexport function toObjectRef(object: ObjectRef | string): ObjectRef {\n if (typeof object !== \"string\") return object;\n const first = object.indexOf(\":\");\n const second = first < 0 ? -1 : object.indexOf(\":\", first + 1);\n if (first <= 0 || second <= first + 1 || second === object.length - 1) {\n throw new NiadraValidationError(\"object must look like `type:namespace:id`\");\n }\n return { type: object.slice(0, first), namespace: object.slice(first + 1, second), id: object.slice(second + 1) };\n}\n","/**\n * What every framework adapter shares: the five primitives (context before the model call, the\n * turns, the tools, the verification and the handoff) wired the same way, and fail-open.\n *\n * Nothing here throws into the framework: a failed read gives an empty context, a failed write\n * is logged without content, and the agent keeps working without memory.\n */\n\nimport { recordedTool } from \"../capture/tool.js\";\nimport type { Played } from \"../replay/playback.js\";\nimport type { AgentMemoryParams } from \"../agent-memory.js\";\nimport type { TurnParams } from \"../agent-session.js\";\nimport type { CallCapture, TurnFrame } from \"../capture/frame.js\";\nimport type { ContextOptions, ContextResult } from \"../context.js\";\nimport { emptyResult } from \"../context.js\";\nimport type { Conversation, TurnOptions } from \"../conversation.js\";\nimport type { Logger } from \"../logger.js\";\nimport type { Task } from \"../task.js\";\nimport type { BoundTools } from \"../tools.js\";\nimport type { Handle } from \"../types/common.js\";\nimport type { Verification, VerifyMethod } from \"../types/vocabulary.js\";\n\n/** A customer conversation or an internal agent's task. */\nexport type Session = Conversation | Task;\n\n/** A session, or a function that finds the one the current call belongs to (`null` passes the call through). */\nexport type SessionResolver<S extends Session = Session> = S | (() => S | null | undefined);\n\n/** What the framework proved about the person, recorded with `verify()` before the first context read. */\nexport interface Proof {\n method: VerifyMethod;\n level: Verification;\n /** Defaults to the session's subject. */\n handle?: Handle;\n}\n\n/** A proof, or a function that reads it from the framework when the first context is needed. */\nexport type ProofSource =\n | Proof\n | null\n | undefined\n | (() => Proof | null | undefined | PromiseLike<Proof | null | undefined>);\n\n/**\n * The agent's own working notes: `true` puts the block before the customer's context and offers\n * `search_agent_memory`; `{ write: true }` also offers `remember` (for a key with the\n * `agent_memory:write` scope); the other fields go to `agentMemory()`.\n */\nexport type AgentMemoryOption = boolean | (AgentMemoryParams & { write?: boolean });\n\n/** What goes into the prompt for one model call. */\nexport interface Read {\n context: ContextResult;\n /** The agent's own notes, or an empty string. */\n memory: string;\n /** What goes after the instructions: the notes, then the customer's pack. */\n prefix: string;\n /** What goes at the end: live turns, the turn's slots and deltas. */\n suffix: string;\n}\n\n/** A tool as every framework needs it: the name, the text for the model, the JSON Schema and the call. */\ninterface ToolSpec {\n name: string;\n description: string;\n /** JSON Schema of the arguments. None of them names the customer. */\n parameters: Record<string, unknown>;\n /** Runs the call and returns the text for the model. Never rejects in fail-open mode. */\n execute(args: string | Record<string, unknown>): Promise<string>;\n}\n\nconst ATTESTED: Record<string, Verification> = { A: \"V2\", B: \"V1\", C: \"V1\" };\n\n/**\n * The proof a carrier's STIR/SHAKEN attestation gives a phone call: `A` proves V2, `B` and `C`\n * prove V1, anything else proves nothing (`null`). Accepts the bare letter or the forms carriers\n * and platforms send, such as Twilio's `TN-Validation-Passed-A`.\n */\nexport function attestationProof(attestation: string | null | undefined): Proof | null {\n if (!attestation) return null;\n const value = attestation.trim().toUpperCase();\n if (value.includes(\"FAILED\") || value.startsWith(\"NO-\")) return null;\n const letter = value.length === 1 ? value : /PASSED-([ABC])$/.exec(value)?.[1];\n const level = letter ? ATTESTED[letter] : undefined;\n return level ? { method: \"network_attestation\", level } : null;\n}\n\nexport function resolveSession<S extends Session>(source: SessionResolver<S>): S | null {\n if (typeof source !== \"function\") return source;\n try {\n return source() ?? null;\n } catch {\n return null;\n }\n}\n\nfunction isConversation(session: Session): session is Conversation {\n return \"customer\" in session && typeof session.customer === \"function\";\n}\n\n/** A phone number as an E.164 handle, or `null` when it does not look like one. */\nexport function phoneHandle(value: string | null | undefined): Handle | null {\n const raw = value?.trim().replace(/^tel:/i, \"\").replace(/[\\s().-]/g, \"\");\n if (!raw) return null;\n const e164 = raw.startsWith(\"+\") ? raw : `+${raw}`;\n return /^\\+[1-9]\\d{6,14}$/.test(e164) ? { type: \"phone_e164\", value: e164 } : null;\n}\n\nexport function errorName(error: unknown): string {\n return error instanceof Error ? error.name : typeof error;\n}\n\n/**\n * One session as an adapter drives it. Keeps the verification to a single call made before the\n * first context read, and turns every failure into a log line.\n */\nexport class Bridge {\n private proving: Promise<void> | null = null;\n /** `begin()` started the first read of a call: the first `context()` waits for it with `ready()`. */\n private begun = false;\n private opened = false;\n\n constructor(\n readonly session: Session,\n private readonly proof?: ProofSource,\n private readonly memory: AgentMemoryOption = false,\n ) {}\n\n /**\n * The context and, when enabled, the agent's notes, read side by side within the same budget.\n * Never rejects.\n */\n async read(options: ContextOptions = {}): Promise<Read> {\n const memoryParams = this.memory === false ? null : this.memory === true ? {} : withoutWrite(this.memory);\n const [context, memory] = await Promise.all([\n this.context(options),\n memoryParams ? this.agentMemory(memoryParams, options) : Promise.resolve(\"\"),\n ]);\n return { context, memory, prefix: [memory, context.text].filter(Boolean).join(\"\\n\\n\"), suffix: context.suffix };\n }\n\n get logger(): Logger {\n return this.session.logger;\n }\n\n /**\n * Starts the first read of a call now, in the background, after the proof when one was given (the\n * pack depends on the level it proves): at call start, so the read runs while the call is set up.\n * The first `context()` then waits for it within `timeouts.contextVoiceStart`. Never throws.\n */\n begin(): void {\n const session = this.session;\n if (!isConversation(session)) return;\n const start = (): void => {\n this.safe(\"start the first read\", () => {\n // False off the voice read path (a chat view, the cache off): reads go on as before.\n this.begun = session.begin();\n });\n };\n if (this.proof === undefined || this.proof === null) start();\n else void this.verifyOnce().then(start);\n }\n\n /** Verifies once (when a proof was given), then reads the context. Never rejects. */\n async context(options: ContextOptions = {}): Promise<ContextResult> {\n await this.verifyOnce();\n try {\n const session = this.session;\n if (this.begun && !this.opened && isConversation(session)) {\n this.opened = true;\n // The call's first read, begun at call start: waited for within its own, longer budget.\n await session.ready(options.timeout === undefined ? {} : { timeout: options.timeout });\n }\n return await this.session.context(options);\n } catch (error) {\n this.logger.warn(`could not read context (${errorName(error)})`);\n return emptyResult(null);\n }\n }\n\n /** Records that `context` went into the prompt, unless it was empty for a reason other than the holdout. */\n injected(context: ContextResult): void {\n if (!context.text && !context.suffix && context.response?.path !== \"holdout\") return;\n this.safe(\"mark the injection\", () => {\n this.session.markInjected(context);\n });\n }\n\n customer(text: string, options: TurnOptions = {}): void {\n const session = this.session;\n if (!text.trim() || !isConversation(session)) return;\n this.safe(\"record the customer's turn\", () => session.customer(text, options));\n }\n\n /** Sends the customer's turn so far, while they speak. In the background; never throws. */\n prefetch(text: string): void {\n const session = this.session;\n if (!text.trim() || !isConversation(session)) return;\n this.safe(\"prefetch the turn\", () => session.prefetch(text));\n }\n\n agent(text: string, options: TurnOptions = {}): void {\n if (!text.trim()) return;\n this.safe(\"record the agent's turn\", () => this.session.agent(text, options));\n }\n\n /** Records a transfer. Tasks have no transfer; the call is ignored for them. */\n async handoff(target: \"agent\" | \"human\", reason?: string): Promise<void> {\n const session = this.session;\n if (!isConversation(session)) return;\n try {\n await session.handoff(reason ? { target, reason } : { target });\n } catch (error) {\n this.logger.warn(`could not record the handoff (${errorName(error)})`);\n }\n }\n\n async end(): Promise<void> {\n try {\n await this.session.end();\n } catch (error) {\n this.logger.warn(`could not end the session (${errorName(error)})`);\n }\n }\n\n /** The navigation kit as plain specs (with the agent's memory tools when enabled), or none for a task about an object only. */\n tools(): ToolSpec[] {\n let bound: BoundTools | null;\n try {\n const memory = this.memory;\n bound = this.session.tools(memory === false ? {} : { agentMemory: true, writeAgentMemory: memory !== true && memory.write === true });\n } catch (error) {\n this.logger.warn(`could not build the tools (${errorName(error)})`);\n return [];\n }\n return bound ? toolSpecs(bound) : [];\n }\n\n private async agentMemory(params: AgentMemoryParams, options: ContextOptions): Promise<string> {\n try {\n const result = await this.session.agentMemory(params, options.timeout === undefined ? {} : { timeout: options.timeout });\n return result.text;\n } catch (error) {\n this.logger.warn(`could not read the agent's memory (${errorName(error)})`);\n return \"\";\n }\n }\n\n private async verifyOnce(): Promise<void> {\n if (this.proof === undefined || this.proof === null) return;\n this.proving ??= this.runVerify();\n await this.proving;\n }\n\n private async runVerify(): Promise<void> {\n try {\n const source = this.proof;\n const proof = typeof source === \"function\" ? await source() : source;\n if (!proof) return;\n await this.session.verify(proof);\n } catch (error) {\n this.logger.warn(`could not record the verification (${errorName(error)})`);\n }\n }\n\n private safe(what: string, run: () => unknown): void {\n try {\n run();\n } catch (error) {\n this.logger.warn(`could not ${what} (${errorName(error)})`);\n }\n }\n}\n\nfunction withoutWrite(option: AgentMemoryParams & { write?: boolean }): AgentMemoryParams {\n const { write: _, ...params } = option;\n return params;\n}\n\n/** The bound kit as specs. The customer stays inside `bound`; no parameter names them. */\nfunction toolSpecs(bound: BoundTools): ToolSpec[] {\n return bound.definitions.map(({ function: definition }) => ({\n name: definition.name,\n description: definition.description,\n parameters: definition.parameters,\n execute: (args) => bound.call(definition.name, args),\n }));\n}\n\n/** The text of a message content in the shapes model APIs use: a string, or a list of text parts. */\nexport function textOf(content: unknown): string {\n if (typeof content === \"string\") return content;\n if (!Array.isArray(content)) return \"\";\n const parts: string[] = [];\n for (const part of content as unknown[]) {\n if (typeof part === \"string\") parts.push(part);\n else if (isRecord(part) && typeof part.text === \"string\" && (part.type === undefined || part.type === \"text\")) {\n parts.push(part.text);\n }\n }\n return parts.join(\"\");\n}\n\nexport function isRecord(value: unknown): value is Record<PropertyKey, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\n/** A non-negative integer, or `null`. */\nexport function count(value: unknown): number | null {\n return typeof value === \"number\" && Number.isInteger(value) && value >= 0 ? value : null;\n}\n\n/**\n * The turn records an adapter keeps from its framework's callbacks, keyed by the framework's own ids (a\n * run, a request, a tool call id). A run starting while the session has a turn in progress (yours, from\n * `conversation.turn()`, or a replay's) records into it; otherwise the adapter opens its own turn and closes\n * it when the run ends. A tool call recorded here is taken over by a `tool()` wrapper running inside it, so\n * the call is recorded once. Every method does nothing when turns are off and never throws.\n */\nexport class TurnHooks {\n /** Runs held open at most; past it the oldest run's turn closes, incomplete (a run whose end never came). */\n static readonly MAX_OPEN = 100;\n private readonly frames = new Map<string, { frame: TurnFrame; own: boolean }>();\n private readonly calls = new Map<string, CallCapture>();\n\n constructor(\n readonly adapter: string,\n readonly enabled: boolean,\n ) {}\n\n /** The turn of the run `key`: the session's turn in progress, or a new one of its own. */\n open(session: Session, key: string, params: TurnParams = {}): TurnFrame | undefined {\n if (!this.enabled) return undefined;\n const held = this.frames.get(key);\n if (held !== undefined) return held.frame;\n try {\n const active = session.activeTurn();\n if (active !== undefined && !this.owns(active)) {\n this.frames.set(key, { frame: active, own: false });\n return active;\n }\n const frame = session.openTurn({ ...params, adapter: this.adapter });\n this.frames.set(key, { frame, own: true });\n if (this.frames.size > TurnHooks.MAX_OPEN) {\n const [oldest] = this.frames.keys();\n if (oldest !== undefined) {\n this.frames.get(oldest)?.frame.incomplete();\n this.close(oldest);\n }\n }\n return frame;\n } catch (error) {\n session.logger.warn(`could not open the turn (${errorName(error)})`);\n return undefined;\n }\n }\n\n /** The turn of `key`, else the session's turn in progress. */\n frame(session: Session, key?: string): TurnFrame | undefined {\n if (!this.enabled) return undefined;\n const held = key === undefined ? undefined : this.frames.get(key);\n const frame = held?.frame ?? session.activeTurn();\n return frame === undefined || frame.closed ? undefined : frame;\n }\n\n has(key: string): boolean {\n return this.frames.has(key);\n }\n\n private owns(frame: TurnFrame): boolean {\n for (const held of this.frames.values()) if (held.own && held.frame === frame) return true;\n return false;\n }\n\n /** Ends the run `key`: a turn the adapter opened closes; the calls it left open failed. */\n close(key: string, error?: unknown): void {\n const held = this.frames.get(key);\n if (held === undefined) return;\n this.frames.delete(key);\n for (const [callKey, call] of this.calls) {\n if (call.frame !== held.frame) continue;\n this.calls.delete(callKey);\n if (held.own) call.failed(error ?? \"error\");\n }\n if (held.own) held.frame.close(error);\n }\n\n /** A tool call starting, keyed by `callKey`; `callId` is the provider's id of the call, when there is one. */\n toolStart(session: Session, callKey: string, name: string, args: unknown, options: { frameKey?: string; callId?: string | null } = {}): void {\n const frame = this.frame(session, options.frameKey);\n if (frame === undefined) return;\n try {\n this.calls.set(callKey, frame.toolCall(name, args, { callId: options.callId ?? null, adoptable: true }));\n } catch {\n frame.incomplete();\n }\n }\n\n /**\n * In a replay, how a framework tool the adapter stands in for answers: the recorded result of a call with the\n * same arguments, or empty (a divergence), never the live tool. `null` outside a replay, and for a function\n * wrapped with `tool()`, which answers itself.\n */\n replayed(session: Session, name: string, args: unknown, fn?: unknown, frameKey?: string): Played | null {\n const frame = this.enabled ? this.frame(session, frameKey) : session.activeTurn();\n if (frame?.playback == null || (fn !== undefined && recordedTool(fn) !== null)) return null;\n return frame.playback.answer(name, args, false);\n }\n\n toolEnd(callKey: string, result?: unknown, error?: unknown): void {\n const call = this.calls.get(callKey);\n if (call === undefined) return;\n this.calls.delete(callKey);\n try {\n if (error !== undefined) call.failed(error);\n else call.result(result);\n } catch {\n call.frame.incomplete();\n }\n }\n\n /** A model call that ended, with its tokens when the framework reported them. */\n model(\n session: Session,\n model: string | null | undefined,\n tokens: { in?: number | null; out?: number | null; cached?: number | null } = {},\n frameKey?: string,\n ): void {\n const frame = this.frame(session, frameKey);\n if (frame === undefined) return;\n try {\n frame.modelCall(model, { tokensIn: tokens.in ?? null, tokensOut: tokens.out ?? null, tokensCached: tokens.cached ?? 0 });\n } catch {\n frame.incomplete();\n }\n }\n}\n","/**\n * Helpers for the webhook adapters (ElevenLabs, Vapi, WhatsApp, Twilio): signatures with Web\n * Crypto and header lookup. Only web APIs, so the handlers run on Node 20+, Deno, Bun, Cloudflare\n * Workers and the Vercel Edge Runtime alike.\n */\n\nimport type { Handle } from \"../types/common.js\";\nimport type { ContextStamp } from \"../types/events.js\";\nimport type { Verification } from \"../types/vocabulary.js\";\n\n/**\n * What a call's first webhook learned, for the webhooks that come later in the same call (tool\n * calls, the end-of-call report): who the caller is, the level they proved and the context stamp.\n */\nexport interface CallRecord {\n subject: Handle;\n verification: Verification;\n stamp?: ContextStamp | null;\n}\n\n/**\n * Where the webhook adapters keep a `CallRecord` between requests, by the platform's call id.\n * The default lives in this process's memory; on serverless or several instances, pass one backed\n * by your key-value store.\n */\nexport interface CallStore {\n get(id: string): CallRecord | undefined | PromiseLike<CallRecord | undefined>;\n set(id: string, record: CallRecord): void | PromiseLike<void>;\n}\n\n/** An in-memory `CallStore` that keeps the most recent `max` calls. */\nexport function memoryCallStore(max = 10_000): CallStore {\n const calls = new Map<string, CallRecord>();\n return {\n get: (id) => calls.get(id),\n set: (id, record) => {\n calls.delete(id);\n calls.set(id, record);\n if (calls.size > max) {\n const oldest = calls.keys().next().value;\n if (oldest !== undefined) calls.delete(oldest);\n }\n },\n };\n}\n\n/** Request headers as a `Headers` object or a plain record (any case). */\nexport type HeadersLike = Headers | Record<string, string | string[] | undefined>;\n\n/** What a handler answers: the HTTP status and a JSON-serializable body (`null` for no body). */\nexport interface WebhookResponse<B = unknown> {\n status: number;\n body: B;\n}\n\nexport function header(headers: HeadersLike, name: string): string | undefined {\n if (typeof (headers as Headers).get === \"function\") return (headers as Headers).get(name) ?? undefined;\n const wanted = name.toLowerCase();\n for (const [key, value] of Object.entries(headers as Record<string, string | string[] | undefined>)) {\n if (key.toLowerCase() !== wanted) continue;\n return Array.isArray(value) ? value[0] : value;\n }\n return undefined;\n}\n\nfunction subtle(): SubtleCrypto {\n const crypto = (globalThis as { crypto?: Crypto }).crypto;\n if (!crypto?.subtle) throw new Error(\"Web Crypto is not available on this runtime\");\n return crypto.subtle;\n}\n\nconst encoder = new TextEncoder();\n\nexport async function hmac(hash: \"SHA-1\" | \"SHA-256\", secret: string, message: string): Promise<Uint8Array> {\n const key = await subtle().importKey(\"raw\", encoder.encode(secret), { name: \"HMAC\", hash }, false, [\"sign\"]);\n return new Uint8Array(await subtle().sign(\"HMAC\", key, encoder.encode(message)));\n}\n\nexport function hex(bytes: Uint8Array): string {\n let out = \"\";\n for (const byte of bytes) out += byte.toString(16).padStart(2, \"0\");\n return out;\n}\n\nexport function base64(bytes: Uint8Array): string {\n let binary = \"\";\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary);\n}\n\n/** Compares two strings in time that depends only on their length. */\nexport function safeEqual(a: string, b: string): boolean {\n const left = encoder.encode(a);\n const right = encoder.encode(b);\n if (left.length !== right.length) return false;\n let diff = 0;\n for (let i = 0; i < left.length; i++) diff |= (left[i] ?? 0) ^ (right[i] ?? 0);\n return diff === 0;\n}\n\n/** The raw body as text, whatever form the framework hands it in. */\nexport function bodyText(body: string | Uint8Array | ArrayBuffer): string {\n if (typeof body === \"string\") return body;\n return new TextDecoder().decode(body instanceof Uint8Array ? body : new Uint8Array(body));\n}\n\nexport function parseJson(text: string): unknown {\n try {\n return JSON.parse(text) as unknown;\n } catch {\n return undefined;\n }\n}\n","/**\n * Niadra for the WhatsApp Cloud API (Meta). Translates only: it checks and reads Meta's webhook,\n * gives you each inbound message with the customer's handle and the turn ready to record, and\n * records the agent's turn from the send API's answer. It never sends a message.\n *\n * @example\n * const { status, messages } = await readWhatsApp(await request.text(), request.headers, { appSecret });\n * for (const inbound of messages) {\n * const convo = niadra.conversation({ subject: inbound.subject, channel: \"whatsapp\", conversation_id: yourThreadId });\n * recordInbound(convo, inbound);\n * const ctx = await convo.context();\n * ...\n * recordOutbound(convo, reply, await sendResponse.json());\n * }\n */\n\nimport type { Conversation } from \"../conversation.js\";\nimport { handles } from \"../handles.js\";\nimport type { Handle } from \"../types/common.js\";\nimport type { Content } from \"../types/events.js\";\nimport { errorName, isRecord } from \"./shared.js\";\nimport { bodyText, header, hex, hmac, parseJson, safeEqual } from \"./webhook.js\";\nimport type { HeadersLike, WebhookResponse } from \"./webhook.js\";\n\nexport type { HeadersLike, WebhookResponse } from \"./webhook.js\";\n\n/** A media attachment as Meta reports it. Download it with the Graph API, then `niadra.uploadMedia()`. */\nexport interface WhatsAppMedia {\n /** Meta's media id. */\n id: string;\n mimeType?: string | undefined;\n filename?: string | undefined;\n}\n\n/** One inbound message, read from the webhook. */\nexport interface WhatsAppInbound {\n /** The customer, as `wa_id`. */\n subject: Handle;\n waId: string;\n /** The name on the customer's WhatsApp profile, when Meta sends it. */\n profileName?: string | undefined;\n /** Your business number that received the message. */\n phoneNumberId: string;\n /** Meta's message id (`wamid...`): the idempotency key of the turn. */\n messageId: string;\n /** Meta's message type: `text`, `image`, `audio`, `document`, `interactive`, ... */\n type: string;\n /** The text of the message, the caption of media, or the title of a button or list reply. */\n text: string;\n media?: WhatsAppMedia | undefined;\n occurredAt?: Date | undefined;\n /** The message this one replies to, when it is a reply. */\n replyTo?: string | undefined;\n}\n\nexport interface ReadResult extends WebhookResponse<null> {\n /** The inbound messages, empty for status updates and for requests that fail the signature. */\n messages: WhatsAppInbound[];\n}\n\nconst MEDIA = new Set([\"image\", \"audio\", \"video\", \"document\", \"sticker\", \"voice\"]);\nconst CONTENT: Record<string, Content[\"type\"]> = { image: \"image\", sticker: \"image\", audio: \"audio\", voice: \"audio\", video: \"file\", document: \"file\" };\n\n/** Checks `X-Hub-Signature-256: sha256=<hex HMAC-SHA256 of the raw body with the app secret>`. */\nexport async function validWhatsAppSignature(rawBody: string | Uint8Array | ArrayBuffer, signature: string | undefined, appSecret: string): Promise<boolean> {\n if (!signature || !appSecret || !signature.startsWith(\"sha256=\")) return false;\n const expected = `sha256=${hex(await hmac(\"SHA-256\", appSecret, bodyText(rawBody)))}`;\n return safeEqual(signature.toLowerCase(), expected);\n}\n\n/**\n * Answers Meta's subscription check (`GET` with `hub.mode`, `hub.verify_token`, `hub.challenge`):\n * the challenge when the token matches, 403 otherwise.\n */\nexport function whatsAppChallenge(query: URLSearchParams | Record<string, string | undefined>, verifyToken: string): WebhookResponse<string> {\n const get = (name: string): string | undefined =>\n query instanceof URLSearchParams ? (query.get(name) ?? undefined) : query[name];\n const token = get(\"hub.verify_token\");\n const challenge = get(\"hub.challenge\");\n if (get(\"hub.mode\") === \"subscribe\" && token !== undefined && verifyToken && safeEqual(token, verifyToken) && challenge) {\n return { status: 200, body: challenge };\n }\n return { status: 403, body: \"\" };\n}\n\n/**\n * Checks the signature and reads every inbound message of a webhook delivery. Answer Meta with\n * `status` at once (200, or 401 for a bad signature); a delivery that fails is not read.\n */\nexport async function readWhatsApp(\n rawBody: string | Uint8Array | ArrayBuffer,\n headers: HeadersLike,\n options: { appSecret: string },\n): Promise<ReadResult> {\n const text = bodyText(rawBody);\n if (!(await validWhatsAppSignature(text, header(headers, \"x-hub-signature-256\"), options.appSecret))) {\n return { status: 401, body: null, messages: [] };\n }\n return { status: 200, body: null, messages: parseWhatsApp(parseJson(text)) };\n}\n\n/** Reads the inbound messages of an already verified webhook payload. */\nexport function parseWhatsApp(payload: unknown): WhatsAppInbound[] {\n const messages: WhatsAppInbound[] = [];\n const entries = isRecord(payload) && Array.isArray(payload.entry) ? (payload.entry as unknown[]) : [];\n for (const entry of entries) {\n const changes = isRecord(entry) && Array.isArray(entry.changes) ? (entry.changes as unknown[]) : [];\n for (const change of changes) {\n const value = isRecord(change) && isRecord(change.value) ? change.value : null;\n if (!value || !Array.isArray(value.messages)) continue;\n const phoneNumberId = isRecord(value.metadata) ? str(value.metadata.phone_number_id) : undefined;\n const names = new Map<string, string>();\n for (const contact of Array.isArray(value.contacts) ? (value.contacts as unknown[]) : []) {\n const waId = isRecord(contact) ? str(contact.wa_id) : undefined;\n const name = isRecord(contact) && isRecord(contact.profile) ? str(contact.profile.name) : undefined;\n if (waId && name) names.set(waId, name);\n }\n for (const message of value.messages as unknown[]) {\n const inbound = isRecord(message) && phoneNumberId ? readMessage(message, phoneNumberId, names) : null;\n if (inbound) messages.push(inbound);\n }\n }\n }\n return messages;\n}\n\n/**\n * Records an inbound message as the customer's turn, with Meta's message id as the idempotency key,\n * so a redelivered webhook is harmless. For media, pass what `niadra.uploadMedia()` returned.\n */\nexport function recordInbound(\n conversation: Conversation,\n inbound: WhatsAppInbound,\n media?: { media_ref: string; media_sha256?: string | undefined },\n): string | null {\n try {\n const kind = CONTENT[inbound.type];\n if (media && kind) {\n return conversation.track({\n speaker: \"customer\",\n idempotency_key: inbound.messageId,\n ...(inbound.occurredAt ? { occurred_at: inbound.occurredAt } : {}),\n content: {\n type: kind,\n media_ref: media.media_ref,\n ...(media.media_sha256 ? { media_sha256: media.media_sha256 } : {}),\n ...(inbound.text ? { text: inbound.text } : {}),\n },\n });\n }\n if (!inbound.text) return null;\n return conversation.customer(inbound.text, {\n idempotency_key: inbound.messageId,\n ...(inbound.occurredAt ? { occurred_at: inbound.occurredAt } : {}),\n });\n } catch (error) {\n conversation.logger.warn(`could not record the WhatsApp message (${errorName(error)})`);\n return null;\n }\n}\n\n/**\n * Records the agent's turn after the send API accepted it, with the `wamid` Meta returned as the\n * idempotency key. `response` is the JSON of `POST /{phone_number_id}/messages`.\n */\nexport function recordOutbound(conversation: Conversation, text: string, response?: unknown): string | null {\n try {\n const messages = isRecord(response) && Array.isArray(response.messages) ? (response.messages as unknown[]) : [];\n const id = isRecord(messages[0]) ? str(messages[0].id) : undefined;\n return conversation.agent(text, id ? { idempotency_key: id } : {});\n } catch (error) {\n conversation.logger.warn(`could not record the agent's WhatsApp message (${errorName(error)})`);\n return null;\n }\n}\n\nfunction readMessage(message: Record<PropertyKey, unknown>, phoneNumberId: string, names: Map<string, string>): WhatsAppInbound | null {\n const waId = str(message.from);\n const id = str(message.id);\n const type = str(message.type) ?? \"unknown\";\n if (!waId || !id) return null;\n let text = \"\";\n let media: WhatsAppMedia | undefined;\n const body = message[type];\n if (type === \"text\" && isRecord(body)) text = str(body.body) ?? \"\";\n else if (type === \"button\" && isRecord(body)) text = str(body.text) ?? \"\";\n else if (type === \"interactive\" && isRecord(body)) {\n const reply = isRecord(body.button_reply) ? body.button_reply : isRecord(body.list_reply) ? body.list_reply : null;\n text = reply ? (str(reply.title) ?? \"\") : \"\";\n } else if (MEDIA.has(type) && isRecord(body)) {\n const mediaId = str(body.id);\n text = str(body.caption) ?? \"\";\n if (mediaId) media = { id: mediaId, mimeType: str(body.mime_type), filename: str(body.filename) };\n }\n const seconds = Number(message.timestamp);\n const context = isRecord(message.context) ? str(message.context.id) : undefined;\n return {\n subject: handles.waId(waId),\n waId,\n profileName: names.get(waId),\n phoneNumberId,\n messageId: id,\n type,\n text,\n media,\n occurredAt: Number.isFinite(seconds) && seconds > 0 ? new Date(seconds * 1000) : undefined,\n replyTo: context,\n };\n}\n\nfunction str(value: unknown): string | undefined {\n return typeof value === \"string\" && value.length > 0 ? value : undefined;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@niadra/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.4",
|
|
4
4
|
"description": "TypeScript SDK for Niadra, the omnichannel memory layer for a company's AI agents: context before the model call, history, tools and MCP.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Niadra",
|