@mnemom/mnemom 0.10.0 → 0.12.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.
Files changed (40) hide show
  1. package/README.md +25 -25
  2. package/dist/commands/agents.js +2 -7
  3. package/dist/commands/api-key.d.ts +37 -0
  4. package/dist/commands/api-key.js +179 -0
  5. package/dist/commands/auth.js +1 -2
  6. package/dist/commands/card.js +33 -22
  7. package/dist/commands/governance.d.ts +85 -0
  8. package/dist/commands/governance.js +329 -0
  9. package/dist/commands/integrity.js +1 -2
  10. package/dist/commands/license.js +13 -4
  11. package/dist/commands/listen.d.ts +29 -0
  12. package/dist/commands/listen.js +201 -0
  13. package/dist/commands/logs.js +1 -3
  14. package/dist/commands/org.js +4 -6
  15. package/dist/commands/protection.js +70 -23
  16. package/dist/commands/recipes.d.ts +34 -0
  17. package/dist/commands/recipes.js +80 -0
  18. package/dist/commands/status.js +6 -169
  19. package/dist/commands/team.js +5 -10
  20. package/dist/commands/validate.js +7 -4
  21. package/dist/commands/webhooks.d.ts +61 -0
  22. package/dist/commands/webhooks.js +328 -0
  23. package/dist/index.js +489 -1
  24. package/dist/lib/api.d.ts +199 -2
  25. package/dist/lib/api.js +207 -0
  26. package/dist/lib/auth.js +1 -5
  27. package/dist/lib/format.d.ts +4 -0
  28. package/dist/lib/format.js +6 -0
  29. package/dist/lib/listen-stream.d.ts +40 -0
  30. package/dist/lib/listen-stream.js +101 -0
  31. package/dist/lib/webhooks-api.d.ts +80 -0
  32. package/dist/lib/webhooks-api.js +172 -0
  33. package/dist/smoltbot-shim.js +3 -3
  34. package/package.json +1 -1
  35. package/dist/lib/model-cache.d.ts +0 -16
  36. package/dist/lib/model-cache.js +0 -137
  37. package/dist/lib/models.d.ts +0 -41
  38. package/dist/lib/models.js +0 -357
  39. package/dist/lib/openclaw.d.ts +0 -221
  40. package/dist/lib/openclaw.js +0 -474
package/dist/lib/api.d.ts CHANGED
@@ -18,7 +18,7 @@ export interface Agent {
18
18
  created_at: string;
19
19
  }
