@jini-ai/platform 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/CHANGELOG.md +146 -0
  2. package/README.md +121 -2
  3. package/dist/asset-cache.d.ts +0 -22
  4. package/dist/asset-cache.js +3 -138
  5. package/dist/download.js +2 -2
  6. package/dist/fs/atomic-write.d.ts +103 -0
  7. package/dist/fs/atomic-write.js +201 -0
  8. package/dist/fs/durable-json.d.ts +89 -0
  9. package/dist/fs/durable-json.js +166 -0
  10. package/dist/fs/env-text.d.ts +11 -0
  11. package/dist/fs/env-text.js +22 -0
  12. package/dist/fs/file-lock.d.ts +126 -0
  13. package/dist/fs/file-lock.js +306 -0
  14. package/dist/fs/guarded-reader.d.ts +60 -0
  15. package/dist/fs/guarded-reader.js +310 -0
  16. package/dist/fs/index.d.ts +8 -0
  17. package/dist/fs/index.js +9 -0
  18. package/dist/fs/strict-containment.d.ts +5 -0
  19. package/dist/fs/strict-containment.js +13 -0
  20. package/dist/fs.js +2 -3
  21. package/dist/http/guarded/client.d.ts +13 -0
  22. package/dist/http/guarded/client.js +240 -0
  23. package/dist/http/guarded/errors.d.ts +21 -0
  24. package/dist/http/guarded/errors.js +20 -0
  25. package/dist/http/guarded/fetch-adapter.d.ts +23 -0
  26. package/dist/http/guarded/fetch-adapter.js +30 -0
  27. package/dist/http/guarded/index.d.ts +12 -0
  28. package/dist/http/guarded/index.js +7 -0
  29. package/dist/http/guarded/node-ports.d.ts +8 -0
  30. package/dist/http/guarded/node-ports.js +11 -0
  31. package/dist/http/guarded/ports.d.ts +51 -0
  32. package/dist/http/guarded/ports.js +2 -0
  33. package/dist/http/guarded/transport.fetch.d.ts +11 -0
  34. package/dist/http/guarded/transport.fetch.js +109 -0
  35. package/dist/http/guarded/types.d.ts +8 -0
  36. package/dist/http/guarded/types.js +2 -0
  37. package/dist/index.d.ts +61 -1
  38. package/dist/index.js +61 -1
  39. package/dist/mail/adapter-module.d.ts +20 -0
  40. package/dist/mail/adapter-module.js +2 -0
  41. package/dist/mail/delivery-availability.d.ts +7 -0
  42. package/dist/mail/delivery-availability.js +6 -0
  43. package/dist/mail/index.d.ts +7 -0
  44. package/dist/mail/index.js +3 -0
  45. package/dist/mail/ports.d.ts +43 -0
  46. package/dist/mail/ports.js +2 -0
  47. package/dist/mail/purpose-scoped-mailer.d.ts +18 -0
  48. package/dist/mail/purpose-scoped-mailer.js +50 -0
  49. package/dist/mail/smtp.d.ts +79 -0
  50. package/dist/mail/smtp.js +102 -0
  51. package/dist/mail/types.d.ts +83 -0
  52. package/dist/mail/types.js +2 -0
  53. package/dist/messages.d.ts +22 -0
  54. package/dist/messages.js +15 -0
  55. package/dist/net/address.d.ts +38 -0
  56. package/dist/net/address.js +241 -0
  57. package/dist/net/index.d.ts +2 -0
  58. package/dist/net/index.js +2 -0
  59. package/dist/process.d.ts +2 -1
  60. package/dist/process.js +2 -1
  61. package/dist/proxy-env.js +1 -1
  62. package/dist/resource-paths.js +1 -1
  63. package/dist/sandbox-env.d.ts +1 -1
  64. package/dist/sandbox-env.js +1 -1
  65. package/dist/secrets/aad.d.ts +7 -0
  66. package/dist/secrets/aad.js +5 -0
  67. package/dist/secrets/credential-sets/aad.d.ts +7 -0
  68. package/dist/secrets/credential-sets/aad.js +7 -0
  69. package/dist/secrets/credential-sets/index.d.ts +4 -0
  70. package/dist/secrets/credential-sets/index.js +3 -0
  71. package/dist/secrets/credential-sets/repo.memory.d.ts +36 -0
  72. package/dist/secrets/credential-sets/repo.memory.js +72 -0
  73. package/dist/secrets/credential-sets/types.d.ts +67 -0
  74. package/dist/secrets/credential-sets/types.js +6 -0
  75. package/dist/secrets/index.d.ts +9 -0
  76. package/dist/secrets/index.js +5 -0
  77. package/dist/secrets/keyring.env.d.ts +176 -0
  78. package/dist/secrets/keyring.env.js +271 -0
  79. package/dist/secrets/keyring.memory.d.ts +10 -0
  80. package/dist/secrets/keyring.memory.js +9 -0
  81. package/dist/secrets/ports.d.ts +33 -0
  82. package/dist/secrets/ports.js +2 -0
  83. package/dist/secrets/secret-sealer.aesgcm.d.ts +22 -0
  84. package/dist/secrets/secret-sealer.aesgcm.js +64 -0
  85. package/dist/secrets/testing.d.ts +4 -0
  86. package/dist/secrets/testing.js +4 -0
  87. package/dist/secrets/types.d.ts +10 -0
  88. package/dist/secrets/types.js +2 -0
  89. package/dist/shell.d.ts +10 -6
  90. package/dist/shell.js +10 -6
  91. package/dist/terminal.d.ts +2 -2
  92. package/dist/terminal.js +9 -5
  93. package/package.json +79 -4
  94. package/dist/asset-cache.d.ts.map +0 -1
  95. package/dist/asset-cache.js.map +0 -1
  96. package/dist/aws-sigv4.d.ts.map +0 -1
  97. package/dist/aws-sigv4.js.map +0 -1
  98. package/dist/blob-storage.d.ts.map +0 -1
  99. package/dist/blob-storage.js.map +0 -1
  100. package/dist/command.d.ts.map +0 -1
  101. package/dist/command.js.map +0 -1
  102. package/dist/download.d.ts.map +0 -1
  103. package/dist/download.js.map +0 -1
  104. package/dist/fetch-with-timeout.d.ts.map +0 -1
  105. package/dist/fetch-with-timeout.js.map +0 -1
  106. package/dist/fs.d.ts.map +0 -1
  107. package/dist/fs.js.map +0 -1
  108. package/dist/home-expansion.d.ts.map +0 -1
  109. package/dist/home-expansion.js.map +0 -1
  110. package/dist/http.d.ts.map +0 -1
  111. package/dist/http.js.map +0 -1
  112. package/dist/index.d.ts.map +0 -1
  113. package/dist/index.js.map +0 -1
  114. package/dist/process.d.ts.map +0 -1
  115. package/dist/process.js.map +0 -1
  116. package/dist/proxy-env.d.ts.map +0 -1
  117. package/dist/proxy-env.js.map +0 -1
  118. package/dist/resource-paths.d.ts.map +0 -1
  119. package/dist/resource-paths.js.map +0 -1
  120. package/dist/sandbox-env.d.ts.map +0 -1
  121. package/dist/sandbox-env.js.map +0 -1
  122. package/dist/shell.d.ts.map +0 -1
  123. package/dist/shell.js.map +0 -1
  124. package/dist/terminal.d.ts.map +0 -1
  125. package/dist/terminal.js.map +0 -1
  126. package/dist/toolchain.d.ts.map +0 -1
  127. package/dist/toolchain.js.map +0 -1
