@open-mercato/channel-imap 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/.turbo/turbo-build.log +2 -0
  2. package/AGENTS.md +56 -0
  3. package/build.mjs +7 -0
  4. package/dist/index.js +5 -0
  5. package/dist/index.js.map +7 -0
  6. package/dist/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-001.spec.js +62 -0
  7. package/dist/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-001.spec.js.map +7 -0
  8. package/dist/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-002.spec.js +19 -0
  9. package/dist/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-002.spec.js.map +7 -0
  10. package/dist/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-003.spec.js +16 -0
  11. package/dist/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-003.spec.js.map +7 -0
  12. package/dist/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-021.spec.js +26 -0
  13. package/dist/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-021.spec.js.map +7 -0
  14. package/dist/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-022.spec.js +27 -0
  15. package/dist/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-022.spec.js.map +7 -0
  16. package/dist/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-023.spec.js +15 -0
  17. package/dist/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-023.spec.js.map +7 -0
  18. package/dist/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-024.spec.js +15 -0
  19. package/dist/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-024.spec.js.map +7 -0
  20. package/dist/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-025.spec.js +6 -0
  21. package/dist/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-025.spec.js.map +7 -0
  22. package/dist/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-026.spec.js +6 -0
  23. package/dist/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-026.spec.js.map +7 -0
  24. package/dist/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-027.spec.js +6 -0
  25. package/dist/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-027.spec.js.map +7 -0
  26. package/dist/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-028.spec.js +6 -0
  27. package/dist/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-028.spec.js.map +7 -0
  28. package/dist/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-029.spec.js +48 -0
  29. package/dist/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-029.spec.js.map +7 -0
  30. package/dist/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-030.spec.js +6 -0
  31. package/dist/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-030.spec.js.map +7 -0
  32. package/dist/modules/channel_imap/acl.js +10 -0
  33. package/dist/modules/channel_imap/acl.js.map +7 -0
  34. package/dist/modules/channel_imap/di.js +23 -0
  35. package/dist/modules/channel_imap/di.js.map +7 -0
  36. package/dist/modules/channel_imap/index.js +9 -0
  37. package/dist/modules/channel_imap/index.js.map +7 -0
  38. package/dist/modules/channel_imap/integration.js +135 -0
  39. package/dist/modules/channel_imap/integration.js.map +7 -0
  40. package/dist/modules/channel_imap/lib/adapter.js +291 -0
  41. package/dist/modules/channel_imap/lib/adapter.js.map +7 -0
  42. package/dist/modules/channel_imap/lib/capabilities.js +8 -0
  43. package/dist/modules/channel_imap/lib/capabilities.js.map +7 -0
  44. package/dist/modules/channel_imap/lib/convert-outbound.js +54 -0
  45. package/dist/modules/channel_imap/lib/convert-outbound.js.map +7 -0
  46. package/dist/modules/channel_imap/lib/credentials.js +104 -0
  47. package/dist/modules/channel_imap/lib/credentials.js.map +7 -0
  48. package/dist/modules/channel_imap/lib/health.js +39 -0
  49. package/dist/modules/channel_imap/lib/health.js.map +7 -0
  50. package/dist/modules/channel_imap/lib/host-pinning.js +34 -0
  51. package/dist/modules/channel_imap/lib/host-pinning.js.map +7 -0
  52. package/dist/modules/channel_imap/lib/imap-client.js +210 -0
  53. package/dist/modules/channel_imap/lib/imap-client.js.map +7 -0
  54. package/dist/modules/channel_imap/lib/normalize-inbound.js +19 -0
  55. package/dist/modules/channel_imap/lib/normalize-inbound.js.map +7 -0
  56. package/dist/modules/channel_imap/lib/smtp-client.js +113 -0
  57. package/dist/modules/channel_imap/lib/smtp-client.js.map +7 -0
  58. package/dist/modules/channel_imap/lib/transport.js +17 -0
  59. package/dist/modules/channel_imap/lib/transport.js.map +7 -0
  60. package/dist/modules/channel_imap/lib/validate-credentials.js +69 -0
  61. package/dist/modules/channel_imap/lib/validate-credentials.js.map +7 -0
  62. package/dist/modules/channel_imap/setup.js +25 -0
  63. package/dist/modules/channel_imap/setup.js.map +7 -0
  64. package/dist/modules/channel_imap/widgets/injection/connect/widget.client.js +337 -0
  65. package/dist/modules/channel_imap/widgets/injection/connect/widget.client.js.map +7 -0
  66. package/dist/modules/channel_imap/widgets/injection/connect/widget.js +17 -0
  67. package/dist/modules/channel_imap/widgets/injection/connect/widget.js.map +7 -0
  68. package/dist/modules/channel_imap/widgets/injection-table.js +14 -0
  69. package/dist/modules/channel_imap/widgets/injection-table.js.map +7 -0
  70. package/jest.config.cjs +34 -0
  71. package/package.json +99 -0
  72. package/src/index.ts +1 -0
  73. package/src/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-001.spec.ts +80 -0
  74. package/src/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-002.spec.ts +28 -0
  75. package/src/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-003.spec.ts +23 -0
  76. package/src/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-021.spec.ts +40 -0
  77. package/src/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-022.spec.ts +38 -0
  78. package/src/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-023.spec.ts +31 -0
  79. package/src/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-024.spec.ts +27 -0
  80. package/src/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-025.spec.ts +23 -0
  81. package/src/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-026.spec.ts +18 -0
  82. package/src/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-027.spec.ts +18 -0
  83. package/src/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-028.spec.ts +19 -0
  84. package/src/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-029.spec.ts +72 -0
  85. package/src/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-030.spec.ts +19 -0
  86. package/src/modules/channel_imap/acl.ts +6 -0
  87. package/src/modules/channel_imap/di.ts +26 -0
  88. package/src/modules/channel_imap/index.ts +6 -0
  89. package/src/modules/channel_imap/integration.ts +131 -0
  90. package/src/modules/channel_imap/lib/__tests__/adapter.test.ts +499 -0
  91. package/src/modules/channel_imap/lib/__tests__/convert-outbound.test.ts +73 -0
  92. package/src/modules/channel_imap/lib/__tests__/credentials.test.ts +154 -0
  93. package/src/modules/channel_imap/lib/__tests__/host-pinning.test.ts +68 -0
  94. package/src/modules/channel_imap/lib/__tests__/imap-client.test.ts +180 -0
  95. package/src/modules/channel_imap/lib/__tests__/normalize-inbound.test.ts +126 -0
  96. package/src/modules/channel_imap/lib/__tests__/transport.test.ts +68 -0
  97. package/src/modules/channel_imap/lib/__tests__/validate-credentials.test.ts +156 -0
  98. package/src/modules/channel_imap/lib/adapter.ts +451 -0
  99. package/src/modules/channel_imap/lib/capabilities.ts +16 -0
  100. package/src/modules/channel_imap/lib/convert-outbound.ts +79 -0
  101. package/src/modules/channel_imap/lib/credentials.ts +172 -0
  102. package/src/modules/channel_imap/lib/health.ts +70 -0
  103. package/src/modules/channel_imap/lib/host-pinning.ts +59 -0
  104. package/src/modules/channel_imap/lib/imap-client.ts +382 -0
  105. package/src/modules/channel_imap/lib/normalize-inbound.ts +47 -0
  106. package/src/modules/channel_imap/lib/smtp-client.ts +214 -0
  107. package/src/modules/channel_imap/lib/transport.ts +37 -0
  108. package/src/modules/channel_imap/lib/validate-credentials.ts +98 -0
  109. package/src/modules/channel_imap/setup.ts +34 -0
  110. package/src/modules/channel_imap/widgets/injection/connect/widget.client.tsx +359 -0
  111. package/src/modules/channel_imap/widgets/injection/connect/widget.ts +16 -0
  112. package/src/modules/channel_imap/widgets/injection-table.ts +12 -0
  113. package/tsconfig.json +9 -0
  114. package/watch.mjs +7 -0
