@ingram-tech/ingram-cloud-sdk 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # `@ingram-tech/ingram-cloud-sdk`
2
2
 
3
3
  The Ingram Cloud `/v1` API wire contract in TypeScript: **Zod request/response
4
- schemas + SSE/webhook event types + JSON response types**. The `schemas` map is
5
- generated from the OpenAPI document — the source of truth for the wire — and the
6
- event/response types are hand-authored. This package is the **source of truth for
7
- the wire shapes**, not an HTTP client.
4
+ schemas + SSE/webhook event types + JSON response types**. The schemas are
5
+ hand-authored and are the **source of truth for the wire**the API imports the
6
+ same schemas to validate requests and to emit its OpenAPI document, and the `IC*`
7
+ response types are inferred from them. This package is not an HTTP client.
8
8
 
9
9
  ```ts
10
10
  import { schemas } from "@ingram-tech/ingram-cloud-sdk";
@@ -26,10 +26,13 @@ function render(smith: ICSmith) {
26
26
 
27
27
  - `.` — the `schemas` Zod map plus the SSE/webhook event types (`EVENT_TYPES`,
28
28
  `webhookEvent`, `streamFrame`, …).
29
- - `./schemas` — just the generated Zod `schemas` map.
29
+ - `./schemas` — just the Zod `schemas` map.
30
+ - `./zod` — the same schemas as individual named exports, one module per resource.
30
31
  - `./responses` — the `IC*` TypeScript types for the JSON response bodies.
31
32
  Zod-free; `import type` these to stay dependency-light.
32
- - `./openapi.json` — the raw OpenAPI document.
33
+
34
+ The OpenAPI document is served by the API itself (`/openapi.json`), emitted from
35
+ these schemas — it is no longer shipped as a file in this package.
33
36
 
34
37
  This is **not** an HTTP client — there is no `createApiClient` and no
35
38
  `@ai-sdk/*`-style provider here. Application code talks to the API over the
@@ -43,16 +46,13 @@ OpenAI-compatible surface; use `@ingram-tech/ai-sdk-adapter` and the standard
43
46
 
44
47
  ## Coverage
45
48
 
46
- The contract is in two halves generated + hand-authored:
47
-
48
- - **Generated, from the OpenAPI document:** every path, method, param, **request
49
- body**, and **non-streaming JSON response** is typed. ~28 of 124 operations
50
- stay `unknown` by design the **streaming/union** endpoints (`/runs`,
51
- `/chat/completions`, `/responses` a stream *or* JSON from one handler),
52
- channel **webhook acks**, and the OAuth **redirect**. None are expressible as a
53
- single response schema.
54
- - **Hand-authored:** the `{v:1}` webhook/feed envelope and the SSE run-stream
55
- frames, which OpenAPI can't describe, plus the `IC*` response types.
49
+ Every resource's request bodies and non-streaming JSON responses are typed as
50
+ precise Zod (one module per resource under `./zod`), and the `IC*` types are
51
+ inferred from them. The **streaming/union** endpoints (`/runs` stream,
52
+ `/chat/completions`, `/responses` a stream *or* JSON from one handler), channel
53
+ **webhook acks**, and the OAuth **redirect** are not expressible as a single
54
+ response schema, so they're not in the typed surface; the `{v:1}` webhook/feed
55
+ envelope and the SSE run-stream frames are the hand-authored `./events` half.
56
56
 
57
57
  The OpenAI-compatible stream chunks themselves are standard — use the `@ai-sdk/*`
58
58
  types rather than redefining them here.
package/package.json CHANGED
@@ -1,30 +1,28 @@
1
1
  {
2
2
  "name": "@ingram-tech/ingram-cloud-sdk",
3
- "version": "0.1.2",
4
- "description": "The Ingram Cloud /v1 API wire contract in TypeScript: Zod request/response schemas (generated from the OpenAPI spec), hand-authored SSE/webhook event types, and the JSON response types. The source of truth for the wire shapes.",
3
+ "version": "0.2.0",
4
+ "description": "Typed wire contract for the Ingram Cloud API: Zod schemas to validate request/response bodies, TypeScript types for the JSON you read back, and SSE/webhook event types.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "homepage": "https://cloud.ingram.tech",
8
8
  "keywords": ["ingram-cloud", "ingram", "sdk", "api", "wire", "zod", "openapi"],
9
- "files": ["ts", "openapi.json"],
9
+ "files": ["ts"],
10
10
  "publishConfig": {
11
11
  "access": "public"
12
12
  },
13
13
  "exports": {
14
14
  ".": "./ts/index.ts",
15
15
  "./schemas": "./ts/schemas.ts",
16
- "./responses": "./ts/responses.ts",
17
- "./openapi.json": "./openapi.json"
16
+ "./zod": "./ts/zod/index.ts",
17
+ "./responses": "./ts/responses.ts"
18
18
  },
19
19
  "scripts": {
20
- "generate": "mkdir -p ts && openapi-zod-client openapi.json -o ts/schemas.ts --export-schemas && bun scripts/strip-client.ts",
21
20
  "typecheck": "tsc --noEmit",
22
21
  "lint": "oxlint",
23
22
  "format": "oxfmt --write ."
24
23
  },
25
24
  "devDependencies": {
26
25
  "@ingram-tech/oxlint-config": "^0.2.1",
27
- "openapi-zod-client": "^1.18.3",
28
26
  "oxfmt": "^0.54.0",
29
27
  "oxlint": "^1.70.0",
30
28
  "typescript": "^6.0.0"
package/ts/index.ts CHANGED
@@ -1,18 +1,17 @@
1
1
  /**
2
2
  * The Ingram Cloud API wire contract, in TypeScript.
3
3
  *
4
- * GENERATED do not edit `schemas.ts` by hand. It is produced from
5
- * `../openapi.json`, the hand-maintained source of truth for the wire:
4
+ * `schemas` is the named map of hand-authored Zod schemas for the request and
5
+ * response bodies (defined in `./zod`, one module per resource). It's the source
6
+ * of truth for the wire shapes — the API imports the same schemas to validate
7
+ * requests and to emit its OpenAPI document — and you can use it to validate a
8
+ * body against the contract. This package is not an HTTP client.
6
9
  *
7
- * bun run generate # regenerate schemas.ts from openapi.json
10
+ * `./responses` is the matching `IC*` TypeScript types (inferred from the same
11
+ * schemas), for typing the JSON you read back without pulling in Zod.
8
12
  *
9
- * `schemas` is the named map of Zod schemas for the request/response bodies.
10
- * It's used to validate the API's wire shapes in the `api/` test suite; this
11
- * package is the source of truth for those shapes, not an HTTP client.
12
- *
13
- * `./events` is the hand-authored half: the `{v:1}` webhook/feed envelope and the
14
- * SSE run-stream frames, which OpenAPI can't express. Together they make the
15
- * contract complete — generated request/response bodies + hand-authored streams.
13
+ * `./events` is the hand-authored `{v:1}` webhook/feed envelope and the SSE
14
+ * run-stream frames, which OpenAPI can't express.
16
15
  *
17
16
  * See `../README.md`.
18
17
  */
