@apex-inc/sdk 0.1.0 → 0.5.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.
@@ -42,7 +42,49 @@ export function createManagementClient(cfg) {
42
42
  }
43
43
  return res.json();
44
44
  },
45
+ /** Fetch a single experiment (unified shape). */
46
+ async getExperiment(id) {
47
+ const res = await fetch(`${root}/api/experiments/${encodeURIComponent(id)}?unified=true`, { headers });
48
+ if (!res.ok)
49
+ throw new Error(`Apex getExperiment failed (${res.status}): ${await res.text().catch(() => "")}`);
50
+ return res.json();
51
+ },
52
+ /**
53
+ * Computed results + the shared readiness verdict
54
+ * (collecting | decisive | inconclusive | guardrail) for any surface.
55
+ */
56
+ async getExperimentResults(id) {
57
+ const res = await fetch(`${root}/api/experiments/${encodeURIComponent(id)}/results`, { headers });
58
+ if (!res.ok)
59
+ throw new Error(`Apex getExperimentResults failed (${res.status}): ${await res.text().catch(() => "")}`);
60
+ return res.json();
61
+ },
62
+ /**
63
+ * Promote the experiment's winner (re-verifies the readiness gate
64
+ * server-side; pass `force` to override). Winner content is written into
65
+ * the canonical communication as a new version.
66
+ */
67
+ async promoteExperiment(id, opts) {
68
+ const res = await fetch(`${root}/api/experiments/${encodeURIComponent(id)}/promote`, { method: "POST", headers, body: JSON.stringify(opts ?? {}) });
69
+ if (!res.ok)
70
+ throw new Error(`Apex promoteExperiment failed (${res.status}): ${await res.text().catch(() => "")}`);
71
+ return res.json();
72
+ },
45
73
  // ─── Communications ───────────────────────────────────────────────