20
20
  /**
21
- * Per docs.mnemom.ai/api-reference/openapi.json#components/schemas/IntegrityScore:
21
+ * Per api.mnemom.ai/openapi.json#components/schemas/IntegrityScore:
22
22
  *
23
23
  * { agent_id, total_traces, verified_traces, violation_count, integrity_score }
24
24
  *
@@ -344,7 +344,7 @@ export declare function testPolicyHistorical(agentId: string, policyJson: Record
344
344
  agent_id: string;
345
345
  policy_name: string;
346
346
  total_traces: number;
347
- results: any[];
347
+ results: unknown[];
348
348
  summary: {
349
349
  pass: number;
350
350
  warn: number;
@@ -645,4 +645,201 @@ export declare function getSafeHouseHarnessState(): Promise<{
645
645
  full: HarnessRunSummary | null;
646
646
  fast: HarnessRunSummary | null;
647
647
  }>;
648
+ export type GovernanceSignalScope = "platform" | "org" | "team" | "agent";
649
+ export type GovernanceSignalSource = "sideband.drift" | "sideband.coherence" | "sideband.fault_line" | "sideband.fleet";
650
+ export type GovernanceSignalSeverity = "info" | "warn" | "high" | "critical";
651
+ export type GovernanceSignalStatus = "open" | "acknowledged" | "resolved" | "dismissed" | "expired";
652
+ export type GovernanceResolutionStatus = "action_taken" | "wont_fix" | "duplicate" | "false_positive" | "self_resolved";
653
+ export type GovernanceNotificationChannel = "webhook" | "slack" | "email" | "pagerduty";
654
+ export interface GovernanceSignal {
655
+ id: string;
656
+ scope: GovernanceSignalScope;
657
+ scope_id: string;
658
+ source: GovernanceSignalSource;
659
+ pattern_type: string;
660
+ severity: GovernanceSignalSeverity;
661
+ detected_at: string;
662
+ detected_by: string;
663
+ org_id: string;
664
+ team_id: string | null;
665
+ agent_ids: string[];
666
+ detail: Record<string, unknown>;
667
+ source_ref: Record<string, unknown>;
668
+ status: GovernanceSignalStatus;
669
+ acknowledged_by: string | null;
670
+ acknowledged_at: string | null;
671
+ acknowledged_actor_role: string | null;
672
+ resolution_status: GovernanceResolutionStatus | null;
673
+ action_taken: string | null;
674
+ resolved_by: string | null;
675
+ resolved_at: string | null;
676
+ expires_at: string | null;
677
+ notification_state: Record<string, unknown>;
678
+ created_at: string;
679
+ updated_at: string;
680
+ }
681
+ export interface GovernanceDestination {
682
+ id: string;
683
+ org_id: string;
684
+ channel: GovernanceNotificationChannel;
685
+ config: Record<string, unknown>;
686
+ filter: Record<string, unknown>;
687
+ enabled: boolean;
688
+ display_name: string | null;
689
+ last_tested_at: string | null;
690
+ last_test_status: "ok" | "failed" | null;
691
+ last_test_error: string | null;
692
+ created_at: string;
693
+ updated_at: string;
694
+ }
695
+ export interface GovernanceRule {
696
+ id: string;
697
+ org_id: string;
698
+ name: string;
699
+ predicate: Record<string, unknown>;
700
+ destination_ids: string[];
701
+ enabled: boolean;
702
+ last_fired_at: string | null;
703
+ fire_count: number;
704
+ created_at: string;
705
+ updated_at: string;
706
+ }
707
+ interface SignalListFilters {
708
+ source?: string;
709
+ severity?: string;
710
+ status?: string;
711
+ scope?: string;
712
+ pattern_type?: string;
713
+ since?: string;
714
+ limit?: number;
715
+ }
716
+ export declare function listGovernanceSignalsForOrg(orgId: string, opts?: SignalListFilters): Promise<{
717
+ org_id: string;
718
+ signals: GovernanceSignal[];
719
+ }>;
720
+ export declare function listGovernanceSignalsForTeam(teamId: string, opts?: SignalListFilters): Promise<{
721
+ team_id: string;
722
+ signals: GovernanceSignal[];
723
+ }>;
724
+ export declare function listGovernanceSignalsForAgent(agentId: string, opts?: SignalListFilters): Promise<{
725
+ agent_id: string;
726
+ signals: GovernanceSignal[];
727
+ }>;
728
+ export declare function getGovernanceSignal(id: string): Promise<GovernanceSignal>;
729
+ export declare function acknowledgeGovernanceSignal(id: string, body?: {
730
+ action_taken?: string;
731
+ }): Promise<GovernanceSignal>;
732
+ export declare function resolveGovernanceSignal(id: string, body: {
733
+ resolution_status: GovernanceResolutionStatus;
734
+ action_taken?: string;
735
+ }): Promise<GovernanceSignal>;
736
+ export declare function dismissGovernanceSignal(id: string, body?: {
737
+ reason?: string;
738
+ }): Promise<GovernanceSignal>;
739
+ export declare function listGovernanceDestinations(orgId: string): Promise<{
740
+ org_id: string;
741
+ destinations: GovernanceDestination[];
742
+ }>;
743
+ export declare function createGovernanceDestination(orgId: string, body: {
744
+ channel: GovernanceNotificationChannel;
745
+ config: Record<string, unknown>;
746
+ filter?: Record<string, unknown>;
747
+ display_name?: string;
748
+ enabled?: boolean;
749
+ }): Promise<GovernanceDestination>;
750
+ export declare function deleteGovernanceDestination(orgId: string, destinationId: string): Promise<{
751
+ ok: true;
752
+ }>;
753
+ export declare function testGovernanceDestination(orgId: string, destinationId: string): Promise<{
754
+ destination_id: string;
755
+ channel: GovernanceNotificationChannel;
756
+ result: {
757
+ ok: boolean;
758
+ attempts: number;
759
+ delivered_at?: string;
760
+ last_error?: string;
761
+ };
762
+ }>;
763
+ export declare function listGovernanceRules(orgId: string): Promise<{
764
+ org_id: string;
765
+ rules: GovernanceRule[];
766
+ }>;
767
+ export declare function createGovernanceRule(orgId: string, body: {
768
+ name: string;
769
+ predicate: Record<string, unknown>;
770
+ destination_ids: string[];
771
+ enabled?: boolean;
772
+ }): Promise<GovernanceRule>;
773
+ export declare function deleteGovernanceRule(orgId: string, ruleId: string): Promise<{
774
+ ok: true;
775
+ }>;
776
+ /**
777
+ * Capability-based scope vocabulary (ADR-049). Mirrors the API's
778
+ * VALID_SCOPES exactly. Update both surfaces together when the
779
+ * vocabulary changes.
780
+ */
781
+ export declare const API_KEY_SCOPES: readonly ["gateway", "api:read", "api:write", "admin:org", "admin:platform"];
782
+ export type ApiKeyScope = (typeof API_KEY_SCOPES)[number];
783
+ export declare const DEFAULT_API_KEY_SCOPES: ApiKeyScope[];
784
+ /**
785
+ * Recognize legacy two-scope sets so the CLI can annotate pre-ADR-049
786
+ * keys appropriately. Mirror of mnemom-api `expandLegacyScopes` logic
787
+ * for display purposes; the auth gate handles the actual aliasing.
788
+ */
789
+ export declare function isLegacyScopeSet(scopes: string[] | undefined): boolean;
790
+ export interface ApiKeyListItem {
791
+ key_id: string;
792
+ key_prefix: string;
793
+ name: string;
794
+ scopes: string[];
795
+ created_at: string;
796
+ last_used_at: string | null;
797
+ org_id?: string | null;
798
+ }
799
+ export interface ApiKeyCreated {
800
+ key_id: string;
801
+ key: string;
802
+ key_prefix: string;
803
+ name: string;
804
+ scopes: string[];
805
+ created_at: string;
806
+ }
807
+ /**
808
+ * GET /v1/api-keys — list the caller's active personal API keys.
809
+ */
810
+ export declare function listApiKeys(): Promise<ApiKeyListItem[]>;
811
+ /**
812
+ * POST /v1/api-keys — mint a new personal API key with explicit scopes.
813
+ *
814
+ * Returns the full secret only on this call. The mint-time ceiling
815
+ * rejects admin scopes the caller is not eligible for (admin:platform
816
+ * for non-staff, admin:org for users not in any org-admin role).
817
+ */
818
+ export declare function createApiKey(name: string, scopes: ApiKeyScope[]): Promise<ApiKeyCreated>;
819
+ /**
820
+ * POST /v1/api-keys/{key_id}/rotate — atomic mint-new + revoke-old.
821
+ * Returns the full new secret only on this call. The new key inherits
822
+ * the old key's name and scopes verbatim; the old key is revoked the
823
+ * moment this returns.
824
+ */
825
+ export declare function rotateApiKey(keyId: string): Promise<ApiKeyCreated>;
826
+ /**
827
+ * DELETE /v1/api-keys/{key_id} — soft-revoke. The key row stays for
828
+ * audit; `is_active` flips to false and `revoked_at` is timestamped.
829
+ */
830
+ export declare function revokeApiKey(keyId: string): Promise<void>;
831
+ export interface RecipeReportInput {
832
+ type: "fn" | "fp";
833
+ summary: string;
834
+ evidence?: string;
835
+ agent_id?: string;
836
+ checkpoint_id?: string;
837
+ }
838
+ export interface RecipeReportResult {
839
+ ok: true;
840
+ candidate_id: string;
841
+ type: "fn" | "fp";
842
+ related_recipe_id: string;
843
+ }
844
+ export declare function reportRecipeFnFp(recipeId: string, input: RecipeReportInput): Promise<RecipeReportResult>;
648
845
  export {};
