crewly 1.20.121 → 1.20.123

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 (78) hide show
  1. package/config/skills/agent/core/whatsapp-draft/SKILL.md +80 -0
  2. package/config/skills/agent/core/whatsapp-draft/execute.sh +84 -0
  3. package/config/skills/agent/core/whatsapp-draft/execute.test.sh +70 -0
  4. package/config/skills/agent/core/whatsapp-inbox/SKILL.md +75 -0
  5. package/config/skills/agent/core/whatsapp-inbox/execute.sh +87 -0
  6. package/config/skills/agent/core/whatsapp-inbox/execute.test.sh +68 -0
  7. package/config/skills/agent/core/whatsapp-read/SKILL.md +80 -0
  8. package/config/skills/agent/core/whatsapp-read/execute.sh +116 -0
  9. package/config/skills/agent/core/whatsapp-read/execute.test.sh +84 -0
  10. package/config/skills/agent/core/whatsapp-send/SKILL.md +70 -0
  11. package/config/skills/agent/core/whatsapp-send/execute.sh +84 -0
  12. package/config/skills/agent/core/whatsapp-send/execute.test.sh +79 -0
  13. package/dist/backend/backend/src/constants.d.ts +107 -0
  14. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  15. package/dist/backend/backend/src/constants.js +105 -0
  16. package/dist/backend/backend/src/constants.js.map +1 -1
  17. package/dist/backend/backend/src/controllers/whatsapp/whatsapp-inbox.controller.d.ts +73 -0
  18. package/dist/backend/backend/src/controllers/whatsapp/whatsapp-inbox.controller.d.ts.map +1 -0
  19. package/dist/backend/backend/src/controllers/whatsapp/whatsapp-inbox.controller.js +310 -0
  20. package/dist/backend/backend/src/controllers/whatsapp/whatsapp-inbox.controller.js.map +1 -0
  21. package/dist/backend/backend/src/controllers/whatsapp/whatsapp.controller.d.ts.map +1 -1
  22. package/dist/backend/backend/src/controllers/whatsapp/whatsapp.controller.js +53 -4
  23. package/dist/backend/backend/src/controllers/whatsapp/whatsapp.controller.js.map +1 -1
  24. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
  25. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js +5 -0
  26. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
  27. package/dist/backend/backend/src/services/browser/browser-session.service.d.ts.map +1 -1
  28. package/dist/backend/backend/src/services/browser/browser-session.service.js +13 -0
  29. package/dist/backend/backend/src/services/browser/browser-session.service.js.map +1 -1
  30. package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.d.ts.map +1 -1
  31. package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js +29 -1
  32. package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js.map +1 -1
  33. package/dist/backend/backend/src/services/whatsapp/index.d.ts +5 -1
  34. package/dist/backend/backend/src/services/whatsapp/index.d.ts.map +1 -1
  35. package/dist/backend/backend/src/services/whatsapp/index.js +5 -1
  36. package/dist/backend/backend/src/services/whatsapp/index.js.map +1 -1
  37. package/dist/backend/backend/src/services/whatsapp/whatsapp-connection-config.d.ts +59 -0
  38. package/dist/backend/backend/src/services/whatsapp/whatsapp-connection-config.d.ts.map +1 -0
  39. package/dist/backend/backend/src/services/whatsapp/whatsapp-connection-config.js +108 -0
  40. package/dist/backend/backend/src/services/whatsapp/whatsapp-connection-config.js.map +1 -0
  41. package/dist/backend/backend/src/services/whatsapp/whatsapp-draft-gate.d.ts +74 -0
  42. package/dist/backend/backend/src/services/whatsapp/whatsapp-draft-gate.d.ts.map +1 -0
  43. package/dist/backend/backend/src/services/whatsapp/whatsapp-draft-gate.js +82 -0
  44. package/dist/backend/backend/src/services/whatsapp/whatsapp-draft-gate.js.map +1 -0
  45. package/dist/backend/backend/src/services/whatsapp/whatsapp-inbox-capture.d.ts +184 -0
  46. package/dist/backend/backend/src/services/whatsapp/whatsapp-inbox-capture.d.ts.map +1 -0
  47. package/dist/backend/backend/src/services/whatsapp/whatsapp-inbox-capture.js +420 -0
  48. package/dist/backend/backend/src/services/whatsapp/whatsapp-inbox-capture.js.map +1 -0
  49. package/dist/backend/backend/src/services/whatsapp/whatsapp-inbox.store.d.ts +243 -0
  50. package/dist/backend/backend/src/services/whatsapp/whatsapp-inbox.store.d.ts.map +1 -0
  51. package/dist/backend/backend/src/services/whatsapp/whatsapp-inbox.store.js +538 -0
  52. package/dist/backend/backend/src/services/whatsapp/whatsapp-inbox.store.js.map +1 -0
  53. package/dist/backend/backend/src/services/whatsapp/whatsapp-initializer.d.ts +32 -2
  54. package/dist/backend/backend/src/services/whatsapp/whatsapp-initializer.d.ts.map +1 -1
  55. package/dist/backend/backend/src/services/whatsapp/whatsapp-initializer.js +65 -9
  56. package/dist/backend/backend/src/services/whatsapp/whatsapp-initializer.js.map +1 -1
  57. package/dist/backend/backend/src/services/whatsapp/whatsapp-orchestrator-bridge.d.ts.map +1 -1
  58. package/dist/backend/backend/src/services/whatsapp/whatsapp-orchestrator-bridge.js +11 -0
  59. package/dist/backend/backend/src/services/whatsapp/whatsapp-orchestrator-bridge.js.map +1 -1
  60. package/dist/backend/backend/src/services/whatsapp/whatsapp.service.d.ts +59 -1
  61. package/dist/backend/backend/src/services/whatsapp/whatsapp.service.d.ts.map +1 -1
  62. package/dist/backend/backend/src/services/whatsapp/whatsapp.service.js +159 -9
  63. package/dist/backend/backend/src/services/whatsapp/whatsapp.service.js.map +1 -1
  64. package/dist/backend/backend/src/types/whatsapp.types.d.ts +108 -0
  65. package/dist/backend/backend/src/types/whatsapp.types.d.ts.map +1 -1
  66. package/dist/backend/backend/src/types/whatsapp.types.js +9 -0
  67. package/dist/backend/backend/src/types/whatsapp.types.js.map +1 -1
  68. package/dist/backend/build-info.json +2 -2
  69. package/dist/cli/backend/src/constants.d.ts +107 -0
  70. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  71. package/dist/cli/backend/src/constants.js +105 -0
  72. package/dist/cli/backend/src/constants.js.map +1 -1
  73. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
  74. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js +5 -0
  75. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
  76. package/frontend/dist/assets/{index-07e338a9.js → index-daf43f1a.js} +268 -268
  77. package/frontend/dist/index.html +1 -1
  78. package/package.json +1 -1