package/dist/index.d.ts CHANGED
@@ -28,6 +28,57 @@
28
28
  *
29
29
  * The set of names exported here is intentionally identical to the pre-split
30
30
  * public surface; importers see no change.
31
+
32
+ * Archived provenance rationale:
33
+ * ## Design decision: `project-storage.ts` was split, not ported wholesale or dropped
34
+ *
35
+ * The task brief asked to verify the recon's "leans OD" flag on
36
+ * `project-storage.ts` firsthand and either drop it or extract a generic core
37
+ * with OD's project-specific parts as an adapter, "depending on what you
38
+ * actually find." Read in full: the `ProjectStorage` interface,
39
+ * `LocalProjectStorage`, and `S3ProjectStorage` classes carry no OD nouns in
40
+ * their own logic — they're a backend-agnostic blob CRUD port (read/write/
41
+ * list/delete/stat under a scoping key) plus a local-disk and an
42
+ * S3-compatible implementation, both traversal-guarded. The **only** OD
43
+ * coupling in the file is the bottom `resolveProjectStorage()` factory
44
+ * function, which reads `OD_PROJECT_STORAGE`/`OD_S3_BUCKET`/`OD_S3_REGION`/
45
+ * `OD_S3_PREFIX`/`OD_S3_ENDPOINT`/`OD_S3_ACCESS_KEY_ID`/
46
+ * `OD_S3_SECRET_ACCESS_KEY`/`OD_S3_SESSION_TOKEN` directly from `process.env`.
47
+ *
48
+ * **Extracted, not dropped:** the interface + both implementations, ported to
49
+ * `src/blob-storage.ts` as `BlobStorage`/`BlobFileMeta`/`LocalBlobStorage`/
50
+ * `S3BlobStorage`/`S3BlobStorageOptions`/`StorageError`, with every
51
+ * `projectId: string` parameter renamed to `namespace: string` — per
52
+ * extraction-plan.md's Task-4 Port-2 finding ("OD's 'project' is a product
53
+ * model; engine needs a generic workspace/session store interface") and §2.1's
54
+ * "Runs key on an opaque `contextRef`, never `projectId`" convention, "project"
55
+ * specifically is flagged elsewhere in this porting effort as OD's noun, not
56
+ * the engine's — `namespace` carries no domain meaning here beyond "a
57
+ * top-level grouping/scoping key," which is what the parameter actually is
58
+ * structurally (nothing in `LocalBlobStorage`/`S3BlobStorage`'s logic assumes
59
+ * it means a design project).
60
+ *
61
+ * **Dropped, not ported:** `resolveProjectStorage()`. This is OD adapter
62
+ * wiring — a specific env-var-naming convention and the choice to read
63
+ * `process.env` directly inside the engine layer — not generic engine
64
+ * behavior. It has no equivalent anywhere else in this porting session's
65
+ * established pattern (`createSqliteEventLog`, `LocalBlobStorage`,
66
+ * `S3BlobStorage` are all called with explicit constructor arguments; none of
67
+ * `@jini/*`'s existing adapters read `process.env` inside their own
68
+ * constructors). A Jini host application composes its own equivalent
69
+ * (`new LocalBlobStorage(root)` or `new S3BlobStorage({ bucket, region,
70
+ * credentials, ... })` called directly, with whatever env-var convention that
71
+ * host prefers) rather than inheriting OD's `OD_S3_*` names or its
72
+ * env-selection shape.
73
+ *
74
+ * Landed in `@jini/platform`, not `@jini/sqlite`: this is a filesystem/network
75
+ * blob-storage primitive with no SQL involvement, parallel to this package's
76
+ * existing `fs.ts` (path containment / atomic copy) and `http.ts` (readiness
77
+ * polling) roles — `@jini/sqlite` is reserved for the `EventLog` port adapter
78
+ * and `better-sqlite3`-specific helpers (`db-inspect.ts`,
79
+ * `backend-config.ts`), none of which this needs. `aws-sigv4.ts` lives
80
+ * alongside it in the same package since it's `S3BlobStorage`'s only
81
+ * dependency and has no other consumer yet.
31
82
  */