@@ -0,0 +1,2 @@
1
+ [build:channel-imap] found 33 entry points
2
+ [build:channel-imap] built successfully
package/AGENTS.md ADDED
@@ -0,0 +1,56 @@
1
+ # `@open-mercato/channel-imap` — Agent Guidelines
2
+
3
+ IMAP + SMTP email channel provider for the Communications Hub (`communication_channels`). Connects any IMAP-capable mailbox (Fastmail, Proton Bridge, generic IMAP host) for inbound polling and outbound SMTP delivery.
4
+
5
+ - **Package**: `@open-mercato/channel-imap` ⇒ **module id**: `channel_imap`
6
+ - **Provider key**: `imap` (registered in the hub's channel adapter registry)
7
+ - This is an integration provider package — keep all IMAP/SMTP-specific logic here. Do NOT add it to `packages/core`.
8
+
9
+ ## Key Files (`src/modules/channel_imap/`)
10
+
11
+ | File | Purpose |
12
+ |------|---------|
13
+ | `integration.ts` | `IntegrationDefinition` (IMAP/SMTP credential fields, `healthCheck.service`, detail widget spot) |
14
+ | `di.ts` | `register(container)` — registers the adapter AND `channelImapHealthCheck` under the exact `healthCheck.service` name |
15
+ | `setup.ts` | Registers the adapter at import time; declares `defaultRoleFeatures` |
16
+ | `acl.ts` | `channel_imap.view`, `channel_imap.configure` |
17
+ | `lib/adapter.ts` | `ImapChannelAdapter` — implements the `ChannelAdapter` contract |
18
+ | `lib/credentials.ts` | Zod schemas: IMAP/SMTP credentials (+ SSRF host guard) + channel sync state |
19
+ | `lib/validate-credentials.ts` | Live LOGIN validation; rejects cleartext transport by default (M5) |
20
+ | `lib/imap-client.ts` | `ImapClient` (`imapflow`-backed); `credentialsToConnection(credentials)` |
21
+ | `lib/smtp-client.ts` | `SmtpClient` (`nodemailer`-backed); `credentialsToSmtpConnection(credentials)` |
22
+ | `lib/health.ts` | `channelImapHealthCheck` liveness probe (real IMAP LOGIN) |
23
+ | `lib/convert-outbound.ts` / `lib/normalize-inbound.ts` | RFC2822 outbound build / inbound MIME normalization |
24
+ | `lib/capabilities.ts` | `ChannelCapabilities` (`realtimePush: false`) |
25
+
26
+ ## Adapter Contract
27
+
28
+ `lib/adapter.ts` implements `ChannelAdapter` from `@open-mercato/core/modules/communication_channels/lib/adapter`. Key methods: `sendMessage`, `normalizeInbound`, `convertOutbound`, `validateCredentials`, `fetchHistory`, `importHistory`, `resolveContact`.
29
+
30
+ - Credentials are a plain IMAP/SMTP blob on `IntegrationCredentials` (per-user scoped). No OAuth.
31
+ - `fetchHistory` is cursor-driven via UIDVALIDITY + UIDNEXT on `channelState`. Bootstrap persists the cursor and fetches zero messages; backlog import is explicit via `importHistory`. Each poll is bounded by `HARD_CAP` (`hasMore: true` re-enqueues).
32
+ - The clients are swappable via `setImapClient` / `setSmtpClient` (test-only hooks).
33
+
34
+ ## Security Rules (MUST)
35
+
36
+ - **Reject cleartext transport.** `imapTls`/`smtpTls: 'none'` is rejected by `validateImapCredentials` unless `OM_CHANNEL_IMAP_ALLOW_INSECURE_TRANSPORT` is truthy. STARTTLS/implicit TLS always allowed.
37
+ - **SSRF guard.** Host strings are attacker-controlled; `credentials.ts` rejects private/loopback hosts unless `OM_CHANNEL_IMAP_ALLOW_INTERNAL_HOSTS` is truthy.
38
+ - Never log credential values.
39
+
40
+ ## Health Check
41
+
42
+ `lib/health.ts` exports `channelImapHealthCheck`, registered in `di.ts` under the name declared in `integration.ts` (`healthCheck.service`). IMAP credentials carry everything needed for a real probe, so it does a cheap IMAP LOGIN (`connectAndValidate`) and skips the SMTP round-trip to stay within the hub's 10s budget. Invalid creds or a failed LOGIN ⇒ `unhealthy`.
43
+
44
+ ## Env Vars
45
+
46
+ | Var | Default | Purpose |
47
+ |-----|---------|---------|
48
+ | `OM_CHANNEL_IMAP_ALLOW_INSECURE_TRANSPORT` | `false` | Permit `'none'` (cleartext) TLS mode in credential validation |
49
+ | `OM_CHANNEL_IMAP_ALLOW_INTERNAL_HOSTS` | `false` | Permit private/loopback IMAP/SMTP hosts (bypasses SSRF guard) |
50
+ | `OM_CHANNEL_IMAP_HARD_CAP_PER_POLL` | `200` | Max UIDs fetched per `fetchHistory` poll |
51
+
52
+ ## After Changes
53
+
54
+ - Run `yarn generate` after adding/modifying module files (DI, setup, acl, integration).
55
+ - Run unit tests: `yarn test` (jest specs live under `lib/__tests__/`).
56
+ - If you change the `healthCheck.service` name, update both `integration.ts` AND `di.ts` so the hub can resolve it.
package/build.mjs ADDED
@@ -0,0 +1,7 @@
1
+ import { dirname } from 'node:path'
2
+ import { fileURLToPath } from 'node:url'
3
+ import { buildPackage } from '../../scripts/build-package.mjs'
4
+
5
+ const packageDir = dirname(fileURLToPath(import.meta.url))
6
+
7
+ await buildPackage(packageDir, { name: 'channel-imap' })
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ import { metadata } from "./modules/channel_imap/index.js";
2
+ export {
3
+ metadata
4
+ };
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/index.ts"],
4
+ "sourcesContent": ["export { metadata } from './modules/channel_imap/index'\n"],
5
+ "mappings": "AAAA,SAAS,gBAAgB;",
6
+ "names": []
7
+ }
@@ -0,0 +1,62 @@
1
+ import { expect, test } from "@playwright/test";
2
+ import { getAuthToken } from "@open-mercato/core/helpers/integration/authFixtures";
3
+ import { apiRequest } from "@open-mercato/core/helpers/integration/api";
4
+ import { readJsonSafe } from "@open-mercato/core/helpers/integration/generalFixtures";
5
+ test.describe("TC-CHANNEL-EMAIL-001: IMAP provider registration", () => {
6
+ test("POST connect/credentials with providerKey=imap reaches the adapter", async ({ request }) => {
7
+ const token = await getAuthToken(request);
8
+ const response = await apiRequest(
9
+ request,
10
+ "POST",
11
+ "/api/communication_channels/channels/connect/credentials",
12
+ {
13
+ token,
14
+ data: {
15
+ providerKey: "imap",
16
+ displayName: "IMAP \u2014 integration test",
17
+ credentials: {
18
+ imapHost: "invalid.test.example",
19
+ imapPort: 993,
20
+ imapTls: "tls",
21
+ imapUser: "fake@example.test",
22
+ imapPassword: "wrong-password",
23
+ smtpHost: "invalid.test.example",
24
+ smtpPort: 465,
25
+ smtpTls: "tls",
26
+ smtpUser: "fake@example.test",
27
+ smtpPassword: "wrong-password",
28
+ fromAddress: "fake@example.test"
29
+ }
30
+ }
31
+ }
32
+ );
33
+ expect(response.status(), "route should not 5xx").toBeLessThan(500);
34
+ expect(response.status(), "IMAP provider should be registered").not.toBe(404);
35
+ });
36
+ test("POST connect/credentials with providerKey=imap and malformed credentials returns 422", async ({ request }) => {
37
+ const token = await getAuthToken(request);
38
+ const response = await apiRequest(
39
+ request,
40
+ "POST",
41
+ "/api/communication_channels/channels/connect/credentials",
42
+ {
43
+ token,
44
+ data: {
45
+ providerKey: "imap",
46
+ displayName: "IMAP \u2014 malformed",
47
+ credentials: {
48
+ // Missing required imap/smtp fields entirely.
49
+ fromAddress: "not-an-email"
50
+ }
51
+ }
52
+ }
53
+ );
54
+ expect(response.status()).toBeLessThan(500);
55
+ expect([401, 422, 400]).toContain(response.status());
56
+ if (response.status() === 422) {
57
+ const body = await readJsonSafe(response);
58
+ expect(body?.fieldErrors ?? body?.error).toBeTruthy();
59
+ }
60
+ });
61
+ });
62
+ //# sourceMappingURL=TC-CHANNEL-EMAIL-001.spec.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-001.spec.ts"],
4
+ "sourcesContent": ["import { expect, test } from '@playwright/test'\nimport { getAuthToken } from '@open-mercato/core/helpers/integration/authFixtures'\nimport { apiRequest } from '@open-mercato/core/helpers/integration/api'\nimport { readJsonSafe } from '@open-mercato/core/helpers/integration/generalFixtures'\n\n/**\n * TC-CHANNEL-EMAIL-001 \u2014 IMAP provider visible to the hub's per-user channel API.\n *\n * After Slice 3e registers the `imap` adapter inside `@open-mercato/channel-imap`,\n * the per-user channel routes must accept `providerKey: 'imap'` as a known provider.\n * Without a live IMAP test server we exercise the routing-and-validation surface only;\n * actual credential validation against a real IMAP host is covered by the unit tests\n * in `lib/__tests__/validate-credentials.test.ts`.\n *\n * The route should NOT 404 the provider (proves registration), and should NOT 500\n * (proves the request reaches the adapter and rejects cleanly when no live server\n * is reachable).\n */\ntest.describe('TC-CHANNEL-EMAIL-001: IMAP provider registration', () => {\n test('POST connect/credentials with providerKey=imap reaches the adapter', async ({ request }) => {\n const token = await getAuthToken(request)\n const response = await apiRequest(\n request,\n 'POST',\n '/api/communication_channels/channels/connect/credentials',\n {\n token,\n data: {\n providerKey: 'imap',\n displayName: 'IMAP \u2014 integration test',\n credentials: {\n imapHost: 'invalid.test.example',\n imapPort: 993,\n imapTls: 'tls',\n imapUser: 'fake@example.test',\n imapPassword: 'wrong-password',\n smtpHost: 'invalid.test.example',\n smtpPort: 465,\n smtpTls: 'tls',\n smtpUser: 'fake@example.test',\n smtpPassword: 'wrong-password',\n fromAddress: 'fake@example.test',\n },\n },\n },\n )\n expect(response.status(), 'route should not 5xx').toBeLessThan(500)\n // 401 (no auth seeded) / 422 (validation failure surfaced via createCrudFormError) /\n // 502 ish \u2014 but never 404, which would indicate the provider isn't registered.\n expect(response.status(), 'IMAP provider should be registered').not.toBe(404)\n })\n\n test('POST connect/credentials with providerKey=imap and malformed credentials returns 422', async ({ request }) => {\n const token = await getAuthToken(request)\n const response = await apiRequest(\n request,\n 'POST',\n '/api/communication_channels/channels/connect/credentials',\n {\n token,\n data: {\n providerKey: 'imap',\n displayName: 'IMAP \u2014 malformed',\n credentials: {\n // Missing required imap/smtp fields entirely.\n fromAddress: 'not-an-email',\n },\n },\n },\n )\n expect(response.status()).toBeLessThan(500)\n expect([401, 422, 400]).toContain(response.status())\n if (response.status() === 422) {\n // The route surfaces credential-validation failures as { error, fieldErrors }\n // (see api/post/channels/connect/credentials/route.ts).\n const body = await readJsonSafe<{ error?: string; fieldErrors?: Record<string, string> }>(response)\n expect(body?.fieldErrors ?? body?.error).toBeTruthy()\n }\n })\n})\n"],
5
+ "mappings": "AAAA,SAAS,QAAQ,YAAY;AAC7B,SAAS,oBAAoB;AAC7B,SAAS,kBAAkB;AAC3B,SAAS,oBAAoB;AAe7B,KAAK,SAAS,oDAAoD,MAAM;AACtE,OAAK,sEAAsE,OAAO,EAAE,QAAQ,MAAM;AAChG,UAAM,QAAQ,MAAM,aAAa,OAAO;AACxC,UAAM,WAAW,MAAM;AAAA,MACrB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,QACE;AAAA,QACA,MAAM;AAAA,UACJ,aAAa;AAAA,UACb,aAAa;AAAA,UACb,aAAa;AAAA,YACX,UAAU;AAAA,YACV,UAAU;AAAA,YACV,SAAS;AAAA,YACT,UAAU;AAAA,YACV,cAAc;AAAA,YACd,UAAU;AAAA,YACV,UAAU;AAAA,YACV,SAAS;AAAA,YACT,UAAU;AAAA,YACV,cAAc;AAAA,YACd,aAAa;AAAA,UACf;AAAA,QACF;AAAA,MACF;AAAA,IACF;AACA,WAAO,SAAS,OAAO,GAAG,sBAAsB,EAAE,aAAa,GAAG;AAGlE,WAAO,SAAS,OAAO,GAAG,oCAAoC,EAAE,IAAI,KAAK,GAAG;AAAA,EAC9E,CAAC;AAED,OAAK,wFAAwF,OAAO,EAAE,QAAQ,MAAM;AAClH,UAAM,QAAQ,MAAM,aAAa,OAAO;AACxC,UAAM,WAAW,MAAM;AAAA,MACrB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,QACE;AAAA,QACA,MAAM;AAAA,UACJ,aAAa;AAAA,UACb,aAAa;AAAA,UACb,aAAa;AAAA;AAAA,YAEX,aAAa;AAAA,UACf;AAAA,QACF;AAAA,MACF;AAAA,IACF;AACA,WAAO,SAAS,OAAO,CAAC,EAAE,aAAa,GAAG;AAC1C,WAAO,CAAC,KAAK,KAAK,GAAG,CAAC,EAAE,UAAU,SAAS,OAAO,CAAC;AACnD,QAAI,SAAS,OAAO,MAAM,KAAK;AAG7B,YAAM,OAAO,MAAM,aAAuE,QAAQ;AAClG,aAAO,MAAM,eAAe,MAAM,KAAK,EAAE,WAAW;AAAA,IACtD;AAAA,EACF,CAAC;AACH,CAAC;",
6
+ "names": []
7
+ }
@@ -0,0 +1,19 @@
1
+ import { expect, test } from "@playwright/test";
2
+ import { getAuthToken } from "@open-mercato/core/helpers/integration/authFixtures";
3
+ import { apiRequest } from "@open-mercato/core/helpers/integration/api";
4
+ test.describe("TC-CHANNEL-EMAIL-002: IMAP webhook is a no-op", () => {
5
+ test("POST /api/communication_channels/webhook/imap does not 5xx", async ({ request }) => {
6
+ const token = await getAuthToken(request);
7
+ const response = await apiRequest(
8
+ request,
9
+ "POST",
10
+ "/api/communication_channels/webhook/imap",
11
+ {
12
+ token,
13
+ data: { ping: true }
14
+ }
15
+ );
16
+ expect(response.status(), "webhook route should not 5xx").toBeLessThan(500);
17
+ });
18
+ });
19
+ //# sourceMappingURL=TC-CHANNEL-EMAIL-002.spec.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-002.spec.ts"],
4
+ "sourcesContent": ["import { expect, test } from '@playwright/test'\nimport { getAuthToken } from '@open-mercato/core/helpers/integration/authFixtures'\nimport { apiRequest } from '@open-mercato/core/helpers/integration/api'\n\n/**\n * TC-CHANNEL-EMAIL-002 \u2014 IMAP webhook endpoint stays disabled.\n *\n * IMAP has no webhook flow; the polling worker drives inbound. We still expose\n * the standard `/api/communication_channels/webhook/[provider]` URL because the\n * hub's route is generic. The IMAP adapter's `verifyWebhook` returns an\n * `eventType: 'other'` event so the route MUST respond 2xx (not handled) instead\n * of 5xx-ing or 404-ing.\n */\ntest.describe('TC-CHANNEL-EMAIL-002: IMAP webhook is a no-op', () => {\n test('POST /api/communication_channels/webhook/imap does not 5xx', async ({ request }) => {\n const token = await getAuthToken(request)\n const response = await apiRequest(\n request,\n 'POST',\n '/api/communication_channels/webhook/imap',\n {\n token,\n data: { ping: true },\n },\n )\n expect(response.status(), 'webhook route should not 5xx').toBeLessThan(500)\n })\n})\n"],
5
+ "mappings": "AAAA,SAAS,QAAQ,YAAY;AAC7B,SAAS,oBAAoB;AAC7B,SAAS,kBAAkB;AAW3B,KAAK,SAAS,iDAAiD,MAAM;AACnE,OAAK,8DAA8D,OAAO,EAAE,QAAQ,MAAM;AACxF,UAAM,QAAQ,MAAM,aAAa,OAAO;AACxC,UAAM,WAAW,MAAM;AAAA,MACrB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,QACE;AAAA,QACA,MAAM,EAAE,MAAM,KAAK;AAAA,MACrB;AAAA,IACF;AACA,WAAO,SAAS,OAAO,GAAG,8BAA8B,EAAE,aAAa,GAAG;AAAA,EAC5E,CAAC;AACH,CAAC;",
6
+ "names": []
7
+ }
@@ -0,0 +1,16 @@
1
+ import { expect, test } from "@playwright/test";
2
+ import { getAuthToken } from "@open-mercato/core/helpers/integration/authFixtures";
3
+ import { apiRequest } from "@open-mercato/core/helpers/integration/api";
4
+ test.describe("TC-CHANNEL-EMAIL-003: profile page with IMAP provider installed", () => {
5
+ test("GET /backend/profile/communication-channels does not 5xx", async ({ request }) => {
6
+ const token = await getAuthToken(request);
7
+ const response = await apiRequest(
8
+ request,
9
+ "GET",
10
+ "/backend/profile/communication-channels",
11
+ { token }
12
+ );
13
+ expect(response.status(), "profile page should not 5xx").toBeLessThan(500);
14
+ });
15
+ });
16
+ //# sourceMappingURL=TC-CHANNEL-EMAIL-003.spec.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-003.spec.ts"],
4
+ "sourcesContent": ["import { expect, test } from '@playwright/test'\nimport { getAuthToken } from '@open-mercato/core/helpers/integration/authFixtures'\nimport { apiRequest } from '@open-mercato/core/helpers/integration/api'\n\n/**\n * TC-CHANNEL-EMAIL-003 \u2014 Profile page renders with the IMAP provider installed.\n *\n * Confirms `@open-mercato/channel-imap` does not break the per-user profile page\n * (`/backend/profile/communication-channels`). Empty-state still renders even\n * when the IMAP provider is the only one registered.\n */\ntest.describe('TC-CHANNEL-EMAIL-003: profile page with IMAP provider installed', () => {\n test('GET /backend/profile/communication-channels does not 5xx', async ({ request }) => {\n const token = await getAuthToken(request)\n const response = await apiRequest(\n request,\n 'GET',\n '/backend/profile/communication-channels',\n { token },\n )\n expect(response.status(), 'profile page should not 5xx').toBeLessThan(500)\n })\n})\n"],
5
+ "mappings": "AAAA,SAAS,QAAQ,YAAY;AAC7B,SAAS,oBAAoB;AAC7B,SAAS,kBAAkB;AAS3B,KAAK,SAAS,mEAAmE,MAAM;AACrF,OAAK,4DAA4D,OAAO,EAAE,QAAQ,MAAM;AACtF,UAAM,QAAQ,MAAM,aAAa,OAAO;AACxC,UAAM,WAAW,MAAM;AAAA,MACrB;AAAA,MACA;AAAA,MACA;AAAA,MACA,EAAE,MAAM;AAAA,IACV;AACA,WAAO,SAAS,OAAO,GAAG,6BAA6B,EAAE,aAAa,GAAG;AAAA,EAC3E,CAAC;AACH,CAAC;",
6
+ "names": []
7
+ }
@@ -0,0 +1,26 @@
1
+ import { expect, test } from "@playwright/test";
2
+ import { getAuthToken } from "@open-mercato/core/helpers/integration/authFixtures";
3
+ import { apiRequest } from "@open-mercato/core/helpers/integration/api";
4
+ test.describe("TC-CHANNEL-EMAIL-021: IMAP bootstrap", () => {
5
+ test("poll-now endpoint requires authentication", async ({ request }) => {
6
+ const response = await apiRequest(
7
+ request,
8
+ "POST",
9
+ "/api/communication_channels/channels/00000000-0000-4000-8000-000000000021/poll-now",
10
+ // Intentionally empty token — this assertion is the 401 unauth path.
11
+ { token: "" }
12
+ );
13
+ expect(response.status()).toBe(401);
14
+ });
15
+ test("poll-now endpoint returns 400 for invalid UUID", async ({ request }) => {
16
+ const token = await getAuthToken(request);
17
+ const response = await apiRequest(
18
+ request,
19
+ "POST",
20
+ "/api/communication_channels/channels/not-a-uuid/poll-now",
21
+ { token }
22
+ );
23
+ expect(response.status()).toBe(400);
24
+ });
25
+ });
26
+ //# sourceMappingURL=TC-CHANNEL-EMAIL-021.spec.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-021.spec.ts"],
4
+ "sourcesContent": ["import { expect, test } from '@playwright/test'\nimport { getAuthToken } from '@open-mercato/core/helpers/integration/authFixtures'\nimport { apiRequest } from '@open-mercato/core/helpers/integration/api'\n\n/**\n * TC-CHANNEL-EMAIL-021 \u2014 IMAP zero-history bootstrap\n *\n * Verifies that the polling worker surface exists and that the\n * `/poll-now` operator endpoint is reachable. The full happy path \u2014\n * a freshly-connected channel persists `UIDVALIDITY` + `UIDNEXT` and\n * fetches ZERO historical messages \u2014 is captured in the QA scenario\n * markdown `TC-CHANNEL-EMAIL-021-imap-bootstrap.md`.\n *\n * Unit-level coverage of the bootstrap branch lives in\n * `packages/channel-imap/.../lib/__tests__/adapter.test.ts`\n * (`fetchHistory` describe block, \"bootstrap\" case).\n */\ntest.describe('TC-CHANNEL-EMAIL-021: IMAP bootstrap', () => {\n test('poll-now endpoint requires authentication', async ({ request }) => {\n const response = await apiRequest(\n request,\n 'POST',\n '/api/communication_channels/channels/00000000-0000-4000-8000-000000000021/poll-now',\n // Intentionally empty token \u2014 this assertion is the 401 unauth path.\n { token: '' },\n )\n expect(response.status()).toBe(401)\n })\n\n test('poll-now endpoint returns 400 for invalid UUID', async ({ request }) => {\n const token = await getAuthToken(request)\n const response = await apiRequest(\n request,\n 'POST',\n '/api/communication_channels/channels/not-a-uuid/poll-now',\n { token },\n )\n expect(response.status()).toBe(400)\n })\n})\n"],
5
+ "mappings": "AAAA,SAAS,QAAQ,YAAY;AAC7B,SAAS,oBAAoB;AAC7B,SAAS,kBAAkB;AAe3B,KAAK,SAAS,wCAAwC,MAAM;AAC1D,OAAK,6CAA6C,OAAO,EAAE,QAAQ,MAAM;AACvE,UAAM,WAAW,MAAM;AAAA,MACrB;AAAA,MACA;AAAA,MACA;AAAA;AAAA,MAEA,EAAE,OAAO,GAAG;AAAA,IACd;AACA,WAAO,SAAS,OAAO,CAAC,EAAE,KAAK,GAAG;AAAA,EACpC,CAAC;AAED,OAAK,kDAAkD,OAAO,EAAE,QAAQ,MAAM;AAC5E,UAAM,QAAQ,MAAM,aAAa,OAAO;AACxC,UAAM,WAAW,MAAM;AAAA,MACrB;AAAA,MACA;AAAA,MACA;AAAA,MACA,EAAE,MAAM;AAAA,IACV;AACA,WAAO,SAAS,OAAO,CAAC,EAAE,KAAK,GAAG;AAAA,EACpC,CAAC;AACH,CAAC;",
6
+ "names": []
7
+ }
@@ -0,0 +1,27 @@
1
+ import { expect, test } from "@playwright/test";
2
+ import { getAuthToken } from "@open-mercato/core/helpers/integration/authFixtures";
3
+ import { apiRequest } from "@open-mercato/core/helpers/integration/api";
4
+ test.describe("TC-CHANNEL-EMAIL-022: IMAP incremental polling", () => {
5
+ test("me/channels endpoint requires authentication", async ({ request }) => {
6
+ const response = await apiRequest(
7
+ request,
8
+ "GET",
9
+ "/api/communication_channels/me/channels",
10
+ { token: "" }
11
+ );
12
+ expect(response.status()).toBe(401);
13
+ });
14
+ test("me/channels returns items array when authenticated", async ({ request }) => {
15
+ const token = await getAuthToken(request);
16
+ const response = await apiRequest(
17
+ request,
18
+ "GET",
19
+ "/api/communication_channels/me/channels",
20
+ { token }
21
+ );
22
+ expect(response.status()).toBe(200);
23
+ const body = await response.json();
24
+ expect(Array.isArray(body.items)).toBe(true);
25
+ });
26
+ });
27
+ //# sourceMappingURL=TC-CHANNEL-EMAIL-022.spec.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-022.spec.ts"],
4
+ "sourcesContent": ["import { expect, test } from '@playwright/test'\nimport { getAuthToken } from '@open-mercato/core/helpers/integration/authFixtures'\nimport { apiRequest } from '@open-mercato/core/helpers/integration/api'\n\n/**\n * TC-CHANNEL-EMAIL-022 \u2014 IMAP incremental ingest within 90s\n *\n * Verifies the channels-list endpoint is reachable (which proves the\n * poll-tick scheduler + per-channel polling pipeline is wired). Full\n * E2E (new mail arrives \u2192 polling worker picks it up within 60s tick \u2192\n * ingest-inbound-message creates a CRM interaction within 90s) is in\n * the QA scenario markdown `TC-CHANNEL-EMAIL-022-imap-incremental.md`.\n */\ntest.describe('TC-CHANNEL-EMAIL-022: IMAP incremental polling', () => {\n test('me/channels endpoint requires authentication', async ({ request }) => {\n // Intentionally empty token \u2014 this assertion is the 401 unauth path.\n const response = await apiRequest(\n request,\n 'GET',\n '/api/communication_channels/me/channels',\n { token: '' },\n )\n expect(response.status()).toBe(401)\n })\n\n test('me/channels returns items array when authenticated', async ({ request }) => {\n const token = await getAuthToken(request)\n const response = await apiRequest(\n request,\n 'GET',\n '/api/communication_channels/me/channels',\n { token },\n )\n expect(response.status()).toBe(200)\n const body = (await response.json()) as { items?: unknown }\n expect(Array.isArray(body.items)).toBe(true)\n })\n})\n"],
5
+ "mappings": "AAAA,SAAS,QAAQ,YAAY;AAC7B,SAAS,oBAAoB;AAC7B,SAAS,kBAAkB;AAW3B,KAAK,SAAS,kDAAkD,MAAM;AACpE,OAAK,gDAAgD,OAAO,EAAE,QAAQ,MAAM;AAE1E,UAAM,WAAW,MAAM;AAAA,MACrB;AAAA,MACA;AAAA,MACA;AAAA,MACA,EAAE,OAAO,GAAG;AAAA,IACd;AACA,WAAO,SAAS,OAAO,CAAC,EAAE,KAAK,GAAG;AAAA,EACpC,CAAC;AAED,OAAK,sDAAsD,OAAO,EAAE,QAAQ,MAAM;AAChF,UAAM,QAAQ,MAAM,aAAa,OAAO;AACxC,UAAM,WAAW,MAAM;AAAA,MACrB;AAAA,MACA;AAAA,MACA;AAAA,MACA,EAAE,MAAM;AAAA,IACV;AACA,WAAO,SAAS,OAAO,CAAC,EAAE,KAAK,GAAG;AAClC,UAAM,OAAQ,MAAM,SAAS,KAAK;AAClC,WAAO,MAAM,QAAQ,KAAK,KAAK,CAAC,EAAE,KAAK,IAAI;AAAA,EAC7C,CAAC;AACH,CAAC;",
6
+ "names": []
7
+ }
@@ -0,0 +1,15 @@
1
+ import { expect, test } from "@playwright/test";
2
+ import { apiRequest } from "@open-mercato/core/helpers/integration/api";
3
+ test.describe("TC-CHANNEL-EMAIL-023: References-token threading", () => {
4
+ test("send-as-user endpoint requires authentication", async ({ request }) => {
5
+ const response = await apiRequest(
6
+ request,
7
+ "POST",
8
+ "/api/communication_channels/send-as-user",
9
+ // Intentionally empty token — this test asserts the 401 unauth path.
10
+ { token: "", data: { channelId: "00000000-0000-4000-8000-000000000023" } }
11
+ );
12
+ expect(response.status()).toBe(401);
13
+ });
14
+ });
15
+ //# sourceMappingURL=TC-CHANNEL-EMAIL-023.spec.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-023.spec.ts"],
4
+ "sourcesContent": ["import { expect, test } from '@playwright/test'\nimport { apiRequest } from '@open-mercato/core/helpers/integration/api'\n\n/**\n * TC-CHANNEL-EMAIL-023 \u2014 Threading via References token\n *\n * Smoke: the layered thread matcher prefers `token-references`\n * (highest-confidence strategy) when an inbound message carries our\n * synthetic `<om_TOKEN@open-mercato.invalid>` Message-ID in\n * `References`. The 5-strategy fallthrough order is unit-tested in\n * `packages/core/.../lib/__tests__/thread-matcher.test.ts`; the\n * end-to-end \"send \u2192 reply preserves References \u2192 CRM threads back\"\n * path is in the QA scenario markdown.\n *\n * Webhook-less providers have no public endpoint to assert against,\n * so this smoke test is intentionally minimal \u2014 it just confirms the\n * platform's send-as-user route is reachable (used by the outbound\n * test setup in the QA scenario).\n */\ntest.describe('TC-CHANNEL-EMAIL-023: References-token threading', () => {\n test('send-as-user endpoint requires authentication', async ({ request }) => {\n const response = await apiRequest(\n request,\n 'POST',\n '/api/communication_channels/send-as-user',\n // Intentionally empty token \u2014 this test asserts the 401 unauth path.\n { token: '', data: { channelId: '00000000-0000-4000-8000-000000000023' } },\n )\n expect(response.status()).toBe(401)\n })\n})\n"],
5
+ "mappings": "AAAA,SAAS,QAAQ,YAAY;AAC7B,SAAS,kBAAkB;AAkB3B,KAAK,SAAS,oDAAoD,MAAM;AACtE,OAAK,iDAAiD,OAAO,EAAE,QAAQ,MAAM;AAC3E,UAAM,WAAW,MAAM;AAAA,MACrB;AAAA,MACA;AAAA,MACA;AAAA;AAAA,MAEA,EAAE,OAAO,IAAI,MAAM,EAAE,WAAW,uCAAuC,EAAE;AAAA,IAC3E;AACA,WAAO,SAAS,OAAO,CAAC,EAAE,KAAK,GAAG;AAAA,EACpC,CAAC;AACH,CAAC;",
6
+ "names": []
7
+ }
@@ -0,0 +1,15 @@
1
+ import { expect, test } from "@playwright/test";
2
+ import { apiRequest } from "@open-mercato/core/helpers/integration/api";
3
+ test.describe("TC-CHANNEL-EMAIL-024: Body-footer threading", () => {
4
+ test("send-as-user endpoint exists at the expected path", async ({ request }) => {
5
+ const response = await apiRequest(
6
+ request,
7
+ "POST",
8
+ "/api/communication_channels/send-as-user",
9
+ // Intentionally empty token — the 401 / 400 branch is what we assert.
10
+ { token: "", data: {} }
11
+ );
12
+ expect([400, 401]).toContain(response.status());
13
+ });
14
+ });
15
+ //# sourceMappingURL=TC-CHANNEL-EMAIL-024.spec.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-024.spec.ts"],
4
+ "sourcesContent": ["import { expect, test } from '@playwright/test'\nimport { apiRequest } from '@open-mercato/core/helpers/integration/api'\n\n/**\n * TC-CHANNEL-EMAIL-024 \u2014 Threading via body footer\n *\n * When the recipient's MUA strips RFC5322 References on reply, our\n * hidden body-footer marker (`<span style=\"display:none\">[OM:TOKEN]</span>`\n * for HTML; `[OM:TOKEN]` bracketed marker for plain text) still\n * survives quoting and the layered matcher's `token-body` strategy\n * threads the reply correctly.\n *\n * Full E2E in the QA scenario markdown. This smoke test confirms the\n * send-as-user endpoint exists (same shape as TC-023).\n */\ntest.describe('TC-CHANNEL-EMAIL-024: Body-footer threading', () => {\n test('send-as-user endpoint exists at the expected path', async ({ request }) => {\n const response = await apiRequest(\n request,\n 'POST',\n '/api/communication_channels/send-as-user',\n // Intentionally empty token \u2014 the 401 / 400 branch is what we assert.\n { token: '', data: {} },\n )\n expect([400, 401]).toContain(response.status())\n })\n})\n"],
5
+ "mappings": "AAAA,SAAS,QAAQ,YAAY;AAC7B,SAAS,kBAAkB;AAc3B,KAAK,SAAS,+CAA+C,MAAM;AACjE,OAAK,qDAAqD,OAAO,EAAE,QAAQ,MAAM;AAC/E,UAAM,WAAW,MAAM;AAAA,MACrB;AAAA,MACA;AAAA,MACA;AAAA;AAAA,MAEA,EAAE,OAAO,IAAI,MAAM,CAAC,EAAE;AAAA,IACxB;AACA,WAAO,CAAC,KAAK,GAAG,CAAC,EAAE,UAAU,SAAS,OAAO,CAAC;AAAA,EAChD,CAAC;AACH,CAAC;",
6
+ "names": []
7
+ }
@@ -0,0 +1,6 @@
1
+ import { test } from "@playwright/test";
2
+ test.describe("TC-CHANNEL-EMAIL-025: JWZ-headers fallback", () => {
3
+ test.skip("behavioral coverage: thread-matcher.test.ts Strategy 3 (jwz-headers). Playwright E2E is infeasible \u2014 provider mock seams are process-local (see TC-CHANNEL-EMAIL-031 / TC-CRM-EMAIL-001).", () => {
4
+ });
5
+ });
6
+ //# sourceMappingURL=TC-CHANNEL-EMAIL-025.spec.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-025.spec.ts"],
4
+ "sourcesContent": ["import { test } from '@playwright/test'\n\n/**\n * TC-CHANNEL-EMAIL-025 \u2014 JWZ-headers fallback threading\n *\n * Verifies the `jwz-headers` strategy (Spec B \u00A7 thread-matcher,\n * confidence: medium) \u2014 when an inbound reply has no `om_*` token\n * but carries `In-Reply-To` / `References` pointing at one of our\n * outbound `Message-Id`s, the matcher walks the conventional JWZ\n * algorithm against the existing `external_messages` table.\n *\n * This path is exercised end-to-end by the QA scenario markdown\n * `TC-CHANNEL-EMAIL-025-jwz-fallback.md`. The unit-level coverage\n * lives in `packages/core/.../lib/__tests__/thread-matcher.test.ts`\n * (Strategy 3 \u2014 jwz-headers describe block).\n *\n * No additional API surface to assert at the integration layer\n * beyond the smoke tests already in TC-023/024 \u2014 this spec file\n * documents the existence of the JWZ pathway for QA-tracking purposes.\n */\ntest.describe('TC-CHANNEL-EMAIL-025: JWZ-headers fallback', () => {\n test.skip('behavioral coverage: thread-matcher.test.ts Strategy 3 (jwz-headers). Playwright E2E is infeasible \u2014 provider mock seams are process-local (see TC-CHANNEL-EMAIL-031 / TC-CRM-EMAIL-001).', () => {})\n})\n"],
5
+ "mappings": "AAAA,SAAS,YAAY;AAoBrB,KAAK,SAAS,8CAA8C,MAAM;AAChE,OAAK,KAAK,kMAA6L,MAAM;AAAA,EAAC,CAAC;AACjN,CAAC;",
6
+ "names": []
7
+ }
@@ -0,0 +1,6 @@
1
+ import { test } from "@playwright/test";
2
+ test.describe("TC-CHANNEL-EMAIL-026: Subject + participants fallback", () => {
3
+ test.skip("behavioral coverage: thread-matcher.test.ts Strategy 4 (subject + participants). Playwright E2E is infeasible \u2014 provider mock seams are process-local (see TC-CHANNEL-EMAIL-031 / TC-CRM-EMAIL-001).", () => {
4
+ });
5
+ });
6
+ //# sourceMappingURL=TC-CHANNEL-EMAIL-026.spec.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-026.spec.ts"],
4
+ "sourcesContent": ["import { test } from '@playwright/test'\n\n/**\n * TC-CHANNEL-EMAIL-026 \u2014 Subject + participants fallback threading\n *\n * Last-ditch matcher strategy: when neither `om_*` token nor JWZ\n * headers are present, the matcher normalises the inbound subject\n * (`Re:`, `Fwd:`, `[EXTERNAL]` stripped) and checks for an existing\n * thread whose participants overlap >= 50% with the inbound's\n * sender/to/cc. Confidence: low.\n *\n * Unit-tested in `packages/core/.../lib/__tests__/thread-matcher.test.ts`\n * (Strategy 4 \u2014 subject-participants). End-to-end is the QA scenario\n * markdown.\n */\ntest.describe('TC-CHANNEL-EMAIL-026: Subject + participants fallback', () => {\n test.skip('behavioral coverage: thread-matcher.test.ts Strategy 4 (subject + participants). Playwright E2E is infeasible \u2014 provider mock seams are process-local (see TC-CHANNEL-EMAIL-031 / TC-CRM-EMAIL-001).', () => {})\n})\n"],
5
+ "mappings": "AAAA,SAAS,YAAY;AAerB,KAAK,SAAS,yDAAyD,MAAM;AAC3E,OAAK,KAAK,6MAAwM,MAAM;AAAA,EAAC,CAAC;AAC5N,CAAC;",
6
+ "names": []
7
+ }
@@ -0,0 +1,6 @@
1
+ import { test } from "@playwright/test";
2
+ test.describe("TC-CHANNEL-EMAIL-027: Auto-recovery sweep", () => {
3
+ test.skip("behavioral coverage: workers/__tests__/poll-tick.test.ts (auto-recovery sweep). Playwright E2E is infeasible \u2014 provider mock seams are process-local + needs scheduler fast-forward.", () => {
4
+ });
5
+ });
6
+ //# sourceMappingURL=TC-CHANNEL-EMAIL-027.spec.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-027.spec.ts"],
4
+ "sourcesContent": ["import { test } from '@playwright/test'\n\n/**\n * TC-CHANNEL-EMAIL-027 \u2014 Auto-recovery from `status='error'`\n *\n * Spec B \u00A7 Phase B5 \u2014 `poll-tick.ts` enumerates two channel pools per\n * tick: (a) `status='connected'` and due for polling, (b) `status='error'`\n * whose `lastFailureAt` is older than `OM_CHANNEL_AUTO_RECOVER_MINUTES`\n * (default 30). A successful poll in pool (b) flips status back to\n * 'connected' via `poll-channel.ts`.\n *\n * Unit-tested in `packages/core/.../workers/__tests__/poll-tick.test.ts`\n * (auto-recovery describe block). Full E2E (force a transient error \u2192\n * fast-forward 30 min \u2192 next tick recovers) is in the QA scenario.\n */\ntest.describe('TC-CHANNEL-EMAIL-027: Auto-recovery sweep', () => {\n test.skip('behavioral coverage: workers/__tests__/poll-tick.test.ts (auto-recovery sweep). Playwright E2E is infeasible \u2014 provider mock seams are process-local + needs scheduler fast-forward.', () => {})\n})\n"],
5
+ "mappings": "AAAA,SAAS,YAAY;AAerB,KAAK,SAAS,6CAA6C,MAAM;AAC/D,OAAK,KAAK,6LAAwL,MAAM;AAAA,EAAC,CAAC;AAC5M,CAAC;",
6
+ "names": []
7
+ }
@@ -0,0 +1,6 @@
1
+ import { test } from "@playwright/test";
2
+ test.describe("TC-CHANNEL-EMAIL-028: Malformed MIME \u2192 dead-letter", () => {
3
+ test.skip("behavioral coverage: workers/__tests__/poll-channel.test.ts (permanent ingest failure \u2192 dead-letter + cursor advances; transient \u2192 cursor held). Playwright E2E is infeasible \u2014 provider mock seams are process-local.", () => {
4
+ });
5
+ });
6
+ //# sourceMappingURL=TC-CHANNEL-EMAIL-028.spec.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-028.spec.ts"],
4
+ "sourcesContent": ["import { test } from '@playwright/test'\n\n/**\n * TC-CHANNEL-EMAIL-028 \u2014 Malformed MIME \u2192 dead-letter, cursor advances\n *\n * Spec B \u00A7 Phase B4 \u2014 `poll-channel.ts` classifies per-message ingest\n * failures: transient failures abort the loop without advancing the\n * cursor (idempotent retry on next tick), permanent failures write the\n * raw MIME blob + error metadata to `ChannelIngestDeadLetter`\n * (encrypted at rest via `defaultEncryptionMaps`) and the cursor\n * advances anyway so the bad blob never re-stalls the channel.\n *\n * Unit-tested via the `ChannelIngestDeadLetter row shape (Spec B \u00A7 B4)`\n * describe in `ingest-inbound-message.test.ts` plus the implementation\n * paths in `poll-channel.test.ts`. Full E2E in the QA scenario.\n */\ntest.describe('TC-CHANNEL-EMAIL-028: Malformed MIME \u2192 dead-letter', () => {\n test.skip('behavioral coverage: workers/__tests__/poll-channel.test.ts (permanent ingest failure \u2192 dead-letter + cursor advances; transient \u2192 cursor held). Playwright E2E is infeasible \u2014 provider mock seams are process-local.', () => {})\n})\n"],
5
+ "mappings": "AAAA,SAAS,YAAY;AAgBrB,KAAK,SAAS,2DAAsD,MAAM;AACxE,OAAK,KAAK,yOAA0N,MAAM;AAAA,EAAC,CAAC;AAC9O,CAAC;",
6
+ "names": []
7
+ }
@@ -0,0 +1,48 @@
1
+ import { expect, test } from "@playwright/test";
2
+ import { getAuthToken } from "@open-mercato/core/helpers/integration/authFixtures";
3
+ import { apiRequest } from "@open-mercato/core/helpers/integration/api";
4
+ test.describe("TC-CHANNEL-EMAIL-029: import-history route wiring", () => {
5
+ const FAKE_CHANNEL_ID = "00000000-0000-0000-0000-000000000029";
6
+ test("rejects unauthenticated requests", async ({ request }) => {
7
+ const response = await apiRequest(
8
+ request,
9
+ "POST",
10
+ `/api/communication_channels/channels/${FAKE_CHANNEL_ID}/import-history`,
11
+ // Intentionally empty token — this test asserts the 401 unauth path.
12
+ { token: "", data: { sinceDays: 14 } }
13
+ );
14
+ expect(response.status()).toBe(401);
15
+ });
16
+ test("returns 400 on invalid channel id", async ({ request }) => {
17
+ const token = await getAuthToken(request);
18
+ const response = await apiRequest(
19
+ request,
20
+ "POST",
21
+ "/api/communication_channels/channels/not-a-uuid/import-history",
22
+ { token, data: { sinceDays: 14 } }
23
+ );
24
+ expect(response.status()).toBe(400);
25
+ });
26
+ test("returns 400 when body fails Zod validation (sinceDays out of range)", async ({ request }) => {
27
+ const token = await getAuthToken(request);
28
+ const response = await apiRequest(
29
+ request,
30
+ "POST",
31
+ `/api/communication_channels/channels/${FAKE_CHANNEL_ID}/import-history`,
32
+ { token, data: { sinceDays: 9999 } }
33
+ );
34
+ expect(response.status()).toBe(400);
35
+ });
36
+ test("returns 404 for a channel the caller does not own", async ({ request }) => {
37
+ const token = await getAuthToken(request);
38
+ const response = await apiRequest(
39
+ request,
40
+ "POST",
41
+ `/api/communication_channels/channels/${FAKE_CHANNEL_ID}/import-history`,
42
+ { token, data: { sinceDays: 14, maxMessages: 100 } }
43
+ );
44
+ expect(response.status(), "route should not 5xx").toBeLessThan(500);
45
+ expect([400, 403, 404]).toContain(response.status());
46
+ });
47
+ });
48
+ //# sourceMappingURL=TC-CHANNEL-EMAIL-029.spec.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-029.spec.ts"],
4
+ "sourcesContent": ["import { expect, test } from '@playwright/test'\nimport { getAuthToken } from '@open-mercato/core/helpers/integration/authFixtures'\nimport { apiRequest } from '@open-mercato/core/helpers/integration/api'\n\n/**\n * TC-CHANNEL-EMAIL-029 \u2014 operator-triggered backlog import (Spec B \u00A7 Phase B6).\n *\n * Verifies that the `/api/communication_channels/channels/{id}/import-history`\n * route is reachable, requires authentication, validates the body, and 404s\n * for non-existent / not-owned channels (the access-control branch).\n *\n * Concurrency guard (429), the ProgressJob lifecycle, and the IMAP SEARCH +\n * fetch loop are exercised by the unit-test layer:\n * - packages/core/src/modules/communication_channels/commands/__tests__/queue-import-history.test.ts\n * - packages/core/src/modules/communication_channels/workers/__tests__/channel-import-history.test.ts\n * - packages/channel-imap/src/modules/channel_imap/lib/__tests__/adapter.test.ts (importHistory cases)\n *\n * The full end-to-end (connect a mailbox \u2192 POST /import-history \u2192 ProgressJob\n * completes \u2192 imported messages visible on Person timeline) is captured in\n * `.ai/qa/scenarios/TC-CHANNEL-EMAIL-029-import-history.md` for manual QA.\n */\ntest.describe('TC-CHANNEL-EMAIL-029: import-history route wiring', () => {\n const FAKE_CHANNEL_ID = '00000000-0000-0000-0000-000000000029'\n\n test('rejects unauthenticated requests', async ({ request }) => {\n const response = await apiRequest(\n request,\n 'POST',\n `/api/communication_channels/channels/${FAKE_CHANNEL_ID}/import-history`,\n // Intentionally empty token \u2014 this test asserts the 401 unauth path.\n { token: '', data: { sinceDays: 14 } },\n )\n expect(response.status()).toBe(401)\n })\n\n test('returns 400 on invalid channel id', async ({ request }) => {\n const token = await getAuthToken(request)\n const response = await apiRequest(\n request,\n 'POST',\n '/api/communication_channels/channels/not-a-uuid/import-history',\n { token, data: { sinceDays: 14 } },\n )\n expect(response.status()).toBe(400)\n })\n\n test('returns 400 when body fails Zod validation (sinceDays out of range)', async ({ request }) => {\n const token = await getAuthToken(request)\n const response = await apiRequest(\n request,\n 'POST',\n `/api/communication_channels/channels/${FAKE_CHANNEL_ID}/import-history`,\n { token, data: { sinceDays: 9999 } },\n )\n expect(response.status()).toBe(400)\n })\n\n test('returns 404 for a channel the caller does not own', async ({ request }) => {\n const token = await getAuthToken(request)\n const response = await apiRequest(\n request,\n 'POST',\n `/api/communication_channels/channels/${FAKE_CHANNEL_ID}/import-history`,\n { token, data: { sinceDays: 14, maxMessages: 100 } },\n )\n // A caller without an organization scope is rejected with 400 (\"No\n // organization scope\") before the channel lookup; a scoped caller reaches\n // the channel-not-found branch (404, same shape as access-denied for parity).\n expect(response.status(), 'route should not 5xx').toBeLessThan(500)\n expect([400, 403, 404]).toContain(response.status())\n })\n})\n"],
5
+ "mappings": "AAAA,SAAS,QAAQ,YAAY;AAC7B,SAAS,oBAAoB;AAC7B,SAAS,kBAAkB;AAmB3B,KAAK,SAAS,qDAAqD,MAAM;AACvE,QAAM,kBAAkB;AAExB,OAAK,oCAAoC,OAAO,EAAE,QAAQ,MAAM;AAC9D,UAAM,WAAW,MAAM;AAAA,MACrB;AAAA,MACA;AAAA,MACA,wCAAwC,eAAe;AAAA;AAAA,MAEvD,EAAE,OAAO,IAAI,MAAM,EAAE,WAAW,GAAG,EAAE;AAAA,IACvC;AACA,WAAO,SAAS,OAAO,CAAC,EAAE,KAAK,GAAG;AAAA,EACpC,CAAC;AAED,OAAK,qCAAqC,OAAO,EAAE,QAAQ,MAAM;AAC/D,UAAM,QAAQ,MAAM,aAAa,OAAO;AACxC,UAAM,WAAW,MAAM;AAAA,MACrB;AAAA,MACA;AAAA,MACA;AAAA,MACA,EAAE,OAAO,MAAM,EAAE,WAAW,GAAG,EAAE;AAAA,IACnC;AACA,WAAO,SAAS,OAAO,CAAC,EAAE,KAAK,GAAG;AAAA,EACpC,CAAC;AAED,OAAK,uEAAuE,OAAO,EAAE,QAAQ,MAAM;AACjG,UAAM,QAAQ,MAAM,aAAa,OAAO;AACxC,UAAM,WAAW,MAAM;AAAA,MACrB;AAAA,MACA;AAAA,MACA,wCAAwC,eAAe;AAAA,MACvD,EAAE,OAAO,MAAM,EAAE,WAAW,KAAK,EAAE;AAAA,IACrC;AACA,WAAO,SAAS,OAAO,CAAC,EAAE,KAAK,GAAG;AAAA,EACpC,CAAC;AAED,OAAK,qDAAqD,OAAO,EAAE,QAAQ,MAAM;AAC/E,UAAM,QAAQ,MAAM,aAAa,OAAO;AACxC,UAAM,WAAW,MAAM;AAAA,MACrB;AAAA,MACA;AAAA,MACA,wCAAwC,eAAe;AAAA,MACvD,EAAE,OAAO,MAAM,EAAE,WAAW,IAAI,aAAa,IAAI,EAAE;AAAA,IACrD;AAIA,WAAO,SAAS,OAAO,GAAG,sBAAsB,EAAE,aAAa,GAAG;AAClE,WAAO,CAAC,KAAK,KAAK,GAAG,CAAC,EAAE,UAAU,SAAS,OAAO,CAAC;AAAA,EACrD,CAAC;AACH,CAAC;",
6
+ "names": []
7
+ }
@@ -0,0 +1,6 @@
1
+ import { test } from "@playwright/test";
2
+ test.describe("TC-CHANNEL-EMAIL-030: Sent-folder dedup", () => {
3
+ test.skip("behavioral coverage: ingest-inbound-message.test.ts (sent-folder dedup contract + dedup \u2192 status=duplicate). Playwright E2E is infeasible \u2014 provider mock seams are process-local.", () => {
4
+ });
5
+ });
6
+ //# sourceMappingURL=TC-CHANNEL-EMAIL-030.spec.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-030.spec.ts"],
4
+ "sourcesContent": ["import { test } from '@playwright/test'\n\n/**\n * TC-CHANNEL-EMAIL-030 \u2014 Sent-folder dedup\n *\n * Spec B \u00A7 Phase B3 \u2014 `ingest-inbound-message.ts` short-circuits when\n * the inbound message's `messageId` matches an outbound\n * `MessageChannelLink.channelMetadata.messageId` we already sent. This\n * prevents IMAP polls of the Sent folder from creating duplicate\n * inbound rows for every outbound the user sent.\n *\n * Contract unit-tested in `ingest-inbound-message.test.ts` (sent-folder\n * dedup describe block). Full E2E in the QA scenario markdown \u2014 send\n * an outbound, observe Sent folder surfaces it, confirm no duplicate\n * `MessageChannelLink` row appears for the inbound direction.\n */\ntest.describe('TC-CHANNEL-EMAIL-030: Sent-folder dedup', () => {\n test.skip('behavioral coverage: ingest-inbound-message.test.ts (sent-folder dedup contract + dedup \u2192 status=duplicate). Playwright E2E is infeasible \u2014 provider mock seams are process-local.', () => {})\n})\n"],
5
+ "mappings": "AAAA,SAAS,YAAY;AAgBrB,KAAK,SAAS,2CAA2C,MAAM;AAC7D,OAAK,KAAK,gMAAsL,MAAM;AAAA,EAAC,CAAC;AAC1M,CAAC;",
6
+ "names": []
7
+ }
@@ -0,0 +1,10 @@
1
+ const features = [
2
+ { id: "channel_imap.view", title: "View IMAP channel configuration", module: "channel_imap" },
3
+ { id: "channel_imap.configure", title: "Configure IMAP channel defaults", module: "channel_imap" }
4
+ ];
5
+ var acl_default = features;
6
+ export {
7
+ acl_default as default,
8
+ features
9
+ };
10
+ //# sourceMappingURL=acl.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/modules/channel_imap/acl.ts"],
4
+ "sourcesContent": ["export const features = [\n { id: 'channel_imap.view', title: 'View IMAP channel configuration', module: 'channel_imap' },\n { id: 'channel_imap.configure', title: 'Configure IMAP channel defaults', module: 'channel_imap' },\n]\n\nexport default features\n"],
5
+ "mappings": "AAAO,MAAM,WAAW;AAAA,EACtB,EAAE,IAAI,qBAAqB,OAAO,mCAAmC,QAAQ,eAAe;AAAA,EAC5F,EAAE,IAAI,0BAA0B,OAAO,mCAAmC,QAAQ,eAAe;AACnG;AAEA,IAAO,cAAQ;",
6
+ "names": []
7
+ }
@@ -0,0 +1,23 @@
1
+ import { asValue } from "awilix";
2
+ import {
3
+ hasChannelAdapter,
4
+ registerChannelAdapter
5
+ } from "@open-mercato/core/modules/communication_channels/lib/adapter-registry-singleton";
6
+ import { getImapChannelAdapter } from "./lib/adapter.js";
7
+ import { channelImapHealthCheck } from "./lib/health.js";
8
+ function register(container) {
9
+ if (!hasChannelAdapter("imap")) {
10
+ registerChannelAdapter(getImapChannelAdapter());
11
+ }
12
+ container.register({
13
+ channelImapAdapter: asValue(getImapChannelAdapter()),
14
+ // Registered under the exact service name declared in `integration.ts`
15
+ // (`healthCheck.service`). Without this, the hub's `container.resolve(...)`
16
+ // throws and the channel reports permanently 'unhealthy'.
17
+ channelImapHealthCheck: asValue(channelImapHealthCheck)
18
+ });
19
+ }
20
+ export {
21
+ register
22
+ };
23
+ //# sourceMappingURL=di.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/modules/channel_imap/di.ts"],
4
+ "sourcesContent": ["import { asValue } from 'awilix'\nimport type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport {\n hasChannelAdapter,\n registerChannelAdapter,\n} from '@open-mercato/core/modules/communication_channels/lib/adapter-registry-singleton'\nimport { getImapChannelAdapter } from './lib/adapter'\nimport { channelImapHealthCheck } from './lib/health'\n\n/**\n * Re-register the adapter on container creation as a safety net for runtime\n * environments that bypass module setup (worker-only nodes, ad-hoc CLI). The\n * underlying registry is process-wide so the registration is idempotent.\n */\nexport function register(container: AppContainer): void {\n if (!hasChannelAdapter('imap')) {\n registerChannelAdapter(getImapChannelAdapter())\n }\n container.register({\n channelImapAdapter: asValue(getImapChannelAdapter()),\n // Registered under the exact service name declared in `integration.ts`\n // (`healthCheck.service`). Without this, the hub's `container.resolve(...)`\n // throws and the channel reports permanently 'unhealthy'.\n channelImapHealthCheck: asValue(channelImapHealthCheck),\n })\n}\n"],
5
+ "mappings": "AAAA,SAAS,eAAe;AAExB;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,6BAA6B;AACtC,SAAS,8BAA8B;AAOhC,SAAS,SAAS,WAA+B;AACtD,MAAI,CAAC,kBAAkB,MAAM,GAAG;AAC9B,2BAAuB,sBAAsB,CAAC;AAAA,EAChD;AACA,YAAU,SAAS;AAAA,IACjB,oBAAoB,QAAQ,sBAAsB,CAAC;AAAA;AAAA;AAAA;AAAA,IAInD,wBAAwB,QAAQ,sBAAsB;AAAA,EACxD,CAAC;AACH;",
6
+ "names": []
7
+ }
@@ -0,0 +1,9 @@
1
+ const metadata = {
2
+ id: "channel_imap",
3
+ title: "IMAP + SMTP Email Channel",
4
+ description: "Connect personal mailboxes via IMAP for inbound polling and SMTP for outbound delivery. Pairs with the Communications Hub (SPEC-045d)."
5
+ };
6
+ export {
7
+ metadata
8
+ };
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/modules/channel_imap/index.ts"],
4
+ "sourcesContent": ["export const metadata = {\n id: 'channel_imap',\n title: 'IMAP + SMTP Email Channel',\n description:\n 'Connect personal mailboxes via IMAP for inbound polling and SMTP for outbound delivery. Pairs with the Communications Hub (SPEC-045d).',\n}\n"],
5
+ "mappings": "AAAO,MAAM,WAAW;AAAA,EACtB,IAAI;AAAA,EACJ,OAAO;AAAA,EACP,aACE;AACJ;",
6
+ "names": []
7
+ }