@@ -0,0 +1,108 @@
1
+ /**
2
+ * WhatsApp Connection Config Persistence
3
+ *
4
+ * Stores how the owner last connected WhatsApp (mode, allowed contacts,
5
+ * whether to reconnect on boot) at `~/.crewly/whatsapp/connection.json`, so
6
+ * a restart keeps inbox mode instead of silently falling back to the
7
+ * auto-replying assistant.
8
+ *
9
+ * @module services/whatsapp/whatsapp-connection-config
10
+ */
11
+ import { promises as fs, existsSync } from 'fs';
12
+ import path from 'path';
13
+ import { WHATSAPP_CONSTANTS } from '../../constants.js';
14
+ import { getCrewlyHomePath } from '../core/crewly-home.utils.js';
15
+ import { isWhatsAppMode } from '../../types/whatsapp.types.js';
16
+ /** Owner-only file mode for the config file. */
17
+ const PRIVATE_FILE_MODE = 0o600;
18
+ const PRIVATE_DIR_MODE = 0o700;
19
+ /**
20
+ * Path of the persisted connection config.
21
+ *
22
+ * @returns `<crewly home>/whatsapp/connection.json`
23
+ */
24
+ export function getConnectionConfigPath() {
25
+ return path.join(getCrewlyHomePath(), WHATSAPP_CONSTANTS.DATA_DIR, WHATSAPP_CONSTANTS.CONNECTION_CONFIG_FILE);
26
+ }
27
+ /**
28
+ * Read the persisted connection config.
29
+ *
30
+ * @returns The config, or null when absent, unreadable or malformed
31
+ */
32
+ export async function loadWhatsAppConnection() {
33
+ const file = getConnectionConfigPath();
34
+ try {
35
+ if (!existsSync(file))
36
+ return null;
37
+ const parsed = JSON.parse(await fs.readFile(file, 'utf8'));
38
+ if (!parsed || typeof parsed !== 'object')
39
+ return null;
40
+ const p = parsed;
41
+ if (!isWhatsAppMode(p.mode))
42
+ return null;
43
+ return {
44
+ mode: p.mode,
45
+ autoConnect: p.autoConnect === true,
46
+ allowedContacts: Array.isArray(p.allowedContacts)
47
+ ? p.allowedContacts.filter((c) => typeof c === 'string')
48
+ : undefined,
49
+ phoneNumber: typeof p.phoneNumber === 'string' ? p.phoneNumber : undefined,
50
+ authStatePath: typeof p.authStatePath === 'string' ? p.authStatePath : undefined,
51
+ updatedAt: typeof p.updatedAt === 'number' ? p.updatedAt : 0,
52
+ };
53
+ }
54
+ catch {
55
+ return null;
56
+ }
57
+ }
58
+ /**
59
+ * Persist the config a connection was started with (auto-connect on).
60
+ *
61
+ * @param config - The config passed to `WhatsAppService.initialize`
62
+ * @param mode - The resolved mode
63
+ */
64
+ export async function saveWhatsAppConnection(config, mode) {
65
+ const record = {
66
+ mode,
67
+ autoConnect: true,
68
+ allowedContacts: config.allowedContacts,
69
+ phoneNumber: config.phoneNumber,
70
+ authStatePath: config.authStatePath,
71
+ updatedAt: Date.now(),
72
+ };
73
+ await writeConnection(record);
74
+ }
75
+ /**
76
+ * Turn auto-connect off after an explicit disconnect, keeping the mode.
77
+ */
78
+ export async function markWhatsAppDisconnected() {
79
+ const current = await loadWhatsAppConnection();
80
+ if (!current)
81
+ return;
82
+ await writeConnection({ ...current, autoConnect: false, updatedAt: Date.now() });
83
+ }
84
+ /**
85
+ * Convert a persisted record into a service config.
86
+ *
87
+ * @param persisted - Stored record
88
+ * @returns Config for `WhatsAppService.initialize`
89
+ */
90
+ export function toWhatsAppConfig(persisted) {
91
+ return {
92
+ mode: persisted.mode,
93
+ allowedContacts: persisted.allowedContacts,
94
+ phoneNumber: persisted.phoneNumber,
95
+ authStatePath: persisted.authStatePath,
96
+ };
97
+ }
98
+ /**
99
+ * Write the record owner-only.
100
+ *
101
+ * @param record - Record to write
102
+ */
103
+ async function writeConnection(record) {
104
+ const file = getConnectionConfigPath();
105
+ await fs.mkdir(path.dirname(file), { recursive: true, mode: PRIVATE_DIR_MODE });
106
+ await fs.writeFile(file, JSON.stringify(record, null, 2), { mode: PRIVATE_FILE_MODE });
107
+ }
108
+ //# sourceMappingURL=whatsapp-connection-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"whatsapp-connection-config.js","sourceRoot":"","sources":["../../../../../../backend/src/services/whatsapp/whatsapp-connection-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAChD,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACjE,OAAO,EAAE,cAAc,EAA0C,MAAM,+BAA+B,CAAC;AAEvG,gDAAgD;AAChD,MAAM,iBAAiB,GAAG,KAAK,CAAC;AAChC,MAAM,gBAAgB,GAAG,KAAK,CAAC;AAoB/B;;;;GAIG;AACH,MAAM,UAAU,uBAAuB;IACrC,OAAO,IAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE,EAAE,kBAAkB,CAAC,QAAQ,EAAE,kBAAkB,CAAC,sBAAsB,CAAC,CAAC;AAChH,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB;IAC1C,MAAM,IAAI,GAAG,uBAAuB,EAAE,CAAC;IACvC,IAAI,CAAC;QACH,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC;QACnC,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;QACpE,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAC;QACvD,MAAM,CAAC,GAAG,MAAiC,CAAC;QAC5C,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC;QACzC,OAAO;YACL,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,WAAW,EAAE,CAAC,CAAC,WAAW,KAAK,IAAI;YACnC,eAAe,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC;gBAC/C,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC;gBACrE,CAAC,CAAC,SAAS;YACb,WAAW,EAAE,OAAO,CAAC,CAAC,WAAW,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS;YAC1E,aAAa,EAAE,OAAO,CAAC,CAAC,aAAa,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,SAAS;YAChF,SAAS,EAAE,OAAO,CAAC,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;SAC7D,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAAC,MAAsB,EAAE,IAAkB;IACrF,MAAM,MAAM,GAAgC;QAC1C,IAAI;QACJ,WAAW,EAAE,IAAI;QACjB,eAAe,EAAE,MAAM,CAAC,eAAe;QACvC,WAAW,EAAE,MAAM,CAAC,WAAW;QAC/B,aAAa,EAAE,MAAM,CAAC,aAAa;QACnC,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;KACtB,CAAC;IACF,MAAM,eAAe,CAAC,MAAM,CAAC,CAAC;AAChC,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,wBAAwB;IAC5C,MAAM,OAAO,GAAG,MAAM,sBAAsB,EAAE,CAAC;IAC/C,IAAI,CAAC,OAAO;QAAE,OAAO;IACrB,MAAM,eAAe,CAAC,EAAE,GAAG,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;AACnF,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,SAAsC;IACrE,OAAO;QACL,IAAI,EAAE,SAAS,CAAC,IAAI;QACpB,eAAe,EAAE,SAAS,CAAC,eAAe;QAC1C,WAAW,EAAE,SAAS,CAAC,WAAW;QAClC,aAAa,EAAE,SAAS,CAAC,aAAa;KACvC,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,KAAK,UAAU,eAAe,CAAC,MAAmC;IAChE,MAAM,IAAI,GAAG,uBAAuB,EAAE,CAAC;IACvC,MAAM,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,gBAAgB,EAAE,CAAC,CAAC;IAChF,MAAM,EAAE,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,iBAAiB,EAAE,CAAC,CAAC;AACzF,CAAC"}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * WhatsApp Draft Send Gate
3
+ *
4
+ * Decides whether a reply draft may be sent. Owner decision (2026-09-24):
5
+ * nothing is ever sent to WhatsApp without the owner's explicit
6
+ * confirmation, one message at a time.
7
+ *
8
+ * - A request with no `X-Agent-Session` header is the owner (dashboard,
9
+ * mobile, portal) — the click is the confirmation.
10
+ * - An agent may send only when a genuine owner chat message (from
11
+ * `ChatV2Service.getRecentOwnerMessageContents`, which agents cannot
12
+ * write) created after the draft says 「发 W12」 for this draft's code, and
13
+ * the draft is at most `DRAFT_CONFIRM_WINDOW_MS` old. Because the owner
14
+ * message must post-date the draft, bounding the draft's age also bounds
15
+ * the confirmation to that window.
16
+ *
17
+ * @module services/whatsapp/whatsapp-draft-gate
18
+ */
19
+ import type { WhatsAppDraft } from '../../types/whatsapp.types.js';
20
+ /** Why an agent's send was refused. */
21
+ export type DraftSendRefusal = 'no_confirmation' | 'window_expired';
22
+ /** Outcome of {@link decideDraftSend}. */
23
+ export type DraftSendDecision = {
24
+ allowed: true;
25
+ via: 'owner' | 'owner_confirmation';
26
+ confirmation?: string;
27
+ } | {
28
+ allowed: false;
29
+ reason: DraftSendRefusal;
30
+ };
31
+ /** Inputs for {@link decideDraftSend}. */
32
+ export interface DraftSendContext {
33
+ /** The draft (must be pending — checked by the caller) */
34
+ draft: WhatsAppDraft;
35
+ /** `X-Agent-Session` of the caller; undefined = owner */
36
+ agentSession: string | undefined;
37
+ /** Owner message texts created at/after `sinceMs`, newest first */
38
+ ownerMessagesSince: (sinceMs: number) => string[];
39
+ /** Current epoch ms */
40
+ now: number;
41
+ }
42
+ /**
43
+ * Whether one owner message confirms sending a given draft.
44
+ *
45
+ * @param text - Owner message text
46
+ * @param draft - The draft
47
+ * @returns True for 「发 W12」 / 「发送 12」 / "send #W12" / 「确认发送 W12」 naming this draft
48
+ */
49
+ export declare function isConfirmationFor(text: string, draft: Pick<WhatsAppDraft, 'seq'>): boolean;
50
+ /**
51
+ * Decide whether the caller may send this draft now.
52
+ *
53
+ * @param ctx - Draft, caller, owner-message source, clock
54
+ * @returns Allowed (and why), or refused (and why)
55
+ */
56
+ export declare function decideDraftSend(ctx: DraftSendContext): DraftSendDecision;
57
+ /**
58
+ * The instruction an agent must follow after drafting: show the owner the
59
+ * draft and ask for 「发 <code>」.
60
+ *
61
+ * @param draft - The draft
62
+ * @param recipient - Human label for the recipient (chat name or JID)
63
+ * @returns Instruction text
64
+ */
65
+ export declare function buildOwnerConfirmPrompt(draft: Pick<WhatsAppDraft, 'code' | 'text'>, recipient: string): string;
66
+ /**
67
+ * Human message for a refused agent send.
68
+ *
69
+ * @param draft - The draft
70
+ * @param reason - Refusal reason
71
+ * @returns Message for the 403 body
72
+ */
73
+ export declare function buildRefusalMessage(draft: Pick<WhatsAppDraft, 'code'>, reason: DraftSendRefusal): string;
74
+ //# sourceMappingURL=whatsapp-draft-gate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"whatsapp-draft-gate.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/whatsapp/whatsapp-draft-gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAMnE,uCAAuC;AACvC,MAAM,MAAM,gBAAgB,GAAG,iBAAiB,GAAG,gBAAgB,CAAC;AAEpE,0CAA0C;AAC1C,MAAM,MAAM,iBAAiB,GACzB;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,GAAG,EAAE,OAAO,GAAG,oBAAoB,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7E;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,gBAAgB,CAAA;CAAE,CAAC;AAEjD,0CAA0C;AAC1C,MAAM,WAAW,gBAAgB;IAC/B,0DAA0D;IAC1D,KAAK,EAAE,aAAa,CAAC;IACrB,yDAAyD;IACzD,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,mEAAmE;IACnE,kBAAkB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,EAAE,CAAC;IAClD,uBAAuB;IACvB,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,CAAC,aAAa,EAAE,KAAK,CAAC,GAAG,OAAO,CAI1F;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,gBAAgB,GAAG,iBAAiB,CASxE;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,IAAI,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAAC,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAM9G;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,IAAI,CAAC,aAAa,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,gBAAgB,GAAG,MAAM,CAYxG"}
@@ -0,0 +1,82 @@
1
+ /**
2
+ * WhatsApp Draft Send Gate
3
+ *
4
+ * Decides whether a reply draft may be sent. Owner decision (2026-09-24):
5
+ * nothing is ever sent to WhatsApp without the owner's explicit
6
+ * confirmation, one message at a time.
7
+ *
8
+ * - A request with no `X-Agent-Session` header is the owner (dashboard,
9
+ * mobile, portal) — the click is the confirmation.
10
+ * - An agent may send only when a genuine owner chat message (from
11
+ * `ChatV2Service.getRecentOwnerMessageContents`, which agents cannot
12
+ * write) created after the draft says 「发 W12」 for this draft's code, and
13
+ * the draft is at most `DRAFT_CONFIRM_WINDOW_MS` old. Because the owner
14
+ * message must post-date the draft, bounding the draft's age also bounds
15
+ * the confirmation to that window.
16
+ *
17
+ * @module services/whatsapp/whatsapp-draft-gate
18
+ */
19
+ import { WHATSAPP_CONSTANTS } from '../../constants.js';
20
+ import { parseDraftCode } from './whatsapp-inbox.store.js';
21
+ /** Milliseconds per minute (for human-readable window text). */
22
+ const MS_PER_MINUTE = 60 * 1000;
23
+ /**
24
+ * Whether one owner message confirms sending a given draft.
25
+ *
26
+ * @param text - Owner message text
27
+ * @param draft - The draft
28
+ * @returns True for 「发 W12」 / 「发送 12」 / "send #W12" / 「确认发送 W12」 naming this draft
29
+ */
30
+ export function isConfirmationFor(text, draft) {
31
+ const m = WHATSAPP_CONSTANTS.DRAFT_CONFIRM_PATTERN.exec(text.trim());
32
+ if (!m)
33
+ return false;
34
+ return parseDraftCode(m[2]) === draft.seq;
35
+ }
36
+ /**
37
+ * Decide whether the caller may send this draft now.
38
+ *
39
+ * @param ctx - Draft, caller, owner-message source, clock
40
+ * @returns Allowed (and why), or refused (and why)
41
+ */
42
+ export function decideDraftSend(ctx) {
43
+ if (!ctx.agentSession)
44
+ return { allowed: true, via: 'owner' };
45
+ if (ctx.now - ctx.draft.createdAt > WHATSAPP_CONSTANTS.DRAFT_CONFIRM_WINDOW_MS) {
46
+ return { allowed: false, reason: 'window_expired' };
47
+ }
48
+ const confirmation = ctx.ownerMessagesSince(ctx.draft.createdAt).find((t) => isConfirmationFor(t, ctx.draft));
49
+ return confirmation !== undefined
50
+ ? { allowed: true, via: 'owner_confirmation', confirmation }
51
+ : { allowed: false, reason: 'no_confirmation' };
52
+ }
53
+ /**
54
+ * The instruction an agent must follow after drafting: show the owner the
55
+ * draft and ask for 「发 <code>」.
56
+ *
57
+ * @param draft - The draft
58
+ * @param recipient - Human label for the recipient (chat name or JID)
59
+ * @returns Instruction text
60
+ */
61
+ export function buildOwnerConfirmPrompt(draft, recipient) {
62
+ return (`Draft ${draft.code} is saved but NOT sent. Show the owner the recipient (${recipient}), ` +
63
+ `the exact draft text, and the code ${draft.code}, and ask them to reply 「发 ${draft.code}」 to send it ` +
64
+ `(or send/discard it from the dashboard). Do not send it until they do.`);
65
+ }
66
+ /**
67
+ * Human message for a refused agent send.
68
+ *
69
+ * @param draft - The draft
70
+ * @param reason - Refusal reason
71
+ * @returns Message for the 403 body
72
+ */
73
+ export function buildRefusalMessage(draft, reason) {
74
+ const windowMinutes = Math.round(WHATSAPP_CONSTANTS.DRAFT_CONFIRM_WINDOW_MS / MS_PER_MINUTE);
75
+ if (reason === 'window_expired') {
76
+ return (`Draft ${draft.code} is older than ${windowMinutes} minutes, so it can no longer be sent on the owner's ` +
77
+ `chat confirmation. Ask the owner to send it from the dashboard, or write a fresh draft and ask them to reply 「发 <new code>」.`);
78
+ }
79
+ return (`The owner has not confirmed draft ${draft.code}. Show the owner the draft (recipient and exact text) ` +
80
+ `and ask them to reply 「发 ${draft.code}」. Only then call send again. Never send without it.`);
81
+ }
82
+ //# sourceMappingURL=whatsapp-draft-gate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"whatsapp-draft-gate.js","sourceRoot":"","sources":["../../../../../../backend/src/services/whatsapp/whatsapp-draft-gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAExD,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAE3D,gEAAgE;AAChE,MAAM,aAAa,GAAG,EAAE,GAAG,IAAI,CAAC;AAsBhC;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAY,EAAE,KAAiC;IAC/E,MAAM,CAAC,GAAG,kBAAkB,CAAC,qBAAqB,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;IACrE,IAAI,CAAC,CAAC;QAAE,OAAO,KAAK,CAAC;IACrB,OAAO,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,GAAG,CAAC;AAC5C,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,GAAqB;IACnD,IAAI,CAAC,GAAG,CAAC,YAAY;QAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC;IAC9D,IAAI,GAAG,CAAC,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,SAAS,GAAG,kBAAkB,CAAC,uBAAuB,EAAE,CAAC;QAC/E,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC;IACtD,CAAC;IACD,MAAM,YAAY,GAAG,GAAG,CAAC,kBAAkB,CAAC,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,CAAC,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC;IAC9G,OAAO,YAAY,KAAK,SAAS;QAC/B,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,oBAAoB,EAAE,YAAY,EAAE;QAC5D,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC;AACpD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,uBAAuB,CAAC,KAA2C,EAAE,SAAiB;IACpG,OAAO,CACL,SAAS,KAAK,CAAC,IAAI,yDAAyD,SAAS,KAAK;QAC1F,sCAAsC,KAAK,CAAC,IAAI,8BAA8B,KAAK,CAAC,IAAI,eAAe;QACvG,wEAAwE,CACzE,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CAAC,KAAkC,EAAE,MAAwB;IAC9F,MAAM,aAAa,GAAG,IAAI,CAAC,KAAK,CAAC,kBAAkB,CAAC,uBAAuB,GAAG,aAAa,CAAC,CAAC;IAC7F,IAAI,MAAM,KAAK,gBAAgB,EAAE,CAAC;QAChC,OAAO,CACL,SAAS,KAAK,CAAC,IAAI,kBAAkB,aAAa,uDAAuD;YACzG,8HAA8H,CAC/H,CAAC;IACJ,CAAC;IACD,OAAO,CACL,qCAAqC,KAAK,CAAC,IAAI,wDAAwD;QACvG,4BAA4B,KAAK,CAAC,IAAI,sDAAsD,CAC7F,CAAC;AACJ,CAAC"}
@@ -0,0 +1,184 @@
1
+ /**
2
+ * WhatsApp Inbox Capture
3
+ *
4
+ * Turns Baileys events into inbox-store rows. Used only in `inbox` mode.
5
+ * Unlike the assistant path (`WhatsAppService.handleIncomingMessage`) nothing
6
+ * is dropped for being the owner's own message, a group message, media, or
7
+ * from a contact outside `allowedContacts` — the inbox has to know whether
8
+ * the owner already replied, and it is the owner's own account.
9
+ *
10
+ * Media are never downloaded: a message is recorded as its kind plus caption
11
+ * (or file name for documents).
12
+ *
13
+ * Baileys shapes relied on (v7, `@whiskeysockets/baileys` typings):
14
+ * - `messages.upsert`: `{ messages: WAMessage[], type: 'notify' | 'append' }`
15
+ * - `messaging-history.set`: `{ chats: Chat[], contacts: Contact[], messages: WAMessage[], syncType? }`
16
+ * - `contacts.upsert` / `contacts.update`: `Contact[]` (`id`, `name` = address book, `notify` = their pushName)
17
+ * - `chats.upsert` / `chats.update`: `Chat[]` (`id`, `name`, `conversationTimestamp`)
18
+ * - `groups.upsert` / `groups.update`: `GroupMetadata[]` (`id`, `subject`)
19
+ * - `WAMessage.key.remoteJidAlt` / `participantAlt` (v7 LID↔PN alternates)
20
+ * - `messageTimestamp`: seconds, as number, numeric string or protobuf Long
21
+ *
22
+ * @module services/whatsapp/whatsapp-inbox-capture
23
+ */
24
+ import type { WhatsAppInboxMessage, WhatsAppMessageKind } from '../../types/whatsapp.types.js';
25
+ import type { UpsertChatInput, WhatsAppInboxStore } from './whatsapp-inbox.store.js';
26
+ /** Loose record type for untyped Baileys payloads. */
27
+ type Rec = Record<string, unknown>;
28
+ /**
29
+ * Convert a Baileys timestamp (seconds as number, numeric string, or
30
+ * protobuf Long with `toNumber()` / `low`+`high`) to epoch ms.
31
+ *
32
+ * @param ts - Raw timestamp
33
+ * @returns Epoch ms, or null when unparseable / non-positive
34
+ */
35
+ export declare function toEpochMs(ts: unknown): number | null;
36
+ /**
37
+ * Whether a JID is a group chat.
38
+ *
39
+ * @param jid - JID
40
+ * @returns True for `...@g.us`
41
+ */
42
+ export declare function isGroupJid(jid: string): boolean;
43
+ /**
44
+ * Pick the canonical JID for a person: prefer the phone-number form when
45
+ * Baileys supplies it as the alternate of a LID, so the same person's
46
+ * messages (some keyed by LID, some by phone number) land in one chat.
47
+ *
48
+ * @param jid - Primary JID (`remoteJid` / `participant`)
49
+ * @param alt - Alternate JID (`remoteJidAlt` / `participantAlt`)
50
+ * @returns The canonical JID
51
+ */
52
+ export declare function canonicalJid(jid: string, alt?: string): string;
53
+ /**
54
+ * Strip wrapper messages (disappearing, view-once, edited, doc-with-caption).
55
+ *
56
+ * @param content - `WAMessage.message`
57
+ * @returns The innermost content object, or undefined
58
+ */
59
+ export declare function unwrapMessageContent(content: unknown): Rec | undefined;
60
+ /**
61
+ * Classify message content into a stored kind and its text/caption.
62
+ *
63
+ * @param content - `WAMessage.message` (wrappers allowed)
64
+ * @returns `{ kind, text }`, or null when this is not a user-visible message
65
+ * (protocol message, reaction, empty envelope)
66
+ */
67
+ export declare function classifyMessageContent(content: unknown): {
68
+ kind: WhatsAppMessageKind;
69
+ text: string;
70
+ } | null;
71
+ /** Result of parsing one Baileys message. */
72
+ export interface ParsedWhatsAppMessage {
73
+ /** Row for the store */
74
+ message: WhatsAppInboxMessage;
75
+ /** Whether the chat is a group */
76
+ isGroup: boolean;
77
+ /** Sender's self-chosen name, when the message carried one (not for own messages) */
78
+ pushName: string | null;
79
+ }
80
+ /**
81
+ * Parse a Baileys `WAMessage` into an inbox row.
82
+ *
83
+ * @param raw - The WAMessage
84
+ * @param ownJid - The connected account's JID (sender of `fromMe` messages), if known
85
+ * @returns The parsed message, or null when it is not storable (no id/chat,
86
+ * status broadcast, protocol-only content, no timestamp)
87
+ */
88
+ export declare function parseBaileysMessage(raw: unknown, ownJid?: string | null): ParsedWhatsAppMessage | null;
89
+ /**
90
+ * Chat row from a Baileys `Contact`: the address-book name outranks the
91
+ * contact's own pushName (`notify`) and business `verifiedName`.
92
+ *
93
+ * @param contact - Contact (full or partial update)
94
+ * @returns Upsert input, or null when the contact names nothing useful
95
+ */
96
+ export declare function chatFromContact(contact: unknown): UpsertChatInput | null;
97
+ /**
98
+ * Chat row from a Baileys `Chat` (history sync, `chats.upsert/update`).
99
+ *
100
+ * @param chat - Chat
101
+ * @returns Upsert input, or null without an id / for the status pseudo-chat
102
+ */
103
+ export declare function chatFromChat(chat: unknown): UpsertChatInput | null;
104
+ /**
105
+ * Chat row from Baileys `GroupMetadata` (subject = group name).
106
+ *
107
+ * @param group - Group metadata (full or partial)
108
+ * @returns Upsert input, or null without an id
109
+ */
110
+ export declare function chatFromGroup(group: unknown): UpsertChatInput | null;
111
+ /** Optional hooks for {@link WhatsAppInboxCapture}. */
112
+ export interface WhatsAppInboxCaptureOptions {
113
+ /** The connected account's JID (sender of own messages) */
114
+ getOwnJid?: () => string | null;
115
+ /** Fetch a group's subject for a group we have no name for (best effort) */
116
+ fetchGroupSubject?: (jid: string) => Promise<string | null>;
117
+ /** Clock (tests) */
118
+ now?: () => number;
119
+ }
120
+ /**
121
+ * Writes Baileys events into a {@link WhatsAppInboxStore}.
122
+ *
123
+ * Every handler is defensive: a malformed payload is skipped, never thrown,
124
+ * because it runs inside the socket's event loop.
125
+ */
126
+ export declare class WhatsAppInboxCapture {
127
+ private readonly store;
128
+ private readonly opts;
129
+ private readonly groupLookups;
130
+ /**
131
+ * @param store - Destination store
132
+ * @param opts - Own-JID accessor, group-subject fetcher, clock
133
+ */
134
+ constructor(store: WhatsAppInboxStore, opts?: WhatsAppInboxCaptureOptions);
135
+ /**
136
+ * Store one message and name its chat.
137
+ *
138
+ * @param raw - A WAMessage
139
+ * @returns True when stored
140
+ */
141
+ captureMessage(raw: unknown): boolean;
142
+ /**
143
+ * Handle `messages.upsert` (both `notify` and `append`: own messages sent
144
+ * from the phone and backfills arrive as `append`).
145
+ *
146
+ * @param upsert - `{ messages, type }`
147
+ * @returns Number of messages stored
148
+ */
149
+ onMessagesUpsert(upsert: unknown): number;
150
+ /**
151
+ * Handle `messaging-history.set`: seed chats, contact names and recent
152
+ * messages. Messages older than `HISTORY_SYNC_MAX_AGE_MS` are skipped.
153
+ *
154
+ * @param payload - `{ chats, contacts, messages }`
155
+ * @returns Number of messages stored
156
+ */
157
+ onHistorySet(payload: unknown): number;
158
+ /**
159
+ * Handle `contacts.upsert` / `contacts.update`.
160
+ *
161
+ * @param contacts - Contact list
162
+ */
163
+ onContacts(contacts: unknown): void;
164
+ /**
165
+ * Handle `chats.upsert` / `chats.update`.
166
+ *
167
+ * @param chats - Chat list
168
+ */
169
+ onChats(chats: unknown): void;
170
+ /**
171
+ * Handle `groups.upsert` / `groups.update`.
172
+ *
173
+ * @param groups - Group metadata list
174
+ */
175
+ onGroups(groups: unknown): void;
176
+ /**
177
+ * Look a group's subject up once per process when we have no name for it.
178
+ *
179
+ * @param jid - Group JID
180
+ */
181
+ private maybeFetchGroupName;
182
+ }
183
+ export {};
184
+ //# sourceMappingURL=whatsapp-inbox-capture.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"whatsapp-inbox-capture.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/whatsapp/whatsapp-inbox-capture.ts"],"names":[],"mappings":"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"}