32
83
  export type { CommandInvocation, CommandInvocationRequest } from "./command.js";
33
84
  export { createCommandInvocation, createPackageManagerInvocation } from "./command.js";
@@ -46,7 +97,7 @@ export { wellKnownUserToolchainBins } from "./toolchain.js";
46
97
  export type { BufferedCommandResult } from "./shell.js";
47
98
  export { execCommandViaLoginShell, execFileBuffered } from "./shell.js";
48
99
  export type { AssetCache, AssetCacheOptions, AssetCacheResult } from "./asset-cache.js";
49
- export { AssetCacheError, assertSafePublicUrl, assetCacheKey, assetCacheRewriteUrl, createAssetCache, createValidatingLookup, isCacheableExternalUrl, isPrivateAddress, } from "./asset-cache.js";
100
+ export { AssetCacheError, assertSafePublicUrl, assetCacheKey, assetCacheRewriteUrl, createAssetCache, createValidatingLookup, isCacheableExternalUrl, } from "./asset-cache.js";
50
101
  export { expandHomePrefix, resolveProjectRelativePath } from "./home-expansion.js";
51
102
  export type { SandboxEnvConfig, SandboxRuntimeConfig, SandboxRuntimeRoots } from "./sandbox-env.js";
52
103
  export { applySandboxRuntimeEnv, ensureSandboxRuntimeDirs, isSandboxImportedProjectRootAllowed, isSandboxModeEnabled, resolveSandboxRuntimeConfig, resolveSandboxRuntimeConfigFromEnv, sandboxAgentProfilesConfigPath, sandboxImportAllowedRoots, sandboxImportedProjectRootUnavailableReason, } from "./sandbox-env.js";
@@ -60,4 +111,13 @@ export type { SigV4Credentials, SignSigV4Input, SignSigV4Result } from "./aws-si
60
111
  export { encodeS3PathSegment, signSigV4 } from "./aws-sigv4.js";
61
112
  export type { BlobFileMeta, BlobStorage, S3BlobStorageOptions } from "./blob-storage.js";
62
113
  export { LocalBlobStorage, S3BlobStorage, StorageError } from "./blob-storage.js";
114
+ export * as filesystem from "./fs/index.js";
115
+ export * as guardedHttp from "./http/guarded/index.js";
116
+ export * as mail from "./mail/index.js";
117
+ export * as smtp from "./mail/smtp.js";
118
+ export * as secrets from "./secrets/index.js";
119
+ export * as credentialSets from "./secrets/credential-sets/index.js";
120
+ export { isPrivateAddress } from "./net/address.js";
121
+ export { defaultPlatformMessages } from "./messages.js";
122
+ export type { PlatformMessages } from "./messages.js";
63
123
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -28,6 +28,57 @@
28
28
  *
29
29
  * The set of names exported here is intentionally identical to the pre-split
30
30
  * public surface; importers see no change.
31
+
32
+ * Archived provenance rationale:
33
+ * ## Design decision: `project-storage.ts` was split, not ported wholesale or dropped
34
+ *
35
+ * The task brief asked to verify the recon's "leans OD" flag on
36
+ * `project-storage.ts` firsthand and either drop it or extract a generic core
37
+ * with OD's project-specific parts as an adapter, "depending on what you
38
+ * actually find." Read in full: the `ProjectStorage` interface,
39
+ * `LocalProjectStorage`, and `S3ProjectStorage` classes carry no OD nouns in
40
+ * their own logic — they're a backend-agnostic blob CRUD port (read/write/
41
+ * list/delete/stat under a scoping key) plus a local-disk and an
42
+ * S3-compatible implementation, both traversal-guarded. The **only** OD
43
+ * coupling in the file is the bottom `resolveProjectStorage()` factory
44
+ * function, which reads `OD_PROJECT_STORAGE`/`OD_S3_BUCKET`/`OD_S3_REGION`/
45
+ * `OD_S3_PREFIX`/`OD_S3_ENDPOINT`/`OD_S3_ACCESS_KEY_ID`/
46
+ * `OD_S3_SECRET_ACCESS_KEY`/`OD_S3_SESSION_TOKEN` directly from `process.env`.
47
+ *
48
+ * **Extracted, not dropped:** the interface + both implementations, ported to
49
+ * `src/blob-storage.ts` as `BlobStorage`/`BlobFileMeta`/`LocalBlobStorage`/
50
+ * `S3BlobStorage`/`S3BlobStorageOptions`/`StorageError`, with every
51
+ * `projectId: string` parameter renamed to `namespace: string` — per
52
+ * extraction-plan.md's Task-4 Port-2 finding ("OD's 'project' is a product
53
+ * model; engine needs a generic workspace/session store interface") and §2.1's
54
+ * "Runs key on an opaque `contextRef`, never `projectId`" convention, "project"
55
+ * specifically is flagged elsewhere in this porting effort as OD's noun, not
56
+ * the engine's — `namespace` carries no domain meaning here beyond "a
57
+ * top-level grouping/scoping key," which is what the parameter actually is
58
+ * structurally (nothing in `LocalBlobStorage`/`S3BlobStorage`'s logic assumes
59
+ * it means a design project).
60
+ *
61
+ * **Dropped, not ported:** `resolveProjectStorage()`. This is OD adapter
62
+ * wiring — a specific env-var-naming convention and the choice to read
63
+ * `process.env` directly inside the engine layer — not generic engine
64
+ * behavior. It has no equivalent anywhere else in this porting session's
65
+ * established pattern (`createSqliteEventLog`, `LocalBlobStorage`,
66
+ * `S3BlobStorage` are all called with explicit constructor arguments; none of
67
+ * `@jini/*`'s existing adapters read `process.env` inside their own
68
+ * constructors). A Jini host application composes its own equivalent
69
+ * (`new LocalBlobStorage(root)` or `new S3BlobStorage({ bucket, region,
70
+ * credentials, ... })` called directly, with whatever env-var convention that
71
+ * host prefers) rather than inheriting OD's `OD_S3_*` names or its
72
+ * env-selection shape.
73
+ *
74
+ * Landed in `@jini/platform`, not `@jini/sqlite`: this is a filesystem/network
75
+ * blob-storage primitive with no SQL involvement, parallel to this package's
76
+ * existing `fs.ts` (path containment / atomic copy) and `http.ts` (readiness
77
+ * polling) roles — `@jini/sqlite` is reserved for the `EventLog` port adapter
78
+ * and `better-sqlite3`-specific helpers (`db-inspect.ts`,
79
+ * `backend-config.ts`), none of which this needs. `aws-sigv4.ts` lives
80
+ * alongside it in the same package since it's `S3BlobStorage`'s only
81
+ * dependency and has no other consumer yet.
31
82
  */
