@dxos/plugin-jmap 0.10.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 (180) hide show
  1. package/LICENSE +105 -0
  2. package/dist/lib/JmapPlugin.mjs +16 -0
  3. package/dist/lib/JmapPlugin.mjs.map +1 -0
  4. package/dist/lib/apis.mjs +2 -0
  5. package/dist/lib/capabilities.mjs +17 -0
  6. package/dist/lib/capabilities.mjs.map +1 -0
  7. package/dist/lib/chunk-apis.mjs +756 -0
  8. package/dist/lib/chunk-apis.mjs.map +1 -0
  9. package/dist/lib/chunk-connector.mjs +111 -0
  10. package/dist/lib/chunk-connector.mjs.map +1 -0
  11. package/dist/lib/chunk-constants.mjs +23 -0
  12. package/dist/lib/chunk-constants.mjs.map +1 -0
  13. package/dist/lib/chunk-errors.mjs +42 -0
  14. package/dist/lib/chunk-errors.mjs.map +1 -0
  15. package/dist/lib/chunk-handler.mjs +27 -0
  16. package/dist/lib/chunk-handler.mjs.map +1 -0
  17. package/dist/lib/chunk-jmap-fixtures.mjs +102 -0
  18. package/dist/lib/chunk-jmap-fixtures.mjs.map +1 -0
  19. package/dist/lib/chunk-mail-send.mjs +17 -0
  20. package/dist/lib/chunk-mail-send.mjs.map +1 -0
  21. package/dist/lib/chunk-meta.mjs +37 -0
  22. package/dist/lib/chunk-meta.mjs.map +1 -0
  23. package/dist/lib/chunk-operation-handler.mjs +12 -0
  24. package/dist/lib/chunk-operation-handler.mjs.map +1 -0
  25. package/dist/lib/chunk-send.mjs +98 -0
  26. package/dist/lib/chunk-send.mjs.map +1 -0
  27. package/dist/lib/chunk-sync-provider.mjs +511 -0
  28. package/dist/lib/chunk-sync-provider.mjs.map +1 -0
  29. package/dist/lib/chunk-sync.mjs +37 -0
  30. package/dist/lib/chunk-sync.mjs.map +1 -0
  31. package/dist/lib/index.mjs +4 -0
  32. package/dist/lib/meta.mjs +2 -0
  33. package/dist/lib/operations.mjs +13 -0
  34. package/dist/lib/operations.mjs.map +1 -0
  35. package/dist/lib/plugin.mjs +9 -0
  36. package/dist/lib/plugin.mjs.map +1 -0
  37. package/dist/lib/services.mjs +139 -0
  38. package/dist/lib/services.mjs.map +1 -0
  39. package/dist/lib/testing/node.mjs +2 -0
  40. package/dist/lib/testing.mjs +19 -0
  41. package/dist/lib/testing.mjs.map +1 -0
  42. package/dist/lib/translations.mjs +7 -0
  43. package/dist/lib/translations.mjs.map +1 -0
  44. package/dist/types/dx.config.d.ts +28 -0
  45. package/dist/types/dx.config.d.ts.map +1 -0
  46. package/dist/types/src/JmapPlugin.d.ts +9 -0
  47. package/dist/types/src/JmapPlugin.d.ts.map +1 -0
  48. package/dist/types/src/apis/Jmap/api.d.ts +42 -0
  49. package/dist/types/src/apis/Jmap/api.d.ts.map +1 -0
  50. package/dist/types/src/apis/Jmap/index.d.ts +3 -0
  51. package/dist/types/src/apis/Jmap/index.d.ts.map +1 -0
  52. package/dist/types/src/apis/Jmap/types.d.ts +65 -0
  53. package/dist/types/src/apis/Jmap/types.d.ts.map +1 -0
  54. package/dist/types/src/apis/JmapMail/api.d.ts +223 -0
  55. package/dist/types/src/apis/JmapMail/api.d.ts.map +1 -0
  56. package/dist/types/src/apis/JmapMail/index.d.ts +4 -0
  57. package/dist/types/src/apis/JmapMail/index.d.ts.map +1 -0
  58. package/dist/types/src/apis/JmapMail/query.d.ts +26 -0
  59. package/dist/types/src/apis/JmapMail/query.d.ts.map +1 -0
  60. package/dist/types/src/apis/JmapMail/query.test.d.ts +2 -0
  61. package/dist/types/src/apis/JmapMail/query.test.d.ts.map +1 -0
  62. package/dist/types/src/apis/JmapMail/types.d.ts +290 -0
  63. package/dist/types/src/apis/JmapMail/types.d.ts.map +1 -0
  64. package/dist/types/src/apis/index.d.ts +3 -0
  65. package/dist/types/src/apis/index.d.ts.map +1 -0
  66. package/dist/types/src/apis/jmap-api.test.d.ts +2 -0
  67. package/dist/types/src/apis/jmap-api.test.d.ts.map +1 -0
  68. package/dist/types/src/capabilities/connector.d.ts +5 -0
  69. package/dist/types/src/capabilities/connector.d.ts.map +1 -0
  70. package/dist/types/src/capabilities/credential-form.d.ts +37 -0
  71. package/dist/types/src/capabilities/credential-form.d.ts.map +1 -0
  72. package/dist/types/src/capabilities/credential-form.test.d.ts +2 -0
  73. package/dist/types/src/capabilities/credential-form.test.d.ts.map +1 -0
  74. package/dist/types/src/capabilities/index.d.ts +5 -0
  75. package/dist/types/src/capabilities/index.d.ts.map +1 -0
  76. package/dist/types/src/capabilities/mail-send.d.ts +6 -0
  77. package/dist/types/src/capabilities/mail-send.d.ts.map +1 -0
  78. package/dist/types/src/capabilities/operation-handler.d.ts +5 -0
  79. package/dist/types/src/capabilities/operation-handler.d.ts.map +1 -0
  80. package/dist/types/src/constants.d.ts +19 -0
  81. package/dist/types/src/constants.d.ts.map +1 -0
  82. package/dist/types/src/errors.d.ts +103 -0
  83. package/dist/types/src/errors.d.ts.map +1 -0
  84. package/dist/types/src/index.d.ts +4 -0
  85. package/dist/types/src/index.d.ts.map +1 -0
  86. package/dist/types/src/meta.d.ts +33 -0
  87. package/dist/types/src/meta.d.ts.map +1 -0
  88. package/dist/types/src/operations/index.d.ts +3 -0
  89. package/dist/types/src/operations/index.d.ts.map +1 -0
  90. package/dist/types/src/operations/mail/mapper.d.ts +49 -0
  91. package/dist/types/src/operations/mail/mapper.d.ts.map +1 -0
  92. package/dist/types/src/operations/mail/materialize/handler.d.ts +11 -0
  93. package/dist/types/src/operations/mail/materialize/handler.d.ts.map +1 -0
  94. package/dist/types/src/operations/mail/send/handler.d.ts +4 -0
  95. package/dist/types/src/operations/mail/send/handler.d.ts.map +1 -0
  96. package/dist/types/src/operations/mail/send/index.d.ts +2 -0
  97. package/dist/types/src/operations/mail/send/index.d.ts.map +1 -0
  98. package/dist/types/src/operations/mail/sync/handler.d.ts +4 -0
  99. package/dist/types/src/operations/mail/sync/handler.d.ts.map +1 -0
  100. package/dist/types/src/operations/mail/sync/index.d.ts +3 -0
  101. package/dist/types/src/operations/mail/sync/index.d.ts.map +1 -0
  102. package/dist/types/src/operations/mail/sync/mapper.test.d.ts +2 -0
  103. package/dist/types/src/operations/mail/sync/mapper.test.d.ts.map +1 -0
  104. package/dist/types/src/operations/mail/sync/sync-e2e.test.d.ts +2 -0
  105. package/dist/types/src/operations/mail/sync/sync-e2e.test.d.ts.map +1 -0
  106. package/dist/types/src/operations/mail/sync/sync-provider.d.ts +11 -0
  107. package/dist/types/src/operations/mail/sync/sync-provider.d.ts.map +1 -0
  108. package/dist/types/src/operations/mail/sync/sync.test.d.ts +2 -0
  109. package/dist/types/src/operations/mail/sync/sync.test.d.ts.map +1 -0
  110. package/dist/types/src/operations/mail/sync/system-tags.d.ts +14 -0
  111. package/dist/types/src/operations/mail/sync/system-tags.d.ts.map +1 -0
  112. package/dist/types/src/operations/mail/tags.d.ts +12 -0
  113. package/dist/types/src/operations/mail/tags.d.ts.map +1 -0
  114. package/dist/types/src/plugin.d.ts +4 -0
  115. package/dist/types/src/plugin.d.ts.map +1 -0
  116. package/dist/types/src/services/index.d.ts +3 -0
  117. package/dist/types/src/services/index.d.ts.map +1 -0
  118. package/dist/types/src/services/jmap-credentials.d.ts +39 -0
  119. package/dist/types/src/services/jmap-credentials.d.ts.map +1 -0
  120. package/dist/types/src/services/jmap-mail-api.d.ts +109 -0
  121. package/dist/types/src/services/jmap-mail-api.d.ts.map +1 -0
  122. package/dist/types/src/testing/index.d.ts +5 -0
  123. package/dist/types/src/testing/index.d.ts.map +1 -0
  124. package/dist/types/src/testing/jmap-fixtures.d.ts +26 -0
  125. package/dist/types/src/testing/jmap-fixtures.d.ts.map +1 -0
  126. package/dist/types/src/testing/jmap-fixtures.test.d.ts +2 -0
  127. package/dist/types/src/testing/jmap-fixtures.test.d.ts.map +1 -0
  128. package/dist/types/src/testing/node.d.ts +4 -0
  129. package/dist/types/src/testing/node.d.ts.map +1 -0
  130. package/dist/types/src/testing/sync-fixture.d.ts +22 -0
  131. package/dist/types/src/testing/sync-fixture.d.ts.map +1 -0
  132. package/dist/types/src/translations.d.ts +8 -0
  133. package/dist/types/src/translations.d.ts.map +1 -0
  134. package/dist/types/tsconfig.tsbuildinfo +1 -0
  135. package/dx.config.ts +32 -0
  136. package/package.json +121 -0
  137. package/src/JmapPlugin.ts +25 -0
  138. package/src/apis/Jmap/api.ts +138 -0
  139. package/src/apis/Jmap/index.ts +6 -0
  140. package/src/apis/Jmap/types.ts +86 -0
  141. package/src/apis/JmapMail/api.ts +324 -0
  142. package/src/apis/JmapMail/index.ts +7 -0
  143. package/src/apis/JmapMail/query.test.ts +181 -0
  144. package/src/apis/JmapMail/query.ts +231 -0
  145. package/src/apis/JmapMail/types.ts +196 -0
  146. package/src/apis/index.ts +8 -0
  147. package/src/apis/jmap-api.test.ts +261 -0
  148. package/src/capabilities/connector.ts +44 -0
  149. package/src/capabilities/credential-form.test.ts +69 -0
  150. package/src/capabilities/credential-form.ts +113 -0
  151. package/src/capabilities/index.ts +24 -0
  152. package/src/capabilities/mail-send.ts +21 -0
  153. package/src/capabilities/operation-handler.ts +16 -0
  154. package/src/constants.ts +24 -0
  155. package/src/errors.ts +38 -0
  156. package/src/index.ts +7 -0
  157. package/src/meta.ts +9 -0
  158. package/src/operations/index.ts +13 -0
  159. package/src/operations/mail/mapper.ts +191 -0
  160. package/src/operations/mail/materialize/handler.ts +41 -0
  161. package/src/operations/mail/send/handler.ts +116 -0
  162. package/src/operations/mail/send/index.ts +5 -0
  163. package/src/operations/mail/sync/handler.ts +52 -0
  164. package/src/operations/mail/sync/index.ts +7 -0
  165. package/src/operations/mail/sync/mapper.test.ts +150 -0
  166. package/src/operations/mail/sync/sync-e2e.test.ts +63 -0
  167. package/src/operations/mail/sync/sync-provider.ts +481 -0
  168. package/src/operations/mail/sync/sync.test.ts +766 -0
  169. package/src/operations/mail/sync/system-tags.ts +24 -0
  170. package/src/operations/mail/tags.ts +29 -0
  171. package/src/plugin.ts +11 -0
  172. package/src/services/index.ts +6 -0
  173. package/src/services/jmap-credentials.ts +61 -0
  174. package/src/services/jmap-mail-api.ts +294 -0
  175. package/src/testing/index.ts +8 -0
  176. package/src/testing/jmap-fixtures.test.ts +107 -0
  177. package/src/testing/jmap-fixtures.ts +105 -0
  178. package/src/testing/node.ts +12 -0
  179. package/src/testing/sync-fixture.ts +39 -0
  180. package/src/translations.ts +15 -0