74
+ /** Append a content variant to a communication (composer-columns model; an experiment arm). */
75
+ async createCommVariant(commId, opts) {
76
+ const res = await fetch(`${root}/api/communications/${encodeURIComponent(commId)}/variants`, { method: "POST", headers, body: JSON.stringify(opts ?? {}) });
77
+ if (!res.ok)
78
+ throw new Error(`Apex createCommVariant failed (${res.status}): ${await res.text().catch(() => "")}`);
79
+ return res.json();
80
+ },
81
+ /** List the content variants (`content.variants[]`) of a communication. */
82
+ async listCommVariants(commId) {
83
+ const res = await fetch(`${root}/api/communications/${encodeURIComponent(commId)}/variants`, { headers });
84
+ if (!res.ok)
85
+ throw new Error(`Apex listCommVariants failed (${res.status}): ${await res.text().catch(() => "")}`);
86
+ return res.json();
87
+ },
46
88
  async recommendCommunications() {
47
89
  const res = await fetch(`${root}/api/communications/recommend`, { method: "POST", headers });
48
90
  if (!res.ok)
@@ -148,5 +190,355 @@ export function createManagementClient(cfg) {
148
190
  throw new Error(`Apex getEventIntegrationStatus failed (${res.status}): ${await res.text().catch(() => "")}`);
149
191
  return res.json();
150
192
  },
193
+ // ─── Partner Network — Programs (MMP-161) ─────────────────────────
194
+ //
195
+ // Every method below runs under the merchant's x-apex-project scope.
196
+ // Partner-facing flows (self-signup, dashboard, Stripe Connect OAuth)
197
+ // are cookie-authed in the portal and not wrapped in this SDK.
198
+ async listPrograms() {
199
+ const res = await fetch(`${root}/api/mobile/affiliates/programs`, { headers });
200
+ if (!res.ok)
201
+ throw new Error(`Apex listPrograms failed (${res.status}): ${await res.text().catch(() => "")}`);
202
+ return res.json();
203
+ },
204
+ async createProgram(params) {
205
+ const res = await fetch(`${root}/api/mobile/affiliates/programs`, {
206
+ method: "POST",
207
+ headers,
208
+ body: JSON.stringify(params),
209
+ });
210
+ if (!res.ok)
211
+ throw new Error(`Apex createProgram failed (${res.status}): ${await res.text().catch(() => "")}`);
212
+ return res.json();
213
+ },
214
+ async updateProgram(programId, updates) {
215
+ const res = await fetch(`${root}/api/mobile/affiliates/programs/${encodeURIComponent(programId)}`, {
216
+ method: "PATCH",
217
+ headers,
218
+ body: JSON.stringify(updates),
219
+ });
220
+ if (!res.ok)
221
+ throw new Error(`Apex updateProgram failed (${res.status}): ${await res.text().catch(() => "")}`);
222
+ return res.json();
223
+ },
224
+ async listProgramMembers(programId) {
225
+ const res = await fetch(`${root}/api/mobile/affiliates/programs/${encodeURIComponent(programId)}/members`, { headers });
226
+ if (!res.ok)
227
+ throw new Error(`Apex listProgramMembers failed (${res.status}): ${await res.text().catch(() => "")}`);
228
+ return res.json();
229
+ },
230
+ /** Apply program default commission to every existing member (respects overrides). */
231
+ async applyProgramDefaultToMembers(programId) {
232
+ const res = await fetch(`${root}/api/mobile/affiliates/programs/${encodeURIComponent(programId)}/apply-to-members`, {
233
+ method: "POST",
234
+ headers,
235
+ });
236
+ if (!res.ok)
237
+ throw new Error(`Apex applyProgramDefaultToMembers failed (${res.status}): ${await res.text().catch(() => "")}`);
238
+ return res.json();
239
+ },
240
+ // ─── Partner Network — Rate overrides ─────────────────────────────
241
+ /** Override a single membership's commission (sticky — survives program default updates). */
242
+ async updateMembershipRate(profileId, params) {
243
+ const res = await fetch(`${root}/api/mobile/affiliates/memberships/${encodeURIComponent(profileId)}/rate`, {
244
+ method: "POST",
245
+ headers,
246
+ body: JSON.stringify(params),
247
+ });
248
+ if (!res.ok)
249
+ throw new Error(`Apex updateMembershipRate failed (${res.status}): ${await res.text().catch(() => "")}`);
250
+ return res.json();
251
+ },
252
+ /** Reset a membership back to its program's default commission. */
253
+ async resetMembershipRate(profileId, reason) {
254
+ const res = await fetch(`${root}/api/mobile/affiliates/memberships/${encodeURIComponent(profileId)}/rate`, {
255
+ method: "POST",
256
+ headers,
257
+ body: JSON.stringify({ reset: true, reason }),
258
+ });
259
+ if (!res.ok)
260
+ throw new Error(`Apex resetMembershipRate failed (${res.status}): ${await res.text().catch(() => "")}`);
261
+ return res.json();
262
+ },
263
+ // ─── Partner Network — Invitations ────────────────────────────────
264
+ /** Send a magic-link invite to a partner (email resolves to existing profile or creates pending). */
265
+ async invitePartner(params) {
266
+ const res = await fetch(`${root}/api/mobile/affiliates/invites`, {
267
+ method: "POST",
268
+ headers,
269
+ body: JSON.stringify(params),
270
+ });
271
+ if (!res.ok)
272
+ throw new Error(`Apex invitePartner failed (${res.status}): ${await res.text().catch(() => "")}`);
273
+ return res.json();
274
+ },
275
+ // ─── Partner Network — Payouts (MMP-145 + MMP-151) ────────────────
276
+ /** Pending payouts grouped into aging buckets (0-30, 30-60, 60+). */
277
+ async listPendingPayouts() {
278
+ const res = await fetch(`${root}/api/mobile/affiliates/payouts/pending`, { headers });
279
+ if (!res.ok)
280
+ throw new Error(`Apex listPendingPayouts failed (${res.status}): ${await res.text().catch(() => "")}`);
281
+ return res.json();
282
+ },
283
+ /** Approve one or more pending payouts (creates HoldbackEntry per approved conversion). */
284
+ async approvePayouts(conversionIds) {
285
+ const res = await fetch(`${root}/api/mobile/affiliates/payouts/approve`, {
286
+ method: "POST",
287
+ headers,
288
+ body: JSON.stringify({ conversionIds }),
289
+ });
290
+ if (!res.ok)
291
+ throw new Error(`Apex approvePayouts failed (${res.status}): ${await res.text().catch(() => "")}`);
292
+ return res.json();
293
+ },
294
+ /** Reject pending payouts (opens an audit FraudCase severity=low). */
295
+ async rejectPayouts(conversionIds, reason) {
296
+ const res = await fetch(`${root}/api/mobile/affiliates/payouts/reject`, {
297
+ method: "POST",
298
+ headers,
299
+ body: JSON.stringify({ conversionIds, reason }),
300
+ });
301
+ if (!res.ok)
302
+ throw new Error(`Apex rejectPayouts failed (${res.status}): ${await res.text().catch(() => "")}`);
303
+ return res.json();
304
+ },
305
+ // ─── Partner Network — Fees (MMP-158) ─────────────────────────────
306
+ /** Merchant-facing fee disclosure. Returns every fee kind Apex charges this merchant. */
307
+ async getEffectiveFees() {
308
+ const res = await fetch(`${root}/api/mobile/affiliates/fees`, { headers });
309
+ if (!res.ok)
310
+ throw new Error(`Apex getEffectiveFees failed (${res.status}): ${await res.text().catch(() => "")}`);
311
+ return res.json();
312
+ },
313
+ // ─── Partner Network — Vouch (MMP-156) ────────────────────────────
314
+ /**
315
+ * Vouch for a partner — one-tier bump for 60 days. Requires indemnity
316
+ * acknowledgment. Fails if the partner is already verified.
317
+ */
318
+ async vouchForPartner(profileId, params) {
319
+ const res = await fetch(`${root}/api/mobile/affiliates/${encodeURIComponent(profileId)}/vouch`, {
320
+ method: "POST",
321
+ headers,
322
+ body: JSON.stringify(params),
323
+ });
324
+ if (!res.ok)
325
+ throw new Error(`Apex vouchForPartner failed (${res.status}): ${await res.text().catch(() => "")}`);
326
+ return res.json();
327
+ },
328
+ // ─── Communications — end-user preferences (DFC-016) ─────────────────
329
+ //
330
+ // Two API-key methods for managing an end-user's communication
331
+ // preferences from the customer's backend. `preferences.get` and
332
+ // `preferences.update` proxy `/api/communications/preferences/[id]`
333
+ // with the org-scoped key — the route's dual-auth layer treats the
334
+ // API-key caller as a trusted server.
335
+ preferences: {
336
+ async get(endUserId) {
337
+ const res = await fetch(`${root}/api/communications/preferences/${encodeURIComponent(endUserId)}?projectKey=${encodeURIComponent(cfg.projectKey)}`, { headers });
338
+ if (!res.ok) {
339
+ throw new Error(`Apex preferences.get failed (${res.status}): ${await res.text().catch(() => "")}`);
340
+ }
341
+ return res.json();
342
+ },
343
+ async update(endUserId, patch) {
344
+ const res = await fetch(`${root}/api/communications/preferences/${encodeURIComponent(endUserId)}?projectKey=${encodeURIComponent(cfg.projectKey)}`, {
345
+ method: "PATCH",
346
+ headers,
347
+ body: JSON.stringify(patch),
348
+ });
349
+ if (!res.ok) {
350
+ throw new Error(`Apex preferences.update failed (${res.status}): ${await res.text().catch(() => "")}`);
351
+ }
352
+ return res.json();
353
+ },
354
+ },
355
+ // ─── Communications — inbox notifications (DFC-016) ──────────────────
356
+ //
357
+ // Read + write helpers for the in-app inbox. The `mark-read` and
358
+ // `mark-all-read` paths target the existing PATCH route on the
359
+ // workspace-admin gate (the SDK's API key satisfies that gate).
360
+ notifications: {
361
+ async list(endUserId, opts) {
362
+ const qs = new URLSearchParams({ userId: endUserId });
363
+ if (opts?.limit)
364
+ qs.set("limit", String(opts.limit));
365
+ if (opts?.unreadOnly)
366
+ qs.set("unreadOnly", "true");
367
+ const res = await fetch(`${root}/api/communications/notifications?${qs.toString()}`, { headers });
368
+ if (!res.ok) {
369
+ throw new Error(`Apex notifications.list failed (${res.status}): ${await res.text().catch(() => "")}`);
370
+ }
371
+ return res.json();
372
+ },
373
+ async markRead(notificationId) {
374
+ const res = await fetch(`${root}/api/communications/notifications`, {
375
+ method: "PATCH",
376
+ headers,
377
+ body: JSON.stringify({ notificationId }),
378
+ });
379
+ if (!res.ok) {
380
+ throw new Error(`Apex notifications.markRead failed (${res.status}): ${await res.text().catch(() => "")}`);
381
+ }
382
+ },
383
+ async markAllRead(endUserId) {
384
+ const res = await fetch(`${root}/api/communications/notifications`, {
385
+ method: "PATCH",
386
+ headers,
387
+ body: JSON.stringify({ markAll: true, endUserId }),
388
+ });
389
+ if (!res.ok) {
390
+ throw new Error(`Apex notifications.markAllRead failed (${res.status}): ${await res.text().catch(() => "")}`);
391
+ }
392
+ },
393
+ },
394
+ // ─── Communications — end-user token mint (DFC-018) ──────────────────
395
+ //
396
+ // Server-side helper that asks Apex to sign an HMAC end-user token
397
+ // for the customer's client. The signing key lives on Apex servers
398
+ // only, so this is the canonical path. Customers call this from
399
+ // their backend, then hand the token to <ApexPreferenceCenter>.
400
+ /**
401
+ * Set up Apex — programmatic access to per-workspace setup state.
402
+ *
403
+ * `get()` returns the current hub signal + per-step overrides
404
+ * (read-only). `complete(stepId)` / `skip(stepId, reason)` mirror
405
+ * the dashboard's write contract at
406
+ * POST /api/workspaces/<projectKey>/setup-state/<stepId>.
407
+ *
408
+ * Allowed step IDs are enforced server-side via the SetupRegistry
409
+ * allowlist. Unknown IDs return 400.
410
+ */
411
+ workspace: {
412
+ setupState: {
413
+ async get() {
414
+ const res = await fetch(`${root}/api/workspaces/${encodeURIComponent(cfg.projectKey)}/setup-state`, { headers });
415
+ if (!res.ok) {
416
+ throw new Error(`Apex workspace.setupState.get failed (${res.status}): ${await res.text().catch(() => "")}`);
417
+ }
418
+ const json = (await res.json());
419
+ return {
420
+ overrides: json.overrides ?? [],
421
+ signal: json.signal ?? {},
422
+ };
423
+ },
424
+ async complete(stepId) {
425
+ const res = await fetch(`${root}/api/workspaces/${encodeURIComponent(cfg.projectKey)}/setup-state/${encodeURIComponent(stepId)}/complete`, { method: "POST", headers });
426
+ if (!res.ok) {
427
+ throw new Error(`Apex workspace.setupState.complete failed (${res.status}): ${await res.text().catch(() => "")}`);
428
+ }
429
+ },
430
+ async skip(stepId, reason) {
431
+ const res = await fetch(`${root}/api/workspaces/${encodeURIComponent(cfg.projectKey)}/setup-state/${encodeURIComponent(stepId)}/skip`, {
432
+ method: "POST",
433
+ headers,
434
+ body: JSON.stringify({ reason: reason ?? null }),
435
+ });
436
+ if (!res.ok) {
437
+ throw new Error(`Apex workspace.setupState.skip failed (${res.status}): ${await res.text().catch(() => "")}`);
438
+ }
439
+ },
440
+ },
441
+ },
442
+ /**
443
+ * Data Sources registry — the producers that send events into this
444
+ * workspace (Website, iOS / Android apps, Backend). Mirrors
445
+ * /api/workspaces/<projectKey>/data-sources. `dataSourceId` is
446
+ * reporting-only; these methods manage the registry, not access control.
447
+ */
448
+ dataSources: {
449
+ async list(opts) {
450
+ const qs = opts?.includeArchived ? "?includeArchived=true" : "";
451
+ const res = await fetch(`${root}/api/workspaces/${encodeURIComponent(cfg.projectKey)}/data-sources${qs}`, { headers });
452
+ if (!res.ok) {
453
+ throw new Error(`Apex dataSources.list failed (${res.status}): ${await res.text().catch(() => "")}`);
454
+ }
455
+ const json = (await res.json());
456
+ return json.data ?? [];
457
+ },
458
+ async get(id) {
459
+ const res = await fetch(`${root}/api/workspaces/${encodeURIComponent(cfg.projectKey)}/data-sources/${encodeURIComponent(id)}`, { headers });
460
+ if (!res.ok) {
461
+ throw new Error(`Apex dataSources.get failed (${res.status}): ${await res.text().catch(() => "")}`);
462
+ }
463
+ const json = (await res.json());
464
+ return json.data;
465
+ },
466
+ async create(params) {
467
+ const res = await fetch(`${root}/api/workspaces/${encodeURIComponent(cfg.projectKey)}/data-sources`, { method: "POST", headers, body: JSON.stringify(params) });
468
+ if (!res.ok) {
469
+ throw new Error(`Apex dataSources.create failed (${res.status}): ${await res.text().catch(() => "")}`);
470
+ }
471
+ const json = (await res.json());
472
+ return json.data;
473
+ },
474
+ async update(id, patch) {
475
+ const res = await fetch(`${root}/api/workspaces/${encodeURIComponent(cfg.projectKey)}/data-sources/${encodeURIComponent(id)}`, { method: "PATCH", headers, body: JSON.stringify(patch) });
476
+ if (!res.ok) {
477
+ throw new Error(`Apex dataSources.update failed (${res.status}): ${await res.text().catch(() => "")}`);
478
+ }
479
+ const json = (await res.json());
480
+ return json.data;
481
+ },
482
+ async archive(id) {
483
+ const res = await fetch(`${root}/api/workspaces/${encodeURIComponent(cfg.projectKey)}/data-sources/${encodeURIComponent(id)}`, { method: "DELETE", headers });
484
+ if (!res.ok) {
485
+ throw new Error(`Apex dataSources.archive failed (${res.status}): ${await res.text().catch(() => "")}`);
486
+ }
487
+ },
488
+ async health(id) {
489
+ const res = await fetch(`${root}/api/workspaces/${encodeURIComponent(cfg.projectKey)}/data-sources/${encodeURIComponent(id)}/health`, { headers });
490
+ if (!res.ok) {
491
+ throw new Error(`Apex dataSources.health failed (${res.status}): ${await res.text().catch(() => "")}`);
492
+ }
493
+ const json = (await res.json());
494
+ return json.data;
495
+ },
496
+ },
497
+ /**
498
+ * Unified analytics reads — per-source / per-platform / per-hostname
499
+ * breakdowns from the ingest-maintained rollups. Honors the source/
500
+ * platform/hostname/range filters the dashboard exposes.
501
+ */
502
+ analytics: {
503
+ async sources(opts) {
504
+ const qs = new URLSearchParams();
505
+ if (opts?.days)
506
+ qs.set("days", String(opts.days));
507
+ if (opts?.source)
508
+ qs.set("source", opts.source);
509
+ const suffix = qs.toString() ? `?${qs}` : "";
510
+ const res = await fetch(`${root}/api/analytics/sources${suffix}`, {
511
+ headers,
512
+ });
513
+ if (!res.ok) {
514
+ throw new Error(`Apex analytics.sources failed (${res.status}): ${await res.text().catch(() => "")}`);
515
+ }
516
+ const json = (await res.json());
517
+ return json.data;
518
+ },
519
+ async hostnames(opts) {
520
+ const qs = opts?.limit ? `?limit=${opts.limit}` : "";
521
+ const res = await fetch(`${root}/api/analytics/hostnames${qs}`, {
522
+ headers,
523
+ });
524
+ if (!res.ok) {
525
+ throw new Error(`Apex analytics.hostnames failed (${res.status}): ${await res.text().catch(() => "")}`);
526
+ }
527
+ const json = (await res.json());
528
+ return json.data ?? [];
529
+ },
530
+ },
531
+ async signEndUserToken(params) {
532
+ const res = await fetch(`${root}/api/v1/tokens/end-user`, {
533
+ method: "POST",
534
+ headers,
535
+ body: JSON.stringify(params),
536
+ });
537
+ if (!res.ok) {
538
+ throw new Error(`Apex signEndUserToken failed (${res.status}): ${await res.text().catch(() => "")}`);
539
+ }
540
+ const json = (await res.json());
541
+ return json.data;
542
+ },
151
543
  };