32
83
  export { createCommandInvocation, createPackageManagerInvocation } from "./command.js";
33
84
  export { mergeProxyAwareEnv, parseMacosScutilProxyOutput, parseWindowsInternetSettingsProxyOutput, resolveSystemProxyEnv, } from "./proxy-env.js";
@@ -37,7 +88,7 @@ export { waitForHttpOk } from "./http.js";
37
88
  export { FETCH_TIMEOUT_MS, FetchTimeoutError, fetchWithTimeout } from "./fetch-with-timeout.js";
38
89
  export { wellKnownUserToolchainBins } from "./toolchain.js";
39
90
  export { execCommandViaLoginShell, execFileBuffered } from "./shell.js";
40
- export { AssetCacheError, assertSafePublicUrl, assetCacheKey, assetCacheRewriteUrl, createAssetCache, createValidatingLookup, isCacheableExternalUrl, isPrivateAddress, } from "./asset-cache.js";
91
+ export { AssetCacheError, assertSafePublicUrl, assetCacheKey, assetCacheRewriteUrl, createAssetCache, createValidatingLookup, isCacheableExternalUrl, } from "./asset-cache.js";
41
92
  export { expandHomePrefix, resolveProjectRelativePath } from "./home-expansion.js";
42
93
  export { applySandboxRuntimeEnv, ensureSandboxRuntimeDirs, isSandboxImportedProjectRootAllowed, isSandboxModeEnabled, resolveSandboxRuntimeConfig, resolveSandboxRuntimeConfigFromEnv, sandboxAgentProfilesConfigPath, sandboxImportAllowedRoots, sandboxImportedProjectRootUnavailableReason, } from "./sandbox-env.js";
43
94
  export { resolveDaemonCliPath, resolveDaemonPluginPreviewsDir, resolveDaemonResourceDir, resolveDaemonResourceRoot, resolveDataDir, resolveProcessResourcesPath, } from "./resource-paths.js";