package/dist/lib/api.js CHANGED
@@ -1162,3 +1162,210 @@ export async function getSafeHouseHarnessState() {
1162
1162
  }
1163
1163
  return (await response.json());
1164
1164
  }
1165
+ function buildSignalListQuery(opts = {}) {
1166
+ const params = new URLSearchParams();
1167
+ if (opts.source)
1168
+ params.set("source", opts.source);
1169
+ if (opts.severity)
1170
+ params.set("severity", opts.severity);
1171
+ if (opts.status)
1172
+ params.set("status", opts.status);
1173
+ if (opts.scope)
1174
+ params.set("scope", opts.scope);
1175
+ if (opts.pattern_type)
1176
+ params.set("pattern_type", opts.pattern_type);
1177
+ if (opts.since)
1178
+ params.set("since", opts.since);
1179
+ if (opts.limit)
1180
+ params.set("limit", String(opts.limit));
1181
+ const qs = params.toString();
1182
+ return qs ? `?${qs}` : "";
1183
+ }
1184
+ async function gFetch(path, init = {}, notFoundLabel) {
1185
+ const url = validateUrl(`${API_BASE}${path}`);
1186
+ const response = await fetchWithAuthRetry(url, async () => ({
1187
+ method: init.method ?? "GET",
1188
+ headers: {
1189
+ ...(await authHeaders()),
1190
+ Accept: "application/json",
1191
+ ...(init.body ? { "Content-Type": "application/json" } : {}),
1192
+ ...(init.headers ?? {}),
1193
+ },
1194
+ body: init.body,
1195
+ }));
1196
+ if (!response.ok) {
1197
+ if (response.status === 401)
1198
+ throw new Error("Not authenticated.");
1199
+ if (response.status === 403)
1200
+ throw new Error("Permission denied (need org admin / membership).");
1201
+ if (response.status === 404 && notFoundLabel)
1202
+ throw new Error(`${notFoundLabel} not found.`);
1203
+ const err = (await response.json().catch(() => ({ error: "unknown" })));
1204
+ throw new Error(err.message || `Request failed: ${response.status}`);
1205
+ }
1206
+ return (await response.json());
1207
+ }
1208
+ export async function listGovernanceSignalsForOrg(orgId, opts = {}) {
1209
+ return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/signals${buildSignalListQuery(opts)}`, {}, "Org");
1210
+ }
1211
+ export async function listGovernanceSignalsForTeam(teamId, opts = {}) {
1212
+ return gFetch(`/v1/teams/${encodeURIComponent(teamId)}/governance/signals${buildSignalListQuery(opts)}`, {}, "Team");
1213
+ }
1214
+ export async function listGovernanceSignalsForAgent(agentId, opts = {}) {
1215
+ return gFetch(`/v1/agents/${encodeURIComponent(agentId)}/governance/signals${buildSignalListQuery(opts)}`, {}, "Agent");
1216
+ }
1217
+ export async function getGovernanceSignal(id) {
1218
+ return gFetch(`/v1/governance/signals/${encodeURIComponent(id)}`, {}, "Signal");
1219
+ }
1220
+ export async function acknowledgeGovernanceSignal(id, body = {}) {
1221
+ return gFetch(`/v1/governance/signals/${encodeURIComponent(id)}/acknowledge`, { method: "POST", body: JSON.stringify(body) }, "Signal");
1222
+ }
1223
+ export async function resolveGovernanceSignal(id, body) {
1224
+ return gFetch(`/v1/governance/signals/${encodeURIComponent(id)}/resolve`, { method: "POST", body: JSON.stringify(body) }, "Signal");
1225
+ }
1226
+ export async function dismissGovernanceSignal(id, body = {}) {
1227
+ return gFetch(`/v1/governance/signals/${encodeURIComponent(id)}/dismiss`, { method: "POST", body: JSON.stringify(body) }, "Signal");
1228
+ }
1229
+ export async function listGovernanceDestinations(orgId) {
1230
+ return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/notification-destinations`, {}, "Org");
1231
+ }
1232
+ export async function createGovernanceDestination(orgId, body) {
1233
+ return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/notification-destinations`, { method: "POST", body: JSON.stringify(body) }, "Org");
1234
+ }
1235
+ export async function deleteGovernanceDestination(orgId, destinationId) {
1236
+ return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/notification-destinations/${encodeURIComponent(destinationId)}`, { method: "DELETE" }, "Destination");
1237
+ }
1238
+ export async function testGovernanceDestination(orgId, destinationId) {
1239
+ return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/notification-destinations/${encodeURIComponent(destinationId)}/test`, { method: "POST", body: JSON.stringify({}) }, "Destination");
1240
+ }
1241
+ export async function listGovernanceRules(orgId) {
1242
+ return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/escalation-rules`, {}, "Org");
1243
+ }
1244
+ export async function createGovernanceRule(orgId, body) {
1245
+ return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/escalation-rules`, { method: "POST", body: JSON.stringify(body) }, "Org");
1246
+ }
1247
+ export async function deleteGovernanceRule(orgId, ruleId) {
1248
+ return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/escalation-rules/${encodeURIComponent(ruleId)}`, { method: "DELETE" }, "Rule");
1249
+ }
1250
+ // ─── api-keys (ADR-049) ─────────────────────────────────────────────────
1251
+ /**
1252
+ * Capability-based scope vocabulary (ADR-049). Mirrors the API's
1253
+ * VALID_SCOPES exactly. Update both surfaces together when the
1254
+ * vocabulary changes.
1255
+ */
1256
+ export const API_KEY_SCOPES = [
1257
+ "gateway",
1258
+ "api:read",
1259
+ "api:write",
1260
+ "admin:org",
1261
+ "admin:platform",
1262
+ ];
1263
+ export const DEFAULT_API_KEY_SCOPES = ["gateway", "api:read", "api:write"];
1264
+ /**
1265
+ * Recognize legacy two-scope sets so the CLI can annotate pre-ADR-049
1266
+ * keys appropriately. Mirror of mnemom-api `expandLegacyScopes` logic
1267
+ * for display purposes; the auth gate handles the actual aliasing.
1268
+ */
1269
+ export function isLegacyScopeSet(scopes) {
1270
+ if (!scopes || scopes.length === 0)
1271
+ return false;
1272
+ if (scopes.length === 2 && scopes.includes("gateway") && scopes.includes("api")) {
1273
+ return true;
1274
+ }
1275
+ if (scopes.length === 1 && scopes[0] === "api")
1276
+ return true;
1277
+ return false;
1278
+ }
1279
+ /**
1280
+ * GET /v1/api-keys — list the caller's active personal API keys.
1281
+ */
1282
+ export async function listApiKeys() {
1283
+ const url = validateUrl(`${API_BASE}/v1/api-keys`);
1284
+ const response = await fetchWithAuthRetry(url, async () => ({
1285
+ headers: await authHeaders(),
1286
+ }));
1287
+ if (!response.ok) {
1288
+ if (response.status === 401) {
1289
+ throw new Error("Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
1290
+ }
1291
+ const err = (await response.json().catch(() => ({ error: "unknown" })));
1292
+ throw new Error(err.message || `Failed to list api keys: ${response.status}`);
1293
+ }
1294
+ const data = (await response.json());
1295
+ return data.keys ?? [];
1296
+ }
1297
+ /**
1298
+ * POST /v1/api-keys — mint a new personal API key with explicit scopes.
1299
+ *
1300
+ * Returns the full secret only on this call. The mint-time ceiling
1301
+ * rejects admin scopes the caller is not eligible for (admin:platform
1302
+ * for non-staff, admin:org for users not in any org-admin role).
1303
+ */
1304
+ export async function createApiKey(name, scopes) {
1305
+ const url = validateUrl(`${API_BASE}/v1/api-keys`);
1306
+ const response = await fetchWithAuthRetry(url, async () => ({
1307
+ method: "POST",
1308
+ headers: { ...(await authHeaders()), "Content-Type": "application/json" },
1309
+ body: JSON.stringify({ name, scopes }),
1310
+ }));
1311
+ if (!response.ok) {
1312
+ if (response.status === 401) {
1313
+ throw new Error("Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
1314
+ }
1315
+ const body = await response.text().catch(() => "");
1316
+ if (response.status === 403) {
1317
+ throw new Error(`Mint-time ceiling rejected scope: ${body}`);
1318
+ }
1319
+ if (response.status === 400) {
1320
+ throw new Error(`Invalid scope(s): ${body}`);
1321
+ }
1322
+ throw new Error(`Failed to create api key: ${response.status} ${body}`);
1323
+ }
1324
+ return (await response.json());
1325
+ }
1326
+ /**
1327
+ * POST /v1/api-keys/{key_id}/rotate — atomic mint-new + revoke-old.
1328
+ * Returns the full new secret only on this call. The new key inherits
1329
+ * the old key's name and scopes verbatim; the old key is revoked the
1330
+ * moment this returns.
1331
+ */
1332
+ export async function rotateApiKey(keyId) {
1333
+ const url = validateUrl(`${API_BASE}/v1/api-keys/${encodeURIComponent(keyId)}/rotate`);
1334
+ const response = await fetchWithAuthRetry(url, async () => ({
1335
+ method: "POST",
1336
+ headers: { ...(await authHeaders()), "Content-Type": "application/json" },
1337
+ }));
1338
+ if (!response.ok) {
1339
+ if (response.status === 401) {
1340
+ throw new Error("Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
1341
+ }
1342
+ if (response.status === 404)
1343
+ throw new Error(`Key not found: ${keyId}`);
1344
+ const body = await response.text().catch(() => "");
1345
+ throw new Error(`Failed to rotate api key: ${response.status} ${body}`);
1346
+ }
1347
+ return (await response.json());
1348
+ }
1349
+ /**
1350
+ * DELETE /v1/api-keys/{key_id} — soft-revoke. The key row stays for
1351
+ * audit; `is_active` flips to false and `revoked_at` is timestamped.
1352
+ */
1353
+ export async function revokeApiKey(keyId) {
1354
+ const url = validateUrl(`${API_BASE}/v1/api-keys/${encodeURIComponent(keyId)}`);
1355
+ const response = await fetchWithAuthRetry(url, async () => ({
1356
+ method: "DELETE",
1357
+ headers: await authHeaders(),
1358
+ }));
1359
+ if (!response.ok) {
1360
+ if (response.status === 401) {
1361
+ throw new Error("Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
1362
+ }
1363
+ if (response.status === 404)
1364
+ throw new Error(`Key not found: ${keyId}`);
1365
+ const body = await response.text().catch(() => "");
1366
+ throw new Error(`Failed to revoke api key: ${response.status} ${body}`);
1367
+ }
1368
+ }
1369
+ export async function reportRecipeFnFp(recipeId, input) {
1370
+ return postApi(`/v1/recipes/${encodeURIComponent(recipeId)}/report`, input);
1371
+ }
package/dist/lib/auth.js CHANGED
@@ -311,11 +311,7 @@ async function startCallbackServer(expectedState) {
311
311
  };
312
312
  }
313
313
  function openBrowser(url) {
314
- const cmd = process.platform === "darwin"
315
- ? "open"
316
- : process.platform === "win32"
317
- ? "start"
318
- : "xdg-open";
314
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
319
315
  exec(`${cmd} ${JSON.stringify(url)}`);
320
316
  }
321
317
  // ============================================================================
@@ -24,6 +24,10 @@ export declare const fmt: {
24
24
  * Dim label with value
25
25
  */
26
26
  label(key: string, val: string): string;
27
+ /**
28
+ * Dim text (for inline annotations and footnotes).
29
+ */
30
+ dim(text: string): string;
27
31
  /**
28
32
  * Syntax-highlighted JSON output
29
33
  */
@@ -38,6 +38,12 @@ export const fmt = {
38
38
  label(key, val) {
39
39
  return `${chalk.dim(key)} ${val}`;
40
40
  },
41
+ /**
42
+ * Dim text (for inline annotations and footnotes).
43
+ */
44
+ dim(text) {
45
+ return chalk.dim(text);
46
+ },
41
47
  /**
42
48
  * Syntax-highlighted JSON output
43
49
  */
@@ -0,0 +1,40 @@
1
+ /**
2
+ * SSE event-stream consumer for `mnemom listen` (Track 2 W3.4b).
3
+ *
4
+ * Parses `text/event-stream` framing into `SseFrame` objects per the
5
+ * W3C EventSource spec subset we need:
6
+ *
7
+ * - Events delimited by `\n\n`.
8
+ * - Lines starting with `:` are comments (heartbeats).
9
+ * - `event: <name>` sets the frame's event name (default "message").
10
+ * - `data: <json>` accumulates into a multi-line payload.
11
+ * - `id: <cursor>` sets the frame's last-event-id.
12
+ *
13
+ * Exposed as a generator so callers can iterate with `for await`.
14
+ */
15
+ export interface SseFrame {
16
+ event: string;
17
+ /** Multi-line `data:` payload joined with `\n`. */
18
+ data: string;
19
+ /** The `id:` cursor (for Last-Event-ID reconnect). */
20
+ id?: string;
21
+ }
22
+ /**
23
+ * Consume a fetch Response body and yield SSE frames.
24
+ *
25
+ * Buffers across chunk boundaries — SSE delimiters can land mid-chunk.
26
+ */
27
+ export declare function parseSseStream(body: ReadableStream<Uint8Array>): AsyncGenerator<SseFrame, void, void>;
28
+ /**
29
+ * Re-sign a webhook event body with a local-only secret. Mirrors the
30
+ * platform's HMAC-SHA256 over `${ts}.${rawBody}` convention so the
31
+ * receiver verifies cleanly with the same scheme.
32
+ *
33
+ * Used by `mnemom listen --forward-to <url> --secret <hex>` to make
34
+ * the local receiver behave identically to a production webhook
35
+ * subscriber.
36
+ */
37
+ export declare function reSignDelivery(rawBody: string, signingSecret: string, nowSeconds?: number): {
38
+ timestamp: string;
39
+ signature: string;
40
+ };
@@ -0,0 +1,101 @@
1
+ /**
2
+ * SSE event-stream consumer for `mnemom listen` (Track 2 W3.4b).
3
+ *
4
+ * Parses `text/event-stream` framing into `SseFrame` objects per the
5
+ * W3C EventSource spec subset we need:
6
+ *
7
+ * - Events delimited by `\n\n`.
8
+ * - Lines starting with `:` are comments (heartbeats).
9
+ * - `event: <name>` sets the frame's event name (default "message").
10
+ * - `data: <json>` accumulates into a multi-line payload.
11
+ * - `id: <cursor>` sets the frame's last-event-id.
12
+ *
13
+ * Exposed as a generator so callers can iterate with `for await`.
14
+ */
15
+ import { createHmac } from "node:crypto";
16
+ /**
17
+ * Consume a fetch Response body and yield SSE frames.
18
+ *
19
+ * Buffers across chunk boundaries — SSE delimiters can land mid-chunk.
20
+ */
21
+ export async function* parseSseStream(body) {
22
+ const reader = body.getReader();
23
+ const decoder = new TextDecoder();
24
+ let buf = "";
25
+ try {
26
+ while (true) {
27
+ const { done, value } = await reader.read();
28
+ if (done)
29
+ break;
30
+ buf += decoder.decode(value, { stream: true });
31
+ // Frames end at \n\n. Process all complete frames; keep the trailing partial.
32
+ let idx;
33
+ while ((idx = buf.indexOf("\n\n")) !== -1) {
34
+ const frameRaw = buf.slice(0, idx);
35
+ buf = buf.slice(idx + 2);
36
+ const frame = parseFrame(frameRaw);
37
+ if (frame)
38
+ yield frame;
39
+ }
40
+ }
41
+ // Drain trailing partial — usually a comment line right before the
42
+ // server-side close. Process if it parses cleanly.
43
+ if (buf.length > 0) {
44
+ const frame = parseFrame(buf);
45
+ if (frame)
46
+ yield frame;
47
+ }
48
+ }
49
+ finally {
50
+ try {
51
+ await reader.cancel();
52
+ }
53
+ catch {
54
+ /* socket already torn down */
55
+ }
56
+ }
57
+ }
58
+ function parseFrame(raw) {
59
+ const lines = raw.split("\n");
60
+ let event = "message";
61
+ let id;
62
+ const dataLines = [];
63
+ let hadDataField = false;
64
+ for (const line of lines) {
65
+ if (line.length === 0)
66
+ continue;
67
+ if (line.startsWith(":"))
68
+ continue; // heartbeat comment
69
+ const colonIdx = line.indexOf(":");
70
+ if (colonIdx === -1)
71
+ continue;
72
+ const field = line.slice(0, colonIdx);
73
+ const value = line.slice(colonIdx + 1).replace(/^ /, ""); // strip optional leading space
74
+ if (field === "event")
75
+ event = value;
76
+ else if (field === "data") {
77
+ dataLines.push(value);
78
+ hadDataField = true;
79
+ }
80
+ else if (field === "id")
81
+ id = value;
82
+ }
83
+ if (!hadDataField)
84
+ return null;
85
+ return { event, data: dataLines.join("\n"), id };
86
+ }
87
+ // ─── HMAC re-signing for local forwarding ───────────────────────────────
88
+ /**
89
+ * Re-sign a webhook event body with a local-only secret. Mirrors the
90
+ * platform's HMAC-SHA256 over `${ts}.${rawBody}` convention so the
91
+ * receiver verifies cleanly with the same scheme.
92
+ *
93
+ * Used by `mnemom listen --forward-to <url> --secret <hex>` to make
94
+ * the local receiver behave identically to a production webhook
95
+ * subscriber.
96
+ */
97
+ export function reSignDelivery(rawBody, signingSecret, nowSeconds = Math.floor(Date.now() / 1000)) {
98
+ const timestamp = String(nowSeconds);
99
+ const signature = "v1=" + createHmac("sha256", signingSecret).update(`${timestamp}.${rawBody}`).digest("hex");
100
+ return { timestamp, signature };
101
+ }
@@ -0,0 +1,80 @@
1
+ export interface WebhookEndpoint {
2
+ endpoint_id: string;
3
+ billing_account_id: string;
4
+ url: string;
5
+ description: string;
6
+ signing_secret?: string;
7
+ event_types: string[];
8
+ is_active: boolean;
9
+ consecutive_failures: number;
10
+ disabled_at: string | null;
11
+ disabled_reason: string | null;
12
+ created_at: string;
13
+ updated_at: string;
14
+ }
15
+ export interface WebhookDelivery {
16
+ delivery_id: string;
17
+ event_id: string;
18
+ endpoint_id: string;
19
+ status: string;
20
+ attempt_count?: number;
21
+ last_attempt_at?: string | null;
22
+ next_attempt_at?: string | null;
23
+ last_status_code?: number | null;
24
+ last_error?: string | null;
25
+ created_at?: string;
26
+ }
27
+ export interface WebhookReplayResponse {
28
+ event_id: string;
29
+ event_type: string;
30
+ deliveries: Array<{
31
+ delivery_id: string;
32
+ endpoint_id: string;
33
+ }>;
34
+ failed_endpoints?: Array<{
35
+ endpoint_id: string;
36
+ error: string;
37
+ }>;
38
+ message?: string;
39
+ }
40
+ export interface TestDeliveryResult {
41
+ success: boolean;
42
+ status: number | null;
43
+ latency_ms: number | null;
44
+ error: string | null;
45
+ }
46
+ export declare function listWebhookEndpoints(orgId: string): Promise<WebhookEndpoint[]>;
47
+ export declare function getWebhookEndpoint(orgId: string, endpointId: string): Promise<WebhookEndpoint>;
48
+ export declare function createWebhookEndpoint(orgId: string, body: {
49
+ url: string;
50
+ description?: string;
51
+ event_types?: string[];
52
+ }): Promise<WebhookEndpoint>;
53
+ export declare function updateWebhookEndpoint(orgId: string, endpointId: string, body: {
54
+ url?: string;
55
+ description?: string;
56
+ event_types?: string[];
57
+ is_active?: boolean;
58
+ }): Promise<WebhookEndpoint>;
59
+ export declare function deleteWebhookEndpoint(orgId: string, endpointId: string): Promise<{
60
+ deleted: true;
61
+ endpoint_id: string;
62
+ }>;
63
+ export declare function rotateWebhookSecret(orgId: string, endpointId: string): Promise<{
64
+ endpoint_id: string;
65
+ signing_secret: string;
66
+ }>;
67
+ export declare function testWebhookEndpoint(orgId: string, endpointId: string): Promise<TestDeliveryResult>;
68
+ export declare function listWebhookDeliveries(orgId: string, opts?: {
69
+ endpointId?: string;
70
+ limit?: number;
71
+ offset?: number;
72
+ }): Promise<WebhookDelivery[]>;
73
+ export declare function redeliverWebhookEvent(orgId: string, deliveryId: string): Promise<{
74
+ delivery_id: string;
75
+ event_id: string;
76
+ status: string;
77
+ }>;
78
+ export declare function replayWebhookEvent(orgId: string, eventId: string, opts?: {
79
+ endpointIds?: string[];
80
+ }): Promise<WebhookReplayResponse>;