152
544
  }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * MWO-009 — Apex PLG Event Catalog.
3
+ *
4
+ * Curated, typed event schemas for the eight most common PLG
5
+ * lifecycle moments. Customers import these interfaces directly so
6
+ * their `apex.track()` calls type-check end-to-end, and the Apex
7
+ * dashboard uses them to derive activation / expansion / retention
8
+ * rollups without bespoke queries per customer.
9
+ *
10
+ * import { EVENTS, type ActivatedEvent } from "@apex-inc/sdk/plg-events";
11
+ * apex.track(EVENTS.ACTIVATED, { daysToActivation: 2 } satisfies ActivatedEvent);
12
+ *
13
+ * Adding a new event:
14
+ * 1. Add its name to `EVENTS`.
15
+ * 2. Add its typed interface below.
16
+ * 3. Update the doc page at /docs/plg-event-catalog (Phase 10).
17
+ *
18
+ * Every event is optional. There's no requirement that a customer
19
+ * emit any of these — the catalog is a convention, not a gate.
20
+ * Archived event definitions (Phase 8 follow-up) will stop deriving
21
+ * new rollups but preserve their historical data.
22
+ */
23
+ export const EVENTS = {
24
+ SIGNUP: "signup",
25
+ ACTIVATED: "activated",
26
+ FEATURE_FIRST_USE: "feature_first_use",
27
+ AHA_MOMENT: "aha_moment",
28
+ EXPANSION: "expansion",
29
+ CONTRACTION: "contraction",
30
+ REACTIVATION: "reactivation",
31
+ CHURN: "churn",
32
+ };
33
+ /**
34
+ * Typed `apex.track` wrapper specific to PLG events. Not a hard gate —
35
+ * `apex.track()` still accepts any event name — but gives customers
36
+ * compile-time safety on the curated set.
37
+ */
38
+ export function trackPlgEvent(_apex, name, payload) {
39
+ _apex.track(name, payload);
40
+ }
41
+ /**
42
+ * Read-only exhaustive list of the catalog. Handy for UI surfaces
43
+ * that need to render "all PLG events we know about."
44
+ */
45
+ export const ALL_PLG_EVENTS = Object.values(EVENTS);