@@ -45,4 +96,13 @@ export { createTerminalService, ensureSpawnHelperExecutable, resolveShell, spawn
45
96
  export { MANAGED_DOWNLOAD_ERROR_CODES, ManagedDownloadError, downloadCopyAndClear, inspectManagedDownload, managedDownload, pruneManagedDownloads, removeManagedDownload, } from "./download.js";
46
97
  export { encodeS3PathSegment, signSigV4 } from "./aws-sigv4.js";
47
98
  export { LocalBlobStorage, S3BlobStorage, StorageError } from "./blob-storage.js";
99
+ // Cohesive subpaths remain available as namespaced APIs without colliding with legacy names.
100
+ export * as filesystem from "./fs/index.js";
101
+ export * as guardedHttp from "./http/guarded/index.js";
102
+ export * as mail from "./mail/index.js";
103
+ export * as smtp from "./mail/smtp.js";
104
+ export * as secrets from "./secrets/index.js";
105
+ export * as credentialSets from "./secrets/credential-sets/index.js";
106
+ export { isPrivateAddress } from "./net/address.js";
107
+ export { defaultPlatformMessages } from "./messages.js";
48
108
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,20 @@
1
+ import type { HttpClientPort } from "@jini-ai/core/primitives";
2
+ import type { MailerPort } from "./ports.js";
3
+ export interface MailAdapterCredential {
4
+ readonly token: string;
5
+ readonly baseUrl?: string;
6
+ readonly username?: string;
7
+ }
8
+ export interface MailAdapterKit {
9
+ /** Provider requests must use the guarded outbound HTTP seam so its egress policy applies. */
10
+ readonly httpClient: HttpClientPort;
11
+ }
12
+ export interface MailAdapterCreateContext {
13
+ readonly credential: MailAdapterCredential;
14
+ readonly kit: MailAdapterKit;
15
+ }
16
+ /** Provider modules return the ordinary mail port; the shared contract names no vendor. */
17
+ export interface MailAdapterModule {
18
+ create(context: MailAdapterCreateContext): MailerPort;
19
+ }
20
+ //# sourceMappingURL=adapter-module.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=adapter-module.js.map
@@ -0,0 +1,7 @@
1
+ import type { MailerPort } from "./ports.js";
2
+ /** Reads adapter capabilities without sending a message. */
3
+ /** The console driver only logs messages locally, so it must never count as real delivery. */
4
+ export declare function isMailDeliveryAvailable({ mailer }: {
5
+ mailer: MailerPort;
6
+ }): boolean;
7
+ //# sourceMappingURL=delivery-availability.d.ts.map
@@ -0,0 +1,6 @@
1
+ /** Reads adapter capabilities without sending a message. */
2
+ /** The console driver only logs messages locally, so it must never count as real delivery. */
3
+ export function isMailDeliveryAvailable({ mailer }) {
4
+ return mailer.capabilities({}).driver !== "console";
5
+ }
6
+ //# sourceMappingURL=delivery-availability.js.map
@@ -0,0 +1,7 @@
1
+ export type * from "./types.js";
2
+ export type * from "./ports.js";
3
+ export type * from "./adapter-module.js";
4
+ export { wrapMailerWithPurposeGate } from "./purpose-scoped-mailer.js";
5
+ export type { WrapMailerWithPurposeGateOptions } from "./purpose-scoped-mailer.js";
6
+ export { isMailDeliveryAvailable } from "./delivery-availability.js";
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,3 @@
1
+ export { wrapMailerWithPurposeGate } from "./purpose-scoped-mailer.js";
2
+ export { isMailDeliveryAvailable } from "./delivery-availability.js";
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,43 @@
1
+ import type { UUID } from "@jini-ai/core/primitives";
2
+ import type { MailerCapabilities, MailerSendResult, MailerSendRequired, MailerBatchRequired, MailerOptional } from "./types.js";
3
+ export type { MailerCapabilities, MailerSendOptions, MailerSendResult, OutboundEmail } from "./types.js";
4
+ export interface MailerPort {
5
+ capabilities(_required: Record<string, never>): MailerCapabilities;
6
+ send(required: MailerSendRequired, optional?: MailerOptional): Promise<MailerSendResult>;
7
+ sendBatch(required: MailerBatchRequired, optional?: MailerOptional): Promise<readonly MailerSendResult[]>;
8
+ }
9
+ export type ConsoleMailerAdapter = MailerPort;
10
+ export type InMemoryMailerAdapter = MailerPort;
11
+ export interface MailSuppressionRepoPort {
12
+ isSuppressed(required: {
13
+ workspaceId: UUID;
14
+ normalizedAddress: string;
15
+ scope: "global" | {
16
+ module: string;
17
+ };
18
+ }): Promise<boolean>;
19
+ suppress(required: {
20
+ workspaceId: UUID;
21
+ normalizedAddress: string;
22
+ scope: "global" | {
23
+ module: string;
24
+ };
25
+ reason: "bounced" | "complained" | "manual";
26
+ }): Promise<void>;
27
+ }
28
+ export interface MailSendDedupRepoPort {
29
+ wasSent(required: {
30
+ workspaceId: UUID;
31
+ idempotencyKey: string;
32
+ }): Promise<boolean>;
33
+ markSent(required: {
34
+ workspaceId: UUID;
35
+ idempotencyKey: string;
36
+ }): Promise<void>;
37
+ }
38
+ export interface DeliveryReadinessPolicy {
39
+ isReady(required: {
40
+ capabilityName: string;
41
+ }): boolean;
42
+ }
43
+ //# sourceMappingURL=ports.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ports.js.map
@@ -0,0 +1,18 @@
1
+ /** Unknown and absent lanes require a durable notification path in production.
2
+ * Local mode and explicit interactive sends proceed without that gate.
3
+ */
4
+ import type { MailerPort, DeliveryReadinessPolicy } from "./ports.js";
5
+ type RuntimeMode = "production" | "local";
6
+ export interface WrapMailerWithPurposeGateOptions {
7
+ inner: MailerPort;
8
+ mode: RuntimeMode;
9
+ readiness: DeliveryReadinessPolicy;
10
+ }
11
+ /** Validates the mode at wrap time and gates notification sends/batches before delivery.
12
+ * Unknown modes throw TypeError; only explicit local mode bypasses durable readiness.
13
+ * @complexity O(1) policy work per send/batch, before delegating delivery.
14
+ * @example wrapMailerWithPurposeGate({ inner, mode: "production", readiness })
15
+ */
16
+ export declare function wrapMailerWithPurposeGate(options: WrapMailerWithPurposeGateOptions): MailerPort;
17
+ export {};
18
+ //# sourceMappingURL=purpose-scoped-mailer.d.ts.map
@@ -0,0 +1,50 @@
1
+ /** Unknown and absent lanes require a durable notification path in production.
2
+ * Local mode and explicit interactive sends proceed without that gate.
3
+ */
4
+ // Allowlist the one interactive value: undefined, unknown values and type-bypassing casts
5
+ // belong to the restrictive notification lane, so a new lane cannot accidentally bypass readiness.
6
+ function resolveLane(options) {
7
+ return options.lane === "interactive" ? "interactive" : "notification";
8
+ }
9
+ function refusalError(capabilityName) {
10
+ return new Error(`MAILER_SEND_REFUSED_NO_DURABLE_PATH: notification-lane mailer send refused for capability "${capabilityName}" — no durable outbox path registered`);
11
+ }
12
+ // Decorate the existing port rather than inventing a second delivery contract. The caller resolves
13
+ // mode once at composition; never re-read the environment per send and change policy mid-process.
14
+ /** Validates the mode at wrap time and gates notification sends/batches before delivery.
15
+ * Unknown modes throw TypeError; only explicit local mode bypasses durable readiness.
16
+ * @complexity O(1) policy work per send/batch, before delegating delivery.
17
+ * @example wrapMailerWithPurposeGate({ inner, mode: "production", readiness })
18
+ */
19
+ export function wrapMailerWithPurposeGate(options) {
20
+ const { inner, mode, readiness } = options;
21
+ // Validate once at composition: typos must never silently disable durable delivery.
22
+ if (mode !== "production" && mode !== "local") {
23
+ throw new TypeError('mailer mode must be "production" or "local"');
24
+ }
25
+ function checkGate(sendOptions) {
26
+ if (mode === "local")
27
+ return; // Local mode never refuses delivery.
28
+ const lane = resolveLane(sendOptions);
29
+ if (lane !== "notification")
30
+ return;
31
+ const capabilityName = sendOptions.sourceContext?.module ?? "unknown";
32
+ if (!readiness.isReady({ capabilityName })) {
33
+ throw refusalError(capabilityName);
34
+ }
35
+ }
36
+ return {
37
+ capabilities: (required) => inner.capabilities(required),
38
+ async send({ message, ...required }, optional = {}) {
39
+ const sendOptions = { ...required, ...optional };
40
+ checkGate(sendOptions);
41
+ return inner.send({ message, ...required }, optional);
42
+ },
43
+ async sendBatch({ messages, ...required }, optional = {}) {
44
+ const sendOptions = { ...required, ...optional };
45
+ checkGate(sendOptions);
46
+ return inner.sendBatch({ messages, ...required }, optional);
47
+ },
48
+ };
49
+ }
50
+ //# sourceMappingURL=purpose-scoped-mailer.js.map
@@ -0,0 +1,79 @@
1
+ /** SMTP adapter over an injected transport and clock; no SMTP vendor is loaded by core. */
2
+ import { type Clock as ClockPort } from "@jini-ai/core/primitives";
3
+ import type { MailerPort } from "./ports.js";
4
+ import type { MailerCapabilities, MailerSendResult, MailerSendRequired, MailerBatchRequired, MailerOptional } from "./types.js";
5
+ export interface SmtpTransport {
6
+ sendMail(required: {
7
+ mail: SmtpMailPayload;
8
+ }): Promise<{
9
+ messageId: string;
10
+ }>;
11
+ }
12
+ export interface SmtpMailPayload {
13
+ from: {
14
+ name?: string;
15
+ address: string;
16
+ };
17
+ to: {
18
+ name?: string;
19
+ address: string;
20
+ };
21
+ replyTo?: {
22
+ name?: string;
23
+ address: string;
24
+ };
25
+ subject: string;
26
+ html?: string;
27
+ text?: string;
28
+ headers?: Readonly<Record<string, string>>;
29
+ attachments?: {
30
+ filename: string;
31
+ content: string;
32
+ encoding: "base64";
33
+ contentType: string;
34
+ }[];
35
+ }
36
+ export declare class SmtpMailerAdapter implements MailerPort {
37
+ private readonly clock;
38
+ private readonly transport;
39
+ constructor({ transport, clock }: {
40
+ transport: SmtpTransport;
41
+ clock: ClockPort;
42
+ });
43
+ capabilities(_required: Record<string, never>): MailerCapabilities;
44
+ send({ message }: MailerSendRequired, _optional?: MailerOptional): Promise<MailerSendResult>;
45
+ sendBatch({ messages, ...required }: MailerBatchRequired, optional?: MailerOptional): Promise<readonly MailerSendResult[]>;
46
+ }
47
+ export interface CreateNodemailerSmtpTransportConfig {
48
+ host: string;
49
+ port: number;
50
+ secure: boolean;
51
+ auth: {
52
+ user: string;
53
+ pass: string;
54
+ };
55
+ nodemailer: NodemailerPort;
56
+ }
57
+ export declare function createNodemailerSmtpTransport(config: CreateNodemailerSmtpTransportConfig, { timeoutMs }?: {
58
+ timeoutMs?: number;
59
+ }): SmtpTransport;
60
+ /** A host can pass nodemailer's module structurally; this package never loads or requires it. */
61
+ export interface NodemailerPort {
62
+ createTransport(required: {
63
+ host: string;
64
+ port: number;
65
+ secure: boolean;
66
+ auth: {
67
+ user: string;
68
+ pass: string;
69
+ };
70
+ connectionTimeout?: number;
71
+ }): NativeSmtpTransport;
72
+ }
73
+ /** Vendor-shaped adapter boundary; application mailers use SmtpTransport. */
74
+ export interface NativeSmtpTransport {
75
+ sendMail(mail: SmtpMailPayload): Promise<{
76
+ messageId: string;
77
+ }>;
78
+ }
79
+ //# sourceMappingURL=smtp.d.ts.map
@@ -0,0 +1,102 @@
1
+ /** SMTP adapter over an injected transport and clock; no SMTP vendor is loaded by core. */
2
+ import { nowIso } from "@jini-ai/core/primitives";
3
+ function toNodemailerAddress(address) {
4
+ // Object addresses delegate header-safe display-name quoting/encoding to the SMTP library.
5
+ return address.name ? { name: address.name, address: address.email } : { address: address.email };
6
+ }
7
+ const NON_RETRYABLE_SMTP_CODES = new Set(["EAUTH", "EENVELOPE", "EMESSAGE"]);
8
+ function classifySmtpError(err) {
9
+ // Prefer the server's RFC 5321 reply: 4xx asks for retry, 5xx rejects the message as-is.
10
+ // Pre-response auth/envelope/content rejection is terminal; connection failures are transient.
11
+ // Unknown failures remain retryable to avoid silently dropping mail on an unfamiliar error shape.
12
+ const message = err instanceof Error ? err.message : String(err);
13
+ const responseCode = hasNumericField(err, "responseCode") ? err.responseCode : undefined;
14
+ if (responseCode !== undefined) {
15
+ const retryable = responseCode >= 400 && responseCode < 500;
16
+ return { ok: false, retryable, errorCode: `SMTP_${responseCode}`, message };
17
+ }
18
+ const code = hasStringField(err, "code") ? err.code : undefined;
19
+ if (code !== undefined) {
20
+ return { ok: false, retryable: !NON_RETRYABLE_SMTP_CODES.has(code), errorCode: code, message };
21
+ }
22
+ return { ok: false, retryable: true, errorCode: "SMTP_UNKNOWN_ERROR", message };
23
+ }
24
+ function hasNumericField(value, key) {
25
+ return typeof value === "object" && value !== null && key in value && typeof value[key] === "number";
26
+ }
27
+ function hasStringField(value, key) {
28
+ return typeof value === "object" && value !== null && key in value && typeof value[key] === "string";
29
+ }
30
+ export class SmtpMailerAdapter {
31
+ clock;
32
+ transport;
33
+ constructor({ transport, clock }) {
34
+ // Inject acceptance time so delivery outcomes can be checked deterministically without wall-clock I/O.
35
+ this.transport = transport;
36
+ this.clock = clock;
37
+ }
38
+ capabilities(_required) {
39
+ return {
40
+ driver: "smtp",
41
+ // SMTP has no dedup-key verb; a caller needs its own send ledger to prevent duplicate delivery.
42
+ supportsIdempotencyKey: false,
43
+ // Bounce/complaint feedback requires a separately configured return-path mailbox parser.
44
+ supportsWebhookFeedback: false,
45
+ maxBatchSize: 1,
46
+ supportsAttachments: true,
47
+ };
48
+ }
49
+ async send({ message }, _optional = {}) {
50
+ try {
51
+ const info = await this.transport.sendMail({ mail: {
52
+ from: toNodemailerAddress(message.from),
53
+ to: toNodemailerAddress(message.to),
54
+ ...(message.replyTo ? { replyTo: toNodemailerAddress(message.replyTo) } : {}),
55
+ subject: message.subject,
56
+ ...(message.html !== undefined ? { html: message.html } : {}),
57
+ ...(message.text !== undefined ? { text: message.text } : {}),
58
+ ...(message.headers ? { headers: message.headers } : {}),
59
+ ...(message.attachments && message.attachments.length > 0
60
+ ? {
61
+ attachments: message.attachments.map((attachment) => ({
62
+ filename: attachment.filename,
63
+ content: attachment.contentBase64,
64
+ encoding: "base64",
65
+ contentType: attachment.contentType,
66
+ })),
67
+ }
68
+ : {}),
69
+ } });
70
+ return { ok: true, providerMessageId: info.messageId, acceptedAt: nowIso({ clock: this.clock }) };
71
+ }
72
+ catch (err) {
73
+ return classifySmtpError(err);
74
+ }
75
+ }
76
+ async sendBatch({ messages, ...required }, optional = {}) {
77
+ const results = [];
78
+ // SMTP has no native batch verb; sequential sends preserve one outcome per input message.
79
+ for (const message of messages) {
80
+ results.push(await this.send({ message, ...required }, optional));
81
+ }
82
+ return results;
83
+ }
84
+ }
85
+ export function createNodemailerSmtpTransport(config, { timeoutMs } = {}) {
86
+ // Delegate SMTP negotiation/authentication to the vendor rather than reimplementing the protocol.
87
+ const nodemailer = config.nodemailer;
88
+ const transporter = nodemailer.createTransport({
89
+ host: config.host,
90
+ port: config.port,
91
+ secure: config.secure,
92
+ auth: config.auth,
93
+ ...(timeoutMs === undefined ? {} : { connectionTimeout: timeoutMs }),
94
+ });
95
+ return {
96
+ sendMail: async ({ mail }) => {
97
+ const info = await transporter.sendMail(mail);
98
+ return { messageId: info.messageId };
99
+ },
100
+ };
101
+ }
102
+ //# sourceMappingURL=smtp.js.map
@@ -0,0 +1,83 @@
1
+ import type { ISODateTime, UUID } from "@jini-ai/core/primitives";
2
+ export interface OutboundEmail {
3
+ workspaceId: UUID;
4
+ to: EmailAddress;
5
+ from: EmailAddress;
6
+ replyTo?: EmailAddress;
7
+ subject: string;
8
+ /** At least one of html/text must be supplied; the transport does not render missing content. */
9
+ html?: string;
10
+ /** A plain-text alternative improves accessibility and deliverability. */
11
+ text?: string;
12
+ /** Allows caller-owned List-Unsubscribe and RFC 8058 one-click headers without provider coupling. */
13
+ headers?: Readonly<Record<string, string>>;
14
+ /** Additive/optional for existing consumers; content is base64 so no live stream/path crosses the ABI. */
15
+ attachments?: readonly EmailAttachment[];
16
+ }
17
+ export interface EmailAddress {
18
+ email: string;
19
+ name?: string;
20
+ }
21
+ export interface EmailAttachment {
22
+ filename: string;
23
+ contentType: string;
24
+ contentBase64: string;
25
+ }
26
+ export interface MailerSendOptions {
27
+ /** Redelivery must reuse this identity; generating a new key defeats outbox deduplication. */
28
+ idempotencyKey: string;
29
+ /** Scope suppression and send dedup to the same delivery boundary. */
30
+ workspaceId: UUID;
31
+ /** Preserve provenance so delivery feedback can be attributed to the originating campaign/module. */
32
+ sourceContext: {
33
+ module: string;
34
+ ref?: string;
35
+ };
36
+ timeoutMs?: number;
37
+ purpose?: "transactional" | "bulk" | `feature:${string}`;
38
+ /** Keep interaction separate from notification readiness: a shared transactional purpose cannot
39
+ * distinguish them. Unmapped/absent lanes resolve to notification rather than opening the
40
+ * interactive bypass; the caller-facing facade applies that fail-closed allowlist. */
41
+ lane?: "interactive" | "notification";
42
+ }
43
+ /** Serializable outcomes cross the adapter ABI; live thrown objects cannot be carried as results. */
44
+ export type MailerSendResult = {
45
+ ok: true;
46
+ providerMessageId: string;
47
+ acceptedAt: ISODateTime;
48
+ } | {
49
+ ok: false;
50
+ retryable: boolean;
51
+ errorCode: string;
52
+ message: string;
53
+ };
54
+ export interface MailerCapabilities {
55
+ driver: string;
56
+ supportsIdempotencyKey: boolean;
57
+ supportsWebhookFeedback: boolean;
58
+ /** A limit of one means sequential send fallback, not an optional batch method consumers branch on. */
59
+ maxBatchSize: number;
60
+ /** Required attachments must be refused when unsupported, never dropped while reporting success. */
61
+ supportsAttachments: boolean;
62
+ }
63
+ export interface MailerFeedbackEvent {
64
+ workspaceId: UUID;
65
+ providerMessageId: string;
66
+ sourceContext: {
67
+ module: string;
68
+ ref?: string;
69
+ };
70
+ kind: "delivered" | "bounced" | "complained";
71
+ hardBounce: boolean;
72
+ occurredAt: ISODateTime;
73
+ }
74
+ /** Required delivery identity/provenance belongs in the first argument of every send. */
75
+ export type MailerDelivery = Pick<MailerSendOptions, "idempotencyKey" | "workspaceId" | "sourceContext">;
76
+ export type MailerOptional = Omit<MailerSendOptions, keyof MailerDelivery>;
77
+ export type MailerSendRequired = MailerDelivery & {
78
+ message: OutboundEmail;
79
+ };
80
+ export type MailerBatchRequired = MailerDelivery & {
81
+ messages: readonly OutboundEmail[];
82
+ };
83
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,22 @@
1
+ /** Neutral defaults for shared network, credential and filesystem boundaries.
2
+ * Hosts may replace the whole object; diagnostics retain structured context separately. */
3
+ export interface PlatformMessages {
4
+ atomicModeInvalid(): string;
5
+ atomicTempPathInvalid(): string;
6
+ atomicTempNameInvalid(): string;
7
+ atomicSymlinkRefused(): string;
8
+ atomicPermissionsRefused(): string;
9
+ atomicJsonUnserializable(): string;
10
+ fileLockIntervalsInvalid(): string;
11
+ fileLockWriteNoProgress(): string;
12
+ egressRefused(): string;
13
+ rootKeySealedWarning(required: {
14
+ subject: string;
15
+ }): string;
16
+ fileLockTimeout(required: {
17
+ waitedMs: number;
18
+ }): string;
19
+ fileLockLost(): string;
20
+ }
21
+ export declare const defaultPlatformMessages: PlatformMessages;
22
+ //# sourceMappingURL=messages.d.ts.map
@@ -0,0 +1,15 @@
1
+ export const defaultPlatformMessages = {
2
+ atomicModeInvalid: () => "atomic file mode must be permission bits between 0000 and 0777",
3
+ atomicTempPathInvalid: () => "atomic temp path must be a different same-directory file",
4
+ atomicTempNameInvalid: () => "atomic temp name must be one safe segment",
5
+ atomicSymlinkRefused: () => "atomic write refused a symbolic-link destination",
6
+ atomicPermissionsRefused: () => "atomic write refused non-owner-only permissions",
7
+ atomicJsonUnserializable: () => "atomic JSON data must be serializable",
8
+ fileLockIntervalsInvalid: () => "file lock intervals must be positive safe integers (timeout may be zero; stale may be Infinity)",
9
+ fileLockWriteNoProgress: () => "file lock ownership write made no progress",
10
+ egressRefused: () => "egress to the requested host was refused by the outbound network policy",
11
+ rootKeySealedWarning: ({ subject }) => `IMPORTANT: anything sealed while ${subject} was in place was sealed under key material derived from these same bytes, `,
12
+ fileLockTimeout: ({ waitedMs }) => `exclusive file lock timed out after ${Math.round(waitedMs)}ms`,
13
+ fileLockLost: () => "exclusive file lock lost ownership before the critical section finished",
14
+ };
15
+ //# sourceMappingURL=messages.js.map