@@ -0,0 +1,98 @@
1
+ import { d as mailboxGet, f as submitEmail, h as JmapCredentials, m as getSession, u as identityGet } from "./chunk-apis.mjs";
2
+ import { n as JmapSendIdentityNotFoundError, r as JmapSendMessageInvalidError, t as JmapApiError } from "./chunk-errors.mjs";
3
+ import "./services.mjs";
4
+ import * as Effect from "effect/Effect";
5
+ import { log } from "@dxos/log";
6
+ import * as Operation from "@dxos/compute/Operation";
7
+ import * as InboxOperation from "@dxos/plugin-inbox/InboxOperation";
8
+ import * as SystemTags from "@dxos/plugin-inbox/SystemTags";
9
+ import * as FetchHttpClient from "@effect/platform/FetchHttpClient";
10
+ //#region src/operations/mail/send/handler.ts
11
+ var __dxlog_file = "/Users/burdon/Code/dxos/dxos/.claude/worktrees/confident-jones-b0b4bf/packages/plugins/plugin-jmap/src/operations/mail/send/handler.ts";
12
+ var MAIL_ACCOUNT_CAPABILITY = "urn:ietf:params:jmap:mail";
13
+ var handler_default = InboxOperation.JmapSend.pipe(Operation.withHandler(({ message, connection: connectionRef }) => Effect.gen(function* () {
14
+ log("sending email via jmap", { connection: connectionRef.uri }, {
15
+ "~LogMeta": "~LogMeta",
16
+ F: __dxlog_file,
17
+ L: 22,
18
+ S: this
19
+ });
20
+ const to = message.properties?.to;
21
+ const cc = message.properties?.cc;
22
+ const bcc = message.properties?.bcc;
23
+ const subject = message.properties?.subject;
24
+ const inReplyTo = message.properties?.inReplyTo;
25
+ const references = message.properties?.references;
26
+ const textBlock = message.blocks.find((block) => block._tag === "text");
27
+ const text = textBlock?._tag === "text" ? textBlock.text : void 0;
28
+ if (!to || !text) return yield* Effect.fail(new JmapSendMessageInvalidError());
29
+ const session = yield* getSession;
30
+ const accountId = session.primaryAccounts[MAIL_ACCOUNT_CAPABILITY];
31
+ if (!accountId) return yield* Effect.fail(new JmapApiError(void 0, "JMAP session has no mail account."));
32
+ const target = {
33
+ apiUrl: session.apiUrl,
34
+ accountId
35
+ };
36
+ const account = (yield* JmapCredentials).account ?? session.username;
37
+ const { list: identities } = yield* identityGet(target);
38
+ const identity = account ? identities.find((candidate) => candidate.email.toLowerCase() === account.toLowerCase()) : identities[0];
39
+ if (!identity) return yield* Effect.fail(new JmapSendIdentityNotFoundError(account));
40
+ const { list: folders } = yield* mailboxGet(target);
41
+ const drafts = folders.find((folder) => folder.role === "drafts");
42
+ const sent = folders.find((folder) => folder.role === "sent");
43
+ if (!drafts || !sent) return yield* Effect.fail(new JmapApiError(void 0, "JMAP account is missing a Drafts or Sent folder."));
44
+ const fromAddress = identity.name ? {
45
+ email: identity.email,
46
+ name: identity.name
47
+ } : { email: identity.email };
48
+ const result = yield* submitEmail(target, {
49
+ identityId: identity.id,
50
+ draftsMailboxId: drafts.id,
51
+ sentMailboxId: sent.id,
52
+ draft: {
53
+ from: [fromAddress],
54
+ to: parseAddressList(to),
55
+ ...cc ? { cc: parseAddressList(cc) } : {},
56
+ ...bcc ? { bcc: parseAddressList(bcc) } : {},
57
+ ...subject ? { subject } : {},
58
+ ...inReplyTo ? { inReplyTo: [inReplyTo] } : {},
59
+ ...references ? { references: splitReferences(references) } : {},
60
+ text
61
+ }
62
+ });
63
+ log("email sent via jmap", { id: result.id }, {
64
+ "~LogMeta": "~LogMeta",
65
+ F: __dxlog_file,
66
+ L: 82,
67
+ S: this
68
+ });
69
+ return {
70
+ id: result.id,
71
+ threadId: result.threadId ?? "",
72
+ sentTag: {
73
+ ...SystemTags.systemTagKey("sent"),
74
+ label: SystemTags.SystemTag.sent.label
75
+ }
76
+ };
77
+ }).pipe(Effect.provide(FetchHttpClient.layer), Effect.provide(JmapCredentials.fromConnection(connectionRef)))), Operation.opaqueHandler);
78
+ /** Parses a comma-separated header value into JMAP structured addresses. */
79
+ var parseAddressList = (value) => value.split(",").map((part) => part.trim()).filter(Boolean).map(parseAddress);
80
+ /** Parses a single `"Name <email>"` (or bare `email`) into a JMAP structured address. */
81
+ var parseAddress = (value) => {
82
+ const match = value.match(/^\s*(?:"?([^"<]*?)"?\s+)?<([^>]+)>\s*$/);
83
+ if (match) {
84
+ const name = match[1]?.trim();
85
+ const email = match[2].trim();
86
+ return name ? {
87
+ email,
88
+ name
89
+ } : { email };
90
+ }
91
+ return { email: value.trim() };
92
+ };
93
+ /** Splits a `References` header (whitespace-separated message ids) into an array. */
94
+ var splitReferences = (value) => value.split(/\s+/).filter(Boolean);
95
+ //#endregion
96
+ export { handler_default as default };
97
+
98
+ //# sourceMappingURL=chunk-send.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chunk-send.mjs","names":[],"sources":["../../src/operations/mail/send/handler.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport * as FetchHttpClient from '@effect/platform/FetchHttpClient';\nimport * as Effect from 'effect/Effect';\n\nimport * as Operation from '@dxos/compute/Operation';\nimport { log } from '@dxos/log';\nimport * as InboxOperation from '@dxos/plugin-inbox/InboxOperation';\nimport * as SystemTags from '@dxos/plugin-inbox/SystemTags';\n\nimport { Jmap, JmapMail } from '../../../apis';\nimport { JmapApiError, JmapSendIdentityNotFoundError, JmapSendMessageInvalidError } from '../../../errors';\nimport { JmapCredentials } from '../../../services';\n\nconst MAIL_ACCOUNT_CAPABILITY = 'urn:ietf:params:jmap:mail';\n\nexport default InboxOperation.JmapSend.pipe(\n Operation.withHandler(({ message, connection: connectionRef }) =>\n Effect.gen(function* () {\n log('sending email via jmap', { connection: connectionRef.uri });\n\n const to = message.properties?.to;\n const cc = message.properties?.cc;\n const bcc = message.properties?.bcc;\n const subject = message.properties?.subject;\n const inReplyTo = message.properties?.inReplyTo;\n const references = message.properties?.references;\n const textBlock = message.blocks.find((block) => block._tag === 'text');\n const text = textBlock?._tag === 'text' ? textBlock.text : undefined;\n\n if (!to || !text) {\n return yield* Effect.fail(new JmapSendMessageInvalidError());\n }\n\n const session = yield* Jmap.getSession;\n const accountId = session.primaryAccounts[MAIL_ACCOUNT_CAPABILITY];\n if (!accountId) {\n return yield* Effect.fail(new JmapApiError(undefined, 'JMAP session has no mail account.'));\n }\n const target: JmapMail.Target = { apiUrl: session.apiUrl, accountId };\n\n const credentials = yield* JmapCredentials;\n const account = credentials.account ?? session.username;\n\n // The submission requires an identity whose email the server permits as `from`; pick the one\n // matching the connection account (fall back to the first if the account is unknown).\n const { list: identities } = yield* JmapMail.identityGet(target);\n const identity = account\n ? identities.find((candidate) => candidate.email.toLowerCase() === account.toLowerCase())\n : identities[0];\n if (!identity) {\n return yield* Effect.fail(new JmapSendIdentityNotFoundError(account));\n }\n\n const { list: folders } = yield* JmapMail.mailboxGet(target);\n const drafts = folders.find((folder) => folder.role === 'drafts');\n const sent = folders.find((folder) => folder.role === 'sent');\n if (!drafts || !sent) {\n return yield* Effect.fail(new JmapApiError(undefined, 'JMAP account is missing a Drafts or Sent folder.'));\n }\n\n const fromAddress = identity.name ? { email: identity.email, name: identity.name } : { email: identity.email };\n\n const result = yield* JmapMail.submitEmail(target, {\n identityId: identity.id,\n draftsMailboxId: drafts.id,\n sentMailboxId: sent.id,\n draft: {\n from: [fromAddress],\n to: parseAddressList(to),\n ...(cc ? { cc: parseAddressList(cc) } : {}),\n ...(bcc ? { bcc: parseAddressList(bcc) } : {}),\n ...(subject ? { subject } : {}),\n ...(inReplyTo ? { inReplyTo: [inReplyTo] } : {}),\n ...(references ? { references: splitReferences(references) } : {}),\n text,\n },\n });\n\n log('email sent via jmap', { id: result.id });\n return {\n id: result.id,\n threadId: result.threadId ?? '',\n // The submission filed the message into the Sent (`sent`-role) folder, which sync maps onto the\n // canonical `sent` system tag; return that same canonical tag so the caller can tag the local\n // draft to match the copy that will sync down.\n sentTag: { ...SystemTags.systemTagKey('sent'), label: SystemTags.SystemTag.sent.label },\n };\n }).pipe(Effect.provide(FetchHttpClient.layer), Effect.provide(JmapCredentials.fromConnection(connectionRef))),\n ),\n Operation.opaqueHandler,\n);\n\n/** Parses a comma-separated header value into JMAP structured addresses. */\nconst parseAddressList = (value: string): { email: string; name?: string }[] =>\n value\n .split(',')\n .map((part) => part.trim())\n .filter(Boolean)\n .map(parseAddress);\n\n/** Parses a single `\"Name <email>\"` (or bare `email`) into a JMAP structured address. */\nconst parseAddress = (value: string): { email: string; name?: string } => {\n const match = value.match(/^\\s*(?:\"?([^\"<]*?)\"?\\s+)?<([^>]+)>\\s*$/);\n if (match) {\n const name = match[1]?.trim();\n const email = match[2].trim();\n return name ? { email, name } : { email };\n }\n return { email: value.trim() };\n};\n\n/** Splits a `References` header (whitespace-separated message ids) into an array. */\nconst splitReferences = (value: string): string[] => value.split(/\\s+/).filter(Boolean);\n"],"mappings":"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"}
@@ -0,0 +1,511 @@
1
+ import { i as resolveMailboxByNameOrRole, n as filterScopesMailbox, r as parseMailQuery } from "./chunk-apis.mjs";
2
+ import { n as JMAP_DOMAIN } from "./chunk-constants.mjs";
3
+ import { JmapMailApi } from "./services.mjs";
4
+ import * as Effect from "effect/Effect";
5
+ import * as Layer from "effect/Layer";
6
+ import { Obj, Ref, Tag } from "@dxos/echo";
7
+ import { log } from "@dxos/log";
8
+ import * as Option from "effect/Option";
9
+ import * as Predicate from "effect/Predicate";
10
+ import { MailSyncError, MailSyncProvider } from "@dxos/plugin-inbox/sync";
11
+ import * as Chunk from "effect/Chunk";
12
+ import * as Stream from "effect/Stream";
13
+ import { resolve } from "@dxos/extractor";
14
+ import { Cursor } from "@dxos/link";
15
+ import { Stage } from "@dxos/pipeline";
16
+ import * as Mailbox from "@dxos/plugin-inbox/Mailbox";
17
+ import * as SystemTags from "@dxos/plugin-inbox/SystemTags";
18
+ import { TagIndex } from "@dxos/schema";
19
+ import { Message, Person } from "@dxos/types";
20
+ //#region src/operations/mail/mapper.ts
21
+ var __dxlog_file$1 = "/Users/burdon/Code/dxos/dxos/.claude/worktrees/confident-jones-b0b4bf/packages/plugins/plugin-jmap/src/operations/mail/mapper.ts";
22
+ /** Attachment parts carry a `blobId` to fetch bytes for; parts without one can't be downloaded. */
23
+ var collectAttachments = (attachments) => (attachments ?? []).flatMap((part) => part.blobId ? [{
24
+ blobId: part.blobId,
25
+ name: part.name ?? void 0,
26
+ mimeType: part.type,
27
+ size: part.size,
28
+ contentId: part.cid ? part.cid.replace(/^<|>$/g, "") : void 0
29
+ }] : []);
30
+ /**
31
+ * Extracts the raw HTML and plaintext bodies from a JMAP email (values fetched via
32
+ * `fetchHTMLBodyValues`/`fetchTextBodyValues`). Returns `null` when the email has neither. No
33
+ * HTML→markdown conversion — the markdown/plain views derive from these raw blocks in the component.
34
+ */
35
+ var decodeBody = (email) => {
36
+ const html = getBodyValue(email, email.htmlBody);
37
+ const plain = getBodyValue(email, email.textBody);
38
+ if (html === void 0 && plain === void 0) {
39
+ log("jmap decodeBody: dropping email with no extractable body", {
40
+ id: email.id,
41
+ htmlBodyParts: email.htmlBody?.length ?? 0,
42
+ textBodyParts: email.textBody?.length ?? 0,
43
+ bodyValues: email.bodyValues ? Object.keys(email.bodyValues).length : 0
44
+ }, {
45
+ "~LogMeta": "~LogMeta",
46
+ F: __dxlog_file$1,
47
+ L: 72,
48
+ S: void 0
49
+ });
50
+ return null;
51
+ }
52
+ return {
53
+ raw: email,
54
+ html,
55
+ plain,
56
+ attachments: collectAttachments(email.attachments)
57
+ };
58
+ };
59
+ /**
60
+ * Builds an ECHO Message from a decoded (already body-normalized) JMAP email and an optional resolved
61
+ * sender contact. Pure — contact resolution is done by the caller. Returns `null` when the email has
62
+ * no sender.
63
+ */
64
+ var mapToMessage = (decoded, contact) => {
65
+ const { raw: email, html, plain } = decoded;
66
+ const fromAddress = email.from?.[0];
67
+ if (!fromAddress) {
68
+ log("jmap mapToMessage: dropping email with no sender", { id: email.id }, {
69
+ "~LogMeta": "~LogMeta",
70
+ F: __dxlog_file$1,
71
+ L: 92,
72
+ S: void 0
73
+ });
74
+ return null;
75
+ }
76
+ const sender = {
77
+ email: fromAddress.email,
78
+ ...fromAddress.name ? { name: fromAddress.name } : {},
79
+ ...contact ? { contact: Ref.make(contact) } : {}
80
+ };
81
+ const blocks = [];
82
+ if (html !== void 0) blocks.push({
83
+ _tag: "text",
84
+ text: html,
85
+ mimeType: "text/html"
86
+ });
87
+ if (plain !== void 0) blocks.push({
88
+ _tag: "text",
89
+ text: plain,
90
+ mimeType: "text/plain"
91
+ });
92
+ const echoMessage = Obj.make(Message.Message, {
93
+ [Obj.Meta]: { keys: [{
94
+ id: email.id,
95
+ source: JMAP_DOMAIN
96
+ }] },
97
+ created: email.receivedAt,
98
+ sender,
99
+ threadId: email.threadId,
100
+ properties: {
101
+ threadId: email.threadId,
102
+ snippet: email.preview ?? void 0,
103
+ subject: email.subject ?? void 0,
104
+ messageId: email.messageId?.[0],
105
+ inReplyTo: email.inReplyTo?.[0],
106
+ references: email.references?.join(" "),
107
+ to: formatAddresses(email.to),
108
+ cc: formatAddresses(email.cc)
109
+ },
110
+ blocks
111
+ });
112
+ const keywords = email.keywords ? Object.entries(email.keywords).filter(([, set]) => set).map(([keyword]) => keyword) : [];
113
+ return {
114
+ message: echoMessage,
115
+ mailboxIds: email.mailboxIds ? Object.keys(email.mailboxIds) : [],
116
+ keywords
117
+ };
118
+ };
119
+ Effect.fnUntraced(function* (email) {
120
+ const decoded = decodeBody(email);
121
+ if (!decoded) return null;
122
+ const fromAddress = email.from?.[0];
123
+ return mapToMessage(decoded, (fromAddress ? yield* resolve(Person.Person, { email: fromAddress.email }) : void 0) ?? void 0);
124
+ });
125
+ /** Formats a JMAP address as `"Name <email>"`, or just the address when unnamed. */
126
+ var formatAddress = (address) => address.name ? `${address.name} <${address.email}>` : address.email;
127
+ /** Joins an address list into a header-style string, or `undefined` when empty. */
128
+ var formatAddresses = (addresses) => addresses && addresses.length > 0 ? addresses.map(formatAddress).join(", ") : void 0;
129
+ /**
130
+ * Returns the first non-empty decoded value among the given body parts (e.g. `email.htmlBody` or
131
+ * `email.textBody`). `Email/get` is requested with `fetchHTMLBodyValues`/`fetchTextBodyValues`, so the
132
+ * referenced `bodyValues` are already decoded.
133
+ */
134
+ var getBodyValue = (email, parts) => {
135
+ for (const part of parts ?? []) {
136
+ const value = part.partId ? email.bodyValues?.[part.partId]?.value : void 0;
137
+ if (value && value.trim().length > 0) return value;
138
+ }
139
+ };
140
+ //#endregion
141
+ //#region src/operations/mail/tags.ts
142
+ /**
143
+ * The pre-rename source, matched as a fallback so folders synced before the rename keep their tag
144
+ * identity instead of being duplicated. Remove one release after landing.
145
+ */
146
+ var LEGACY_JMAP_TAG_SOURCE = "org.ietf.jmap.mailbox";
147
+ /**
148
+ * Finds an existing JMAP provider {@link Tag} object by its JMAP mailbox-id foreign key, or creates one
149
+ * carrying that key ({@link JMAP_DOMAIN}, which also marks it read-only via `Tag.isProviderTag`). Keeps
150
+ * the folder label in sync with the server on re-sync. Used by `sync/` for custom user folders;
151
+ * well-known roles map onto canonical system tags instead. Mirrors Gmail's `findOrCreateGmailTag`.
152
+ */
153
+ var findOrCreateJmapTag = (db, { id, name }) => Tag.findOrCreate(db, {
154
+ key: {
155
+ source: JMAP_DOMAIN,
156
+ id
157
+ },
158
+ legacyKeys: [{
159
+ source: LEGACY_JMAP_TAG_SOURCE,
160
+ id
161
+ }],
162
+ label: name
163
+ });
164
+ //#endregion
165
+ //#region src/operations/mail/sync/system-tags.ts
166
+ /**
167
+ * JMAP mailbox role → canonical system tag ({@link SystemTags.SystemTag}). Roles absent here are
168
+ * intentionally dropped: `archive` is derived as "not in inbox" (Gmail's model), and
169
+ * `drafts`/`trash`/`junk` are never synced.
170
+ */
171
+ var JMAP_ROLE_TAGS = {
172
+ inbox: "inbox",
173
+ sent: "sent"
174
+ };
175
+ /**
176
+ * JMAP keyword → canonical system tag ({@link SystemTags.SystemTag}). Only `$flagged` (starred) is
177
+ * projected; read-state (`$seen`) and the rest (`$answered`, `$draft`, `$forwarded`, …) are intentionally
178
+ * dropped as high-churn and noisier than useful.
179
+ */
180
+ var JMAP_KEYWORD_TAGS = { $flagged: "starred" };
181
+ //#endregion
182
+ //#region src/operations/mail/sync/sync-provider.ts
183
+ var __dxlog_file = "/Users/burdon/Code/dxos/dxos/.claude/worktrees/confident-jones-b0b4bf/packages/plugins/plugin-jmap/src/operations/mail/sync/sync-provider.ts";
184
+ var MAIL_ACCOUNT_CAPABILITY = "urn:ietf:params:jmap:mail";
185
+ /** JMAP mail's streaming-pipeline tuning; see {@link SyncStreamConfig.SyncStreamConfig}. */
186
+ var JMAP_SYNC_CONFIG = {
187
+ listPageSize: 50,
188
+ fetchConcurrency: 5,
189
+ commitPageSize: 10,
190
+ maxItemsPerRun: 500
191
+ };
192
+ /**
193
+ * JMAP's {@link MailSyncProvider}: session/account discovery, the email source, the folder→tag map, and
194
+ * the fused decode+map. Captures {@link JmapMailApi} + {@link Resolver} so the harness never names them.
195
+ * Mirror of the Gmail provider (`googleMailSyncProvider`).
196
+ */
197
+ var jmapMailSyncProvider = () => Layer.effect(MailSyncProvider, Effect.gen(function* () {
198
+ const context = yield* Effect.context();
199
+ const providerApi = yield* JmapMailApi;
200
+ return {
201
+ name: "jmap",
202
+ config: JMAP_SYNC_CONFIG,
203
+ foreignKeySource: JMAP_DOMAIN,
204
+ prepare: ({ db, binding, now, token, maxMessages }) => Effect.gen(function* () {
205
+ const api = yield* JmapMailApi;
206
+ const session = yield* api.getSession;
207
+ const accountId = session.primaryAccounts[MAIL_ACCOUNT_CAPABILITY];
208
+ if (!accountId) {
209
+ log.warn("jmap sync: session has no mail account", { username: session.username }, {
210
+ "~LogMeta": "~LogMeta",
211
+ F: __dxlog_file,
212
+ L: 73,
213
+ S: this
214
+ });
215
+ return;
216
+ }
217
+ const target = {
218
+ apiUrl: session.apiUrl,
219
+ accountId,
220
+ downloadUrl: session.downloadUrl
221
+ };
222
+ log("jmap sync: session resolved", {
223
+ apiUrl: session.apiUrl,
224
+ accountId
225
+ }, {
226
+ "~LogMeta": "~LogMeta",
227
+ F: __dxlog_file,
228
+ L: 77,
229
+ S: this
230
+ });
231
+ const { list: folders } = yield* api.mailboxGet(target);
232
+ const folderTagMap = /* @__PURE__ */ new Map();
233
+ for (const folder of folders) {
234
+ const canonical = folder.role ? JMAP_ROLE_TAGS[folder.role] : void 0;
235
+ if (canonical) {
236
+ const tag = yield* Effect.promise(() => SystemTags.findOrCreateSystemTag(db, canonical));
237
+ folderTagMap.set(folder.id, Mailbox.tagUri(tag));
238
+ } else if (folder.role) continue;
239
+ else {
240
+ const tag = yield* Effect.promise(() => findOrCreateJmapTag(db, {
241
+ id: folder.id,
242
+ name: folder.name
243
+ }));
244
+ folderTagMap.set(folder.id, Mailbox.tagUri(tag));
245
+ }
246
+ }
247
+ const keywordTagMap = /* @__PURE__ */ new Map();
248
+ for (const [keyword, canonical] of Object.entries(JMAP_KEYWORD_TAGS)) {
249
+ if (!canonical) continue;
250
+ const tag = yield* Effect.promise(() => SystemTags.findOrCreateSystemTag(db, canonical));
251
+ keywordTagMap.set(keyword, Mailbox.tagUri(tag));
252
+ }
253
+ const toMapped = (email) => Effect.gen(function* () {
254
+ const decoded = decodeBody(email);
255
+ if (!decoded) return;
256
+ const fromAddress = decoded.raw.from?.[0];
257
+ const mapped = mapToMessage(decoded, (fromAddress ? yield* resolve(Person.Person, { email: fromAddress.email }) : void 0) ?? void 0);
258
+ if (!mapped) return;
259
+ const folderUris = mapped.mailboxIds.flatMap((folderId) => {
260
+ const uri = folderTagMap.get(folderId);
261
+ return uri ? [uri] : [];
262
+ });
263
+ const keywordUris = mapped.keywords.flatMap((keyword) => {
264
+ const uri = keywordTagMap.get(keyword);
265
+ return uri ? [uri] : [];
266
+ });
267
+ const tagUris = [...folderUris, ...keywordUris];
268
+ const attachments = yield* fetchAttachments(target, decoded.attachments);
269
+ return {
270
+ _tag: "insert",
271
+ message: mapped.message,
272
+ foreignId: decoded.raw.id,
273
+ key: new Date(decoded.raw.receivedAt).getTime(),
274
+ tagUris,
275
+ attachments
276
+ };
277
+ });
278
+ const toItem = (email) => ({
279
+ foreignId: email.id,
280
+ key: new Date(email.receivedAt).getTime(),
281
+ process: toMapped(email).pipe(Effect.provide(context))
282
+ });
283
+ const captureFreshDelta = Effect.map(api.emailGet(target, []), (result) => ({
284
+ token: result.state,
285
+ createdIds: void 0,
286
+ updatedIds: [],
287
+ hasMoreDelta: false
288
+ }));
289
+ const { token: capturedToken, createdIds, updatedIds, hasMoreDelta } = yield* token === void 0 ? captureFreshDelta : api.emailChanges(target, token, maxMessages).pipe(Effect.map((result) => {
290
+ const created = new Set(result.created);
291
+ const updatedIds = result.updated.filter((id) => !created.has(id));
292
+ log("jmap sync: incremental delta", {
293
+ created: result.created.length,
294
+ updated: result.updated.length,
295
+ destroyed: result.destroyed.length,
296
+ hasMoreDelta: result.hasMoreChanges
297
+ }, {
298
+ "~LogMeta": "~LogMeta",
299
+ F: __dxlog_file,
300
+ L: 180,
301
+ S: this
302
+ });
303
+ return {
304
+ token: result.newState,
305
+ createdIds: result.created,
306
+ updatedIds,
307
+ hasMoreDelta: result.hasMoreChanges
308
+ };
309
+ }), Effect.catchIf((error) => error.type === "cannotCalculateChanges", () => {
310
+ log("jmap sync: state token stale, falling back to window scan", void 0, {
311
+ "~LogMeta": "~LogMeta",
312
+ F: __dxlog_file,
313
+ L: 196,
314
+ S: this
315
+ });
316
+ Cursor.clearToken(binding);
317
+ return captureFreshDelta;
318
+ }));
319
+ return {
320
+ buildSource: ({ windows, filter, tagIndex, onEnumerated, onRetrieved }) => {
321
+ const forwardIds = filter ? void 0 : createdIds;
322
+ if (forwardIds) onEnumerated(forwardIds.length);
323
+ return {
324
+ additions: jmapEmails(target, folders, {
325
+ windows,
326
+ filter,
327
+ now,
328
+ onEnumerated,
329
+ onRetrieved,
330
+ forwardIds
331
+ }).pipe(Stream.map(toItem), Stream.provideService(JmapMailApi, providerApi), Stream.mapError(MailSyncError.wrap())),
332
+ reconciles: jmapReconcile(updatedIds, target, folderTagMap, keywordTagMap, tagIndex).pipe(Stream.provideService(JmapMailApi, providerApi), Stream.mapError(MailSyncError.wrap()))
333
+ };
334
+ },
335
+ nextToken: () => capturedToken,
336
+ reconcileForeignIds: updatedIds,
337
+ hasMoreDelta: () => hasMoreDelta
338
+ };
339
+ }).pipe(Effect.provide(context), Effect.mapError(MailSyncError.wrap()))
340
+ };
341
+ }));
342
+ /**
343
+ * Reconcile branch for JMAP: for each `updated` email id, re-fetch its current `mailboxIds` + `keywords`,
344
+ * map them to Tags, and diff against the message's current *local* tags — a remote-wins, snapshot-free
345
+ * reconciliation that is idempotent (a crash re-run produces an empty diff). Emits a retag
346
+ * {@link EmailStage.Change} keyed by the resolved EntityId; ids not in the feed or with no net change are
347
+ * dropped.
348
+ *
349
+ * The two axes differ: **folder** tags are fully remote-wins (added *and* removed, since folder
350
+ * membership is server-authoritative), while **keyword** tags (starred) are **add-only** — the remote can
351
+ * add a star but we never auto-remove one, which would clobber a locally-toggled star until we write
352
+ * local flags back to the provider.
353
+ */
354
+ var jmapReconcile = (updatedIds, target, folderTagMap, keywordTagMap, tagIndex) => {
355
+ const folderProviderUris = new Set(folderTagMap.values());
356
+ return Stream.fromIterable(updatedIds).pipe(Stage.map("jmap-reconcile", (id) => Effect.gen(function* () {
357
+ const { foreignIndex } = yield* Cursor.Service;
358
+ const entityId = foreignIndex?.get(id);
359
+ if (!entityId) return;
360
+ const { list } = yield* (yield* JmapMailApi).emailGet(target, [id]);
361
+ const email = list[0];
362
+ if (!email) return;
363
+ const remoteFolderUris = (email.mailboxIds ? Object.keys(email.mailboxIds) : []).flatMap((folderId) => {
364
+ const uri = folderTagMap.get(folderId);
365
+ return uri ? [uri] : [];
366
+ });
367
+ const remoteKeywordUris = (email.keywords ? Object.entries(email.keywords).filter(([, set]) => set).map(([keyword]) => keyword) : []).flatMap((keyword) => {
368
+ const uri = keywordTagMap.get(keyword);
369
+ return uri ? [uri] : [];
370
+ });
371
+ const localTags = TagIndex.bind(tagIndex).tags(entityId);
372
+ const remoteFolders = new Set(remoteFolderUris);
373
+ const addTagIds = [...remoteFolderUris, ...remoteKeywordUris].filter((tagUri) => !localTags.includes(tagUri));
374
+ const removeTagIds = localTags.filter((tagUri) => folderProviderUris.has(tagUri) && !remoteFolders.has(tagUri));
375
+ if (addTagIds.length === 0 && removeTagIds.length === 0) return;
376
+ return {
377
+ _tag: "retag",
378
+ foreignId: id,
379
+ entityId,
380
+ addTagIds,
381
+ removeTagIds
382
+ };
383
+ })));
384
+ };
385
+ /**
386
+ * Downloads each attachment's bytes via `JmapMailApi.downloadBlob`. One failed download (including a
387
+ * session with no `downloadUrl`) is logged and dropped rather than failing the whole message.
388
+ */
389
+ var fetchAttachments = (target, attachments) => Effect.gen(function* () {
390
+ const api = yield* JmapMailApi;
391
+ return (yield* Effect.forEach(attachments, (attachment) => api.downloadBlob(target, attachment.blobId, {
392
+ name: attachment.name,
393
+ type: attachment.mimeType
394
+ }).pipe(Effect.map((bytes) => ({
395
+ name: attachment.name,
396
+ mimeType: attachment.mimeType,
397
+ size: attachment.size ?? bytes.byteLength,
398
+ bytes,
399
+ contentId: attachment.contentId
400
+ })), Effect.catchAll((error) => {
401
+ log.catch(error, {
402
+ blobId: attachment.blobId,
403
+ name: attachment.name
404
+ }, {
405
+ "~LogMeta": "~LogMeta",
406
+ F: __dxlog_file,
407
+ L: 332,
408
+ S: this
409
+ });
410
+ return Effect.succeed(void 0);
411
+ })), { concurrency: JMAP_SYNC_CONFIG.fetchConcurrency })).filter((attachment) => attachment !== void 0);
412
+ });
413
+ /**
414
+ * Streams JMAP email ids over a {@link Cursor.Window}: build the query filter (folder scope + date
415
+ * bounds + optional user DSL), then paginate. Forward pages oldest-first from `max` so a capped run
416
+ * advances `max` gap-free instead of jumping to the newest key and stranding the middle; backward pages
417
+ * newest-first from `min` to the horizon. Backward's upper bound is queried 1ms past `min` so a message
418
+ * sharing that exact millisecond is re-queried (and deduped) rather than skipped once `min` passes it.
419
+ * Split from the full-email fetch so a bidirectional run can cap the combined id stream before fetching.
420
+ */
421
+ var jmapIds = (target, folders, window, options) => Stream.unwrap(Effect.gen(function* () {
422
+ const api = yield* JmapMailApi;
423
+ const userFilter = options.filter ? parseMailQuery(options.filter, {
424
+ now: options.now,
425
+ resolveMailbox: (nameOrRole) => resolveMailboxByNameOrRole(folders, nameOrRole)
426
+ }) : Option.none();
427
+ const scopesMailbox = Option.match(userFilter, {
428
+ onNone: () => false,
429
+ onSome: filterScopesMailbox
430
+ });
431
+ const excludedFolderIds = folders.filter((folder) => folder.role === "trash" || folder.role === "junk" || folder.role === "drafts").map((folder) => folder.id);
432
+ const conditions = [];
433
+ if (!scopesMailbox && excludedFolderIds.length > 0) conditions.push({ inMailboxOtherThan: excludedFolderIds });
434
+ const upperBound = window.direction === "backward" ? new Date(window.end.getTime() + 1) : window.end;
435
+ conditions.push({ after: window.start.toISOString() });
436
+ conditions.push({ before: upperBound.toISOString() });
437
+ if (Option.isSome(userFilter)) conditions.push(userFilter.value);
438
+ const filter = conditions.length === 1 ? conditions[0] : {
439
+ operator: "AND",
440
+ conditions
441
+ };
442
+ log("starting jmap sync", {
443
+ direction: window.direction,
444
+ start: window.start.toISOString(),
445
+ end: window.end.toISOString(),
446
+ conditions: conditions.length
447
+ }, {
448
+ "~LogMeta": "~LogMeta",
449
+ F: __dxlog_file,
450
+ L: 391,
451
+ S: this
452
+ });
453
+ return Stream.paginateChunkEffect(0, (position) => Effect.gen(function* () {
454
+ const { ids } = yield* api.emailQuery(target, {
455
+ filter,
456
+ sort: [{
457
+ property: "receivedAt",
458
+ isAscending: window.direction === "forward"
459
+ }],
460
+ position,
461
+ limit: JMAP_SYNC_CONFIG.listPageSize
462
+ });
463
+ log("jmap sync: queried page", {
464
+ position,
465
+ total: ids.length
466
+ }, {
467
+ "~LogMeta": "~LogMeta",
468
+ F: __dxlog_file,
469
+ L: 406,
470
+ S: this
471
+ });
472
+ options.onEnumerated?.(ids.length);
473
+ const next = ids.length < JMAP_SYNC_CONFIG.listPageSize ? Option.none() : Option.some(position + ids.length);
474
+ return [Chunk.fromIterable(ids), next];
475
+ }));
476
+ }));
477
+ /**
478
+ * Fetches the full JMAP email for each id. Takes a plain id stream (not a window) so a bidirectional
479
+ * run can cap the combined stream once, then fetch full emails for exactly the capped set.
480
+ */
481
+ var jmapEmailsForIds = (target, ids, options = {}) => ids.pipe(Stream.flatMap((id) => Stream.fromEffect(Effect.gen(function* () {
482
+ const { list } = yield* (yield* JmapMailApi).emailGet(target, [id]);
483
+ options.onRetrieved?.();
484
+ return list[0];
485
+ })).pipe(Stream.filter(Predicate.isNotNullable)), {
486
+ concurrency: JMAP_SYNC_CONFIG.fetchConcurrency,
487
+ bufferSize: 10
488
+ }));
489
+ /**
490
+ * Streams full JMAP emails for one run: the forward source's ids then the backward window's,
491
+ * concatenated and fetched in full. Intentionally UNBOUNDED — the harness caps after dedup (see
492
+ * `runMailSync`). The forward source is either the `max`-anchored forward window (first tick / stale
493
+ * fallback) or, when `forwardIds` is set, the incremental delta's created ids — in which case the
494
+ * backward window still runs, so an incremental tick keeps making backfill progress.
495
+ *
496
+ * `Cursor.skipCommitted` drops ids already in the dedup set before `emailGet`, so re-queried boundary
497
+ * ids aren't downloaded; the harness's post-fetch `Cursor.dedupStage` stays the authority.
498
+ */
499
+ var jmapEmails = (target, folders, config) => {
500
+ const idsFor = (window) => window ? jmapIds(target, folders, window, {
501
+ filter: config.filter,
502
+ now: config.now,
503
+ onEnumerated: config.onEnumerated
504
+ }) : Stream.empty;
505
+ const forward = config.forwardIds ? Stream.fromIterable(config.forwardIds) : idsFor(config.windows.forward);
506
+ return jmapEmailsForIds(target, Stream.concat(forward, idsFor(config.windows.backward)).pipe(Cursor.skipCommitted("skip-committed", (id) => id)), { onRetrieved: config.onRetrieved });
507
+ };
508
+ //#endregion
509
+ export { jmapMailSyncProvider as t };
510
+
511
+ //# sourceMappingURL=chunk-sync-provider.mjs.map