package/ts/responses.ts CHANGED
@@ -1,454 +1,66 @@
1
1
  /**
2
- * Hand-authored TypeScript types for the `/v1` JSON response bodies.
2
+ * TypeScript types for the `/v1` JSON response bodies — the consumer-facing
3
+ * companion to the wire schemas, imported by API consumers (e.g. the console) to
4
+ * type the JSON they read back.
3
5
  *
4
- * The consumer-facing companion to the generated `schemas.ts` Zod schemas: these
5
- * are lightweight, ergonomic mirrors of the wire response shapes, meant to be
6
- * imported by API consumers (e.g. the console) to type the JSON they read back.
7
- * `schemas.ts` validates wire shapes in the api/ test suite; these describe the
8
- * same bodies as plain TS interfaces with no runtime/Zod dependency.
6
+ * The `IC*` types are `z.infer`red from the hand-authored Zod in
7
+ * `./zod/<resource>` (the wire's source of truth) and re-exported here as
8
+ * **types** so this module stays Zod-free for `import type` consumers. Only the
9
+ * list-envelope generics ({@link ICList}/{@link ICPaginatedResponse}) are
10
+ * declared inline, since they're generic over any resource.
9
11
  */
10
12
 
11
- export interface ICSmith {
12
- id: string;
13
- external_id: string | null;
14
- display_name: string | null;
15
- locale: string | null;
16
- timezone: string | null;
17
- /** The customer (billable party) this smith's usage rolls up to. */
18
- customer_id?: string | null;
19
- metadata?: Record<string, unknown>;
20
- // The *effective* agent config (agent-resolved when attached).
21
- model: string;
22
- /** The raw instructions template (may contain {{ variables }}). */
23
- instructions: string;
24
- /** What this smith actually runs with — {{ variables }} bound per-smith. */
25
- rendered_instructions?: string | null;
26
- enabled_hosted_tools?: string[];
27
- auto_memory?: boolean;
28
- /** How the config resolved: by reference, with overrides, or embedded. */
29
- config_source?: "agent" | "override" | "custom";
30
- agent_id?: string | null;
31
- agent_version?: number | null;
32
- pin?: number | null;
33
- created_at: string;
34
- }
35
- export interface ICBlock {
36
- id: string;
37
- label: string;
38
- description: string;
39
- value: string;
40
- char_limit: number;
41
- updated_at: string | null;
42
- }
43
- export interface ICMemory {
44
- id: string;
45
- category: string | null;
46
- subject: string | null;
47
- content: string;
48
- /** Extraction confidence (0–1); null when not scored. */
49
- confidence?: number | null;
50
- source: string | null;
51
- /** Relevance score only present on `/memories/search` hits. */
52
- score?: number | null;
53
- metadata?: Record<string, unknown> | null;
54
- created_at: string | null;
55
- updated_at: string | null;
56
- }
57
- export interface ICToolCall {
58
- tool_call_id: string;
59
- tool: string;
60
- args: Record<string, unknown>;
61
- execution: string;
62
- }
63
- /** One run's own token usage (the `usage` map on a run). Distinct from
64
- * {@link ICUsage}, which is the tenant's aggregated *billing* summary. */
65
- export interface ICRunUsage {
66
- input_tokens: number;
67
- output_tokens: number;
68
- total_tokens: number;
69
- }
70
- export interface ICRun {
71
- id: string;
72
- smith_id: string;
73
- thread_id: string;
74
- status: string;
75
- channel: string;
76
- input: Array<{ role: string; content: string }>;
77
- output: { content?: string; tool_calls?: ICToolCall[] } | null;
78
- stop_reason: string | null;
79
- usage: ICRunUsage | null;
80
- metadata?: Record<string, unknown>;
81
- created_at: string | null;
82
- updated_at: string | null;
83
- }
84
- export interface ICRunEvent {
85
- seq: number;
86
- type: string;
87
- data: Record<string, unknown>;
88
- created_at: string | null;
89
- }
90
- export interface ICConnection {
91
- id: string;
92
- provider: string;
93
- scopes: string[];
94
- status: string;
95
- expires_at: string | null;
96
- metadata?: Record<string, unknown>;
97
- created_at: string | null;
98
- }
99
- export interface ICChannel {
100
- id: string;
101
- kind: string;
102
- address: string;
103
- provider: string | null;
104
- provider_metadata?: Record<string, unknown>;
105
- /** Names of the encrypted secret fields that are set (values never returned). */
106
- secret_keys?: string[];
107
- status: string;
108
- created_at: string | null;
109
- }
110
- export interface ICSchedule {
111
- id: string;
112
- name: string | null;
113
- cron: string;
114
- timezone: string;
115
- /** Messages replayed as the run input on each fire. */
116
- input: Array<Record<string, unknown>>;
117
- /** Thread the fired runs append to; null mints a fresh thread per fire. */
118
- thread_id: string | null;
119
- /** Max overlapping fired runs before new fires are skipped. */
120
- max_concurrent: number;
121
- /** What a failed fire does next (e.g. `continue`, `pause`). */
122
- on_failure: string;
123
- enabled: boolean;
124
- next_fire_at: string | null;
125
- last_fire_at: string | null;
126
- created_at: string | null;
127
- }
128
- export interface ICApproval {
129
- id: string;
130
- run_id: string | null;
131
- smith_id: string | null;
132
- tool_call_id: string | null;
133
- tool: string | null;
134
- args: Record<string, unknown>;
135
- status: string;
136
- actor: string | null;
137
- reason: string | null;
138
- created_at: string | null;
139
- resolved_at: string | null;
140
- }
141
- export interface ICEvent {
142
- id: string;
143
- type: string;
144
- smith_id: string | null;
145
- data: Record<string, unknown>;
146
- created_at: string | null;
147
- }
148
- export interface ICMcpTool {
149
- name: string;
150
- description: string | null;
151
- /** Effective gate: server destructiveHint OR an approval_policy match. */
152
- requires_approval: boolean;
153
- /** Passes the default-deny tool_allowlist (always true when no allow-list). */
154
- enabled: boolean;
155
- }
156
- export interface ICApprovalRule {
157
- match: string;
158
- require?: string;
159
- }
160
- export interface ICMcpServer {
161
- id: string;
162
- name: string;
163
- url: string;
164
- auth: { kind: string; provider: string | null; client_mode?: string };
165
- /** tenant_owned | tenant_registered | catalog. */
166
- origin?: string;
167
- catalog_slug?: string | null;
168
- /** null = expose all discovered tools; otherwise the default-deny set. */
169
- tool_allowlist?: string[] | null;
170
- approval_policy?: ICApprovalRule[];
171
- tools: ICMcpTool[];
172
- tools_refreshed_at: string | null;
173
- /** `degraded` when the edge failed discovery or its secret can't be decoded at
174
- * run time; otherwise the stored lifecycle status. */
175
- status: string;
176
- /** Last discovery/runtime-load failure, or null when the edge is healthy. */
177
- discovery_error: string | null;
178
- created_at: string | null;
179
- }
180
- export interface ICCatalogEntry {
181
- slug: string;
182
- display_name: string;
183
- description: string;
184
- mcp_url: string;
185
- auth: { kind: string; provider: string | null; client_mode: string };
186
- scopes: string[];
187
- default_allowlist: string[] | null;
188
- default_approval_policy: ICApprovalRule[];
189
- logo_url: string | null;
190
- docs_url: string | null;
191
- }
192
- export interface ICWebhook {
193
- id: string;
194
- url: string;
195
- events: string[];
196
- active: boolean;
197
- created_at: string | null;
198
- }
199
- export interface ICToken {
200
- id: string;
201
- name: string | null;
202
- sub: string;
203
- scopes: string[];
204
- prefix: string | null;
205
- created_at: string | null;
206
- expires_at: string | null;
207
- revoked_at: string | null;
208
- }
209
- export interface ICUsage {
210
- totals: {
211
- runs: number;
212
- input_tokens: number;
213
- output_tokens: number;
214
- total_tokens: number;
215
- };
216
- series: Array<{ date: string; runs: number; total_tokens: number }>;
217
- }
218
- export interface ICProvider {
219
- provider: string;
220
- client_id: string | null;
221
- has_client_secret: boolean;
222
- token_uri: string | null;
223
- scopes_allowed: string[];
224
- refresh_webhook: string | null;
225
- }
226
- export interface ICModel {
227
- id: string;
228
- provider: string;
229
- label: string;
230
- available: boolean;
231
- /** Whose key a run would use: the tenant's ("byok") or IC's ("platform"). */
232
- source?: "byok" | "platform" | null;
233
- }
234
- export interface ICModelProvider {
235
- id: string;
236
- label: string;
237
- base_url: boolean;
238
- /** True when the platform fallback covers this provider (no tenant key). */
239
- platform_key?: boolean;
240
- }
241
- export interface ICModelCatalog {
242
- data: ICModel[];
243
- providers: ICModelProvider[];
244
- }
245
- export interface ICModelKey {
246
- provider: string;
247
- base_url: string | null;
248
- has_key: boolean;
249
- updated_at: string | null;
250
- }
251
-
252
- /** Span kinds emitted by the observability backend. */
253
- export type ICSpanKind =
254
- | "run"
255
- | "model_call"
256
- | "tool_call"
257
- | "memory_op"
258
- | "retrieval"
259
- | "sub_agent"
260
- | "runtime_event";
261
-
262
- /** A single timed unit of work inside a trace. */
263
- export interface ICSpan {
264
- id: string;
265
- trace_id: string;
266
- parent_span_id: string | null;
267
- kind: ICSpanKind;
268
- name: string;
269
- status: string;
270
- started_at: string;
271
- ended_at: string | null;
272
- duration_ms: number | null;
273
- model: string | null;
274
- input_tokens: number | null;
275
- output_tokens: number | null;
276
- cost: number | null;
277
- attributes: Record<string, unknown>;
278
- }
279
-
280
- /** A span node in the nested `span_tree` (same shape + children). */
281
- export interface ICSpanNode extends ICSpan {
282
- children: ICSpanNode[];
283
- }
284
-
285
- /** Top-level trace — one end-to-end agent operation. */
286
- export interface ICTrace {
287
- id: string;
288
- smith_id: string;
289
- app_id: string | null;
290
- root_kind: ICSpanKind;
291
- name: string;
292
- status: string;
293
- started_at: string;
294
- ended_at: string | null;
295
- duration_ms: number | null;
296
- total_tokens: number | null;
297
- total_cost: number | null;
298
- attributes: Record<string, unknown>;
299
- }
300
-
301
- /** A trace plus its flat spans and nested tree. */
302
- export interface ICTraceDetail extends ICTrace {
303
- spans: ICSpan[];
304
- span_tree: ICSpanNode[];
305
- }
306
-
307
- /** Aggregated usage grouped by app, smith, model, or customer. */
308
- export interface ICUsageBreakdown {
309
- group_by: "app" | "smith" | "model" | "customer";
310
- totals: { tokens: number; cost: number; run_count: number };
311
- groups: Array<{
312
- app?: string | null;
313
- smith?: string | null;
314
- model?: string | null;
315
- // Customer-grouped views label unassigned usage `principal:<smith id>`.
316
- customer?: string | null;
317
- tokens: number;
318
- cost: number;
319
- run_count: number;
320
- }>;
321
- /** Custom billable events aggregated per meter over the same filters. */
322
- meters?: Array<{
323
- meter: string;
324
- customer?: string | null;
325
- quantity: number;
326
- events: number;
327
- }>;
328
- }
329
-
330
- export interface ICMintedToken {
331
- id: string;
332
- token: string;
333
- scope: string;
334
- sub: string;
335
- scopes: string[];
336
- expires_at: string | null;
337
- }
338
-
339
- export interface ICTelegramBot {
340
- configured: boolean;
341
- bot_username?: string;
342
- bot_id?: number;
343
- has_token?: boolean;
344
- webhook_url?: string;
345
- }
346
-
347
- export interface ICSlackApp {
348
- configured: boolean;
349
- bot_user_id?: string;
350
- team_id?: string;
351
- events_url?: string;
352
- /** Shared app's OAuth client is set — "Add to Slack" installs work. */
353
- oauth_ready?: boolean;
354
- oauth_redirect_url?: string;
355
- /** Where the OAuth redirect sends installers back (?slack=… appended). */
356
- return_url?: string;
357
- /** App factory: mints per-smith Slack apps from the manifest template. */
358
- factory?: { configured: boolean };
359
- }
360
-
361
- export interface ICEmailConfig {
362
- configured: boolean;
363
- from_domain?: string;
364
- display_name?: string | null;
365
- has_token?: boolean;
366
- inbound_url?: string;
367
- /** Only present on PUT (shown once) — wire it into the inbound worker. */
368
- inbound_secret?: string;
369
- }
370
-
371
- export interface ICCustomer {
372
- id: string;
373
- name: string;
374
- external_ids: Record<string, string>;
375
- metadata: Record<string, unknown>;
376
- smith_count?: number;
377
- created_at: string | null;
378
- }
379
-
380
- export interface ICBudget {
381
- id: string;
382
- scope: "tenant" | "smith" | "customer";
383
- scope_id?: string | null;
384
- period: string;
385
- limit_usd: number;
386
- action: "warn" | "block";
387
- created_at?: string | null;
388
- }
389
- export interface ICBudgetStatus extends ICBudget {
390
- period_start: string;
391
- period_key: string;
392
- spent: number;
393
- pct: number;
394
- over: boolean;
395
- }
396
-
397
- export interface ICSmithRevision {
398
- version: number;
399
- snapshot: {
400
- instructions?: string | null;
401
- model?: string | null;
402
- enabled_hosted_tools?: string[];
403
- auto_memory?: boolean;
404
- };
405
- created_by?: string | null;
406
- note?: string | null;
407
- created_at?: string | null;
408
- }
409
-
410
- /** A per-smith variable an agent declares; bound at run time. */
411
- export interface ICAgentVariable {
412
- name: string;
413
- default?: string | null;
414
- description?: string | null;
415
- required?: boolean;
416
- }
417
-
418
- export interface ICAgent {
419
- id: string;
420
- /** Immutable, project-unique IaC reconcile key. Null for the default agent. */
421
- slug: string | null;
422
- /** Free, mutable display label (not unique). */
423
- name: string;
424
- /** True for the lazily-created default agent (can't be deleted). */
425
- is_default: boolean;
426
- /** The mutable draft head — what the next publish snapshots. */
427
- draft: {
428
- instructions: string | null;
429
- model: string | null;
430
- enabled_hosted_tools: string[];
431
- auto_memory: boolean | null;
432
- variables: ICAgentVariable[];
433
- };
434
- active_version: number | null;
435
- rollout_version: number | null;
436
- rollout_percent: number;
437
- smith_count?: number;
438
- created_at: string | null;
439
- updated_at: string | null;
440
- }
441
-
442
- export interface ICAgentVersion {
443
- version: number;
444
- snapshot: {
445
- instructions?: string | null;
446
- model?: string | null;
447
- enabled_hosted_tools?: string[];
448
- auto_memory?: boolean | null;
449
- variables?: ICAgentVariable[];
450
- };
451
- created_by?: string | null;
452
- note?: string | null;
453
- created_at?: string | null;
454
- }
13
+ /** The standard list envelope — a `data` array and nothing else. Most list
14
+ * endpoints return this. */
15
+ export interface ICList<T> {
16
+ data: T[];
17
+ }
18
+
19
+ /** A cursor-paginated list. The endpoints that page (smiths, memories,
20
+ * customers) add `has_more`, and some a `next_cursor`; the rest return the
21
+ * plain {@link ICList}. */
22
+ export interface ICPaginatedResponse<T> {
23
+ data: T[];
24
+ has_more: boolean;
25
+ next_cursor?: string | null;
26
+ }
27
+
28
+ // ── Re-exported from the hand-authored Zod source of truth (./zod/*) ──────────
29
+ export type { ICAgent, ICAgentVariable, ICAgentVersion } from "./zod/agents";
30
+ export type { ICApproval } from "./zod/approvals";
31
+ export type { ICBudget, ICBudgetStatus } from "./zod/budgets";
32
+ export type { ICCatalogEntry } from "./zod/catalog";
33
+ export type { ICChannel, ICChannelCreated } from "./zod/channels";
34
+ export type { ICConnection } from "./zod/connections";
35
+ export type { ICCustomer } from "./zod/customers";
36
+ export type { ICEmailConfig } from "./zod/email";
37
+ export type { ICMcpServer, ICMcpTool, ICApprovalRule } from "./zod/mcp";
38
+ export type { ICMemory } from "./zod/memories";
39
+ export type {
40
+ ICSpanKind,
41
+ ICSpan,
42
+ ICSpanNode,
43
+ ICTrace,
44
+ ICTraceDetail,
45
+ ICUsageBreakdown,
46
+ } from "./zod/observability";
47
+ export type { ICProject } from "./zod/projects";
48
+ export type { ICRun, ICRunUsage, ICToolCall, ICRunEvent } from "./zod/runs";
49
+ export type { ICSchedule } from "./zod/schedules";
50
+ export type { ICSlackApp } from "./zod/slack";
51
+ export type { ICSmith, ICBlock } from "./zod/smiths";
52
+ export type { ICSmithRevision } from "./zod/smith-revisions";
53
+ export type { ICTelegramBot } from "./zod/telegram";
54
+ export type {
55
+ ICEvent,
56
+ ICWebhook,
57
+ ICToken,
58
+ ICMintedToken,
59
+ ICUsage,
60
+ ICModel,
61
+ ICModelProvider,
62
+ ICModelCatalog,
63
+ ICModelKey,
64
+ ICProvider,
65
+ } from "./zod/tenant";
66
+ export type { ICWhatsAppConfig } from "./zod/whatsapp";