@jini-ai/platform 0.6.11 → 0.6.15-rc.1
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.
- package/CHANGELOG.md +40 -0
- package/README.md +52 -0
- package/dist/asset-cache.js +27 -13
- package/dist/command.d.ts +1 -1
- package/dist/command.js +11 -8
- package/dist/download.js +26 -32
- package/dist/fetch-with-timeout.d.ts +54 -0
- package/dist/fetch-with-timeout.js +129 -2
- package/dist/fs/directory-user-config.js +2 -1
- package/dist/fs/directory-workspace.js +3 -1
- package/dist/fs/guarded-reader.d.ts +19 -47
- package/dist/fs/guarded-reader.js +82 -13
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/mail/adapter-module.d.ts +6 -0
- package/dist/mail/delivery-wrapper.d.ts +46 -0
- package/dist/mail/delivery-wrapper.js +109 -0
- package/dist/mail/domain-read.d.ts +21 -0
- package/dist/mail/domain-read.js +17 -0
- package/dist/mail/email-sender-registry.d.ts +15 -0
- package/dist/mail/email-sender-registry.js +22 -0
- package/dist/mail/email-sender.d.ts +109 -0
- package/dist/mail/email-sender.js +10 -0
- package/dist/mail/engine-contracts.d.ts +129 -0
- package/dist/mail/engine-contracts.js +2 -0
- package/dist/mail/index.d.ts +15 -2
- package/dist/mail/index.js +8 -1
- package/dist/mail/mail-status-cache.d.ts +33 -0
- package/dist/mail/mail-status-cache.js +56 -0
- package/dist/mail/prepare-sender-email.d.ts +35 -0
- package/dist/mail/prepare-sender-email.js +68 -0
- package/dist/mail/provider-error.d.ts +18 -0
- package/dist/mail/provider-error.js +24 -0
- package/dist/mail/purpose-scoped-mailer.d.ts +12 -1
- package/dist/mail/purpose-scoped-mailer.js +23 -12
- package/dist/mail/resolve-mailer.d.ts +95 -0
- package/dist/mail/resolve-mailer.js +348 -0
- package/dist/mail/sender-domain-verification.d.ts +40 -0
- package/dist/mail/sender-domain-verification.js +71 -0
- package/dist/mail/senders.d.ts +5 -0
- package/dist/mail/senders.js +5 -0
- package/dist/mail/types.d.ts +19 -0
- package/dist/process.d.ts +27 -2
- package/dist/process.js +85 -2
- package/dist/secrets/credential-sets/vault.repo.memory.d.ts +1 -4
- package/dist/secrets/credential-sets/vault.repo.memory.js +3 -1
- package/dist/secrets/credential-sets/vault.service.d.ts +17 -9
- package/dist/secrets/credential-sets/vault.service.js +18 -9
- package/dist/secrets/credential-sets/vault.types.d.ts +10 -4
- package/dist/secrets/index.d.ts +2 -0
- package/dist/secrets/index.js +1 -0
- package/dist/secrets/passphrase-envelope.d.ts +26 -0
- package/dist/secrets/passphrase-envelope.js +125 -0
- package/package.json +8 -2
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { MailAdapterCredential } from "./adapter-module.js";
|
|
2
|
+
import type { MailDomainState, MailProviderCapabilities, MailProviderRegistry, MailProviderSetting, MailStatus } from "./provider.js";
|
|
3
|
+
/** Metadata only: the host reads these facts before decrypting a credential. */
|
|
4
|
+
export interface MailCredentialFact {
|
|
5
|
+
readonly id: string;
|
|
6
|
+
readonly label: string;
|
|
7
|
+
readonly vaultRevision?: number;
|
|
8
|
+
readonly updatedAt: string;
|
|
9
|
+
}
|
|
10
|
+
/** Credential presence exposed in configuration reports; never a token or username. */
|
|
11
|
+
export interface MailProviderCredentialFacts {
|
|
12
|
+
readonly required: boolean;
|
|
13
|
+
readonly saved: boolean;
|
|
14
|
+
readonly label?: string;
|
|
15
|
+
readonly credentialId?: string;
|
|
16
|
+
readonly updatedAt?: string;
|
|
17
|
+
}
|
|
18
|
+
/** Declarative setup copy belongs to the host or plugin. */
|
|
19
|
+
export interface MailProviderField {
|
|
20
|
+
readonly key: string;
|
|
21
|
+
readonly label: string;
|
|
22
|
+
readonly type: "secret" | "text" | "url" | "email";
|
|
23
|
+
readonly required: boolean;
|
|
24
|
+
readonly defaultValue?: string;
|
|
25
|
+
readonly placeholder?: string;
|
|
26
|
+
readonly help?: string;
|
|
27
|
+
readonly helpUrl?: string;
|
|
28
|
+
}
|
|
29
|
+
export interface MailProviderSetup {
|
|
30
|
+
readonly fields: readonly MailProviderField[];
|
|
31
|
+
readonly helpUrl?: string;
|
|
32
|
+
}
|
|
33
|
+
/** Safe host-supplied projection; configuration building and product copy stay in the host. */
|
|
34
|
+
export interface MailProviderConfiguration {
|
|
35
|
+
readonly pluginId?: string;
|
|
36
|
+
readonly id: string;
|
|
37
|
+
readonly label: string;
|
|
38
|
+
readonly origin: "core" | "plugin";
|
|
39
|
+
readonly hosted: boolean;
|
|
40
|
+
readonly lanes: readonly ("transactional" | "bulk")[];
|
|
41
|
+
readonly capabilities: Pick<MailProviderCapabilities, "feedback" | "domainVerification" | "rateLimit">;
|
|
42
|
+
readonly credential: MailProviderCredentialFacts;
|
|
43
|
+
readonly selectable: boolean;
|
|
44
|
+
readonly refusal?: string;
|
|
45
|
+
readonly setup: MailProviderSetup;
|
|
46
|
+
}
|
|
47
|
+
export interface MailProviderConfigurationReport {
|
|
48
|
+
readonly setting: MailProviderSetting;
|
|
49
|
+
readonly providers: readonly MailProviderConfiguration[];
|
|
50
|
+
readonly refusals: readonly string[];
|
|
51
|
+
}
|
|
52
|
+
/** A provider failure safe for callers; contains neither credentials nor message bodies. */
|
|
53
|
+
export interface MailProviderError {
|
|
54
|
+
readonly at: string;
|
|
55
|
+
readonly providerId: string | null;
|
|
56
|
+
readonly source: "send" | "check";
|
|
57
|
+
readonly code?: string;
|
|
58
|
+
readonly message: string;
|
|
59
|
+
}
|
|
60
|
+
/** Alias the provider's existing verdict vocabulary instead of defining another taxonomy. */
|
|
61
|
+
export type ProviderDomainState = MailDomainState;
|
|
62
|
+
/** Provider verification is independent of DNS answers and credential presence. */
|
|
63
|
+
export type SenderDomainVerification = {
|
|
64
|
+
state: ProviderDomainState;
|
|
65
|
+
domain: string;
|
|
66
|
+
providerId: string;
|
|
67
|
+
} | {
|
|
68
|
+
state: "no_sender";
|
|
69
|
+
providerId: string | null;
|
|
70
|
+
} | {
|
|
71
|
+
state: "no_provider";
|
|
72
|
+
domain: string;
|
|
73
|
+
providerId: string | null;
|
|
74
|
+
} | {
|
|
75
|
+
state: "unsupported";
|
|
76
|
+
domain: string;
|
|
77
|
+
providerId: string;
|
|
78
|
+
} | {
|
|
79
|
+
state: "unknown";
|
|
80
|
+
domain?: string;
|
|
81
|
+
providerId: string | null;
|
|
82
|
+
reason: string;
|
|
83
|
+
};
|
|
84
|
+
export interface MailStatusReport extends MailStatus {
|
|
85
|
+
readonly senderDomainVerification?: SenderDomainVerification;
|
|
86
|
+
readonly providers: readonly MailProviderConfiguration[];
|
|
87
|
+
readonly refusals: readonly string[];
|
|
88
|
+
readonly checkedAt: string;
|
|
89
|
+
readonly lastError?: MailProviderError;
|
|
90
|
+
}
|
|
91
|
+
export type MailProviderCheck = {
|
|
92
|
+
ok: true;
|
|
93
|
+
label: string;
|
|
94
|
+
} | {
|
|
95
|
+
ok: false;
|
|
96
|
+
reason: string;
|
|
97
|
+
};
|
|
98
|
+
/** Host-built catalog: trusted loading, labels and setup copy remain outside the engine. */
|
|
99
|
+
export interface MailProviderCatalog {
|
|
100
|
+
readonly registry: MailProviderRegistry;
|
|
101
|
+
readonly pluginIds: readonly string[];
|
|
102
|
+
readonly refusals: readonly string[];
|
|
103
|
+
credentialLabelFor(required: {
|
|
104
|
+
id: string;
|
|
105
|
+
}): string | undefined;
|
|
106
|
+
setupFor(required: {
|
|
107
|
+
id: string;
|
|
108
|
+
}): MailProviderSetup;
|
|
109
|
+
}
|
|
110
|
+
/** Host settings adapter; reads do not infer provider selection from saved credentials. */
|
|
111
|
+
export interface MailProviderSettingPort {
|
|
112
|
+
read(required: Record<string, never>): Promise<MailProviderSetting | null>;
|
|
113
|
+
write(required: {
|
|
114
|
+
setting: MailProviderSetting;
|
|
115
|
+
}): Promise<void>;
|
|
116
|
+
}
|
|
117
|
+
/** Host owns metadata lookup and decryption. Metadata may fail independently of key resolution. */
|
|
118
|
+
export interface MailCredentialPort {
|
|
119
|
+
readFact(required: {
|
|
120
|
+
label: string;
|
|
121
|
+
}): Promise<MailCredentialFact | undefined>;
|
|
122
|
+
resolve(required: {
|
|
123
|
+
catalog: MailProviderCatalog;
|
|
124
|
+
id: string;
|
|
125
|
+
}): Promise<MailAdapterCredential | null>;
|
|
126
|
+
}
|
|
127
|
+
/** Read-only provider verdict; implementations return unknown on lookup/provider failures. */
|
|
128
|
+
export type SenderDomainVerificationPort = () => Promise<SenderDomainVerification>;
|
|
129
|
+
//# sourceMappingURL=engine-contracts.d.ts.map
|
package/dist/mail/index.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
export type * from "./types.js";
|
|
2
2
|
export type * from "./ports.js";
|
|
3
3
|
export type * from "./adapter-module.js";
|
|
4
|
-
export
|
|
5
|
-
export
|
|
4
|
+
export type * from "./engine-contracts.js";
|
|
5
|
+
export { wrapMailerWithPurposeGate, isPurposeGateOpen } from "./purpose-scoped-mailer.js";
|
|
6
|
+
export type { WrapMailerWithPurposeGateOptions, PurposeGateRequired } from "./purpose-scoped-mailer.js";
|
|
6
7
|
export { isMailDeliveryAvailable } from "./delivery-availability.js";
|
|
7
8
|
export { createMailSenderIdentityResolver, NON_DELIVERING_MAIL_DRIVERS, SenderIdentityNotConfiguredError } from "./sender-identity.js";
|
|
8
9
|
export type { ConfiguredMailSender, CreateMailSenderIdentityResolverOptional, MailSenderIdentityPort } from "./sender-identity.js";
|
|
@@ -15,4 +16,16 @@ export type { MailDeliveryPolicy, MailDeliveryPolicyDeps, MailDeliveryPolicyOpti
|
|
|
15
16
|
export { createMailFeedbackIngest, suppressionScopeFor } from "./feedback.js";
|
|
16
17
|
export type { MailFeedbackInboxPort, MailFeedbackIngestDeps, MailFeedbackIngestOptions, MailFeedbackResponse } from "./feedback.js";
|
|
17
18
|
export { createSmtpMailProvider, createConsoleMailProvider, parseSmtpEndpoint } from "./builtin-providers.js";
|
|
19
|
+
export { safeMailProviderText, credentialSecrets } from "./provider-error.js";
|
|
20
|
+
export { readMailDomain, isDomainReadPermissionRefused } from "./domain-read.js";
|
|
21
|
+
export type { MailDomainReadCredentialPort } from "./domain-read.js";
|
|
22
|
+
export { createSenderDomainVerification } from "./sender-domain-verification.js";
|
|
23
|
+
export type { SenderDomainVerificationDeps } from "./sender-domain-verification.js";
|
|
24
|
+
export { createMailPolicyWrapper } from "./delivery-wrapper.js";
|
|
25
|
+
export type { MailPolicyWrapperDeps, MailPolicyWrapperOptions, MailPolicyWrapperInput } from "./delivery-wrapper.js";
|
|
26
|
+
export { createResolvedMailer } from "./resolve-mailer.js";
|
|
27
|
+
export type { ResolveMailerDeps, ResolveMailerOptions, ResolvedMailer } from "./resolve-mailer.js";
|
|
28
|
+
export { createTtlCache, MAIL_STATUS_TTL_MS } from "./mail-status-cache.js";
|
|
29
|
+
export type { TtlCache } from "./mail-status-cache.js";
|
|
30
|
+
export * from "./senders.js";
|
|
18
31
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/mail/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { wrapMailerWithPurposeGate } from "./purpose-scoped-mailer.js";
|
|
1
|
+
export { wrapMailerWithPurposeGate, isPurposeGateOpen } from "./purpose-scoped-mailer.js";
|
|
2
2
|
export { isMailDeliveryAvailable } from "./delivery-availability.js";
|
|
3
3
|
export { createMailSenderIdentityResolver, NON_DELIVERING_MAIL_DRIVERS, SenderIdentityNotConfiguredError } from "./sender-identity.js";
|
|
4
4
|
export { createMailProviderRegistry, validateMailProvider, initialMailProviderSetting, computeMailStatus, MAIL_PROVIDER_SETTING_KEY } from "./provider.js";
|
|
@@ -6,4 +6,11 @@ export { createMailLaneLimiter } from "./rate-limit.js";
|
|
|
6
6
|
export { createMailDeliveryPolicy, createPolicyMailer, laneForPurpose, hasOneClickUnsubscribe, mailPayloadHash, MAIL_RATE_LIMITED, isMailRateLimited } from "./policy.js";
|
|
7
7
|
export { createMailFeedbackIngest, suppressionScopeFor } from "./feedback.js";
|
|
8
8
|
export { createSmtpMailProvider, createConsoleMailProvider, parseSmtpEndpoint } from "./builtin-providers.js";
|
|
9
|
+
export { safeMailProviderText, credentialSecrets } from "./provider-error.js";
|
|
10
|
+
export { readMailDomain, isDomainReadPermissionRefused } from "./domain-read.js";
|
|
11
|
+
export { createSenderDomainVerification } from "./sender-domain-verification.js";
|
|
12
|
+
export { createMailPolicyWrapper } from "./delivery-wrapper.js";
|
|
13
|
+
export { createResolvedMailer } from "./resolve-mailer.js";
|
|
14
|
+
export { createTtlCache, MAIL_STATUS_TTL_MS } from "./mail-status-cache.js";
|
|
15
|
+
export * from "./senders.js";
|
|
9
16
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file A short-TTL, single-flight cache for the mail status report.
|
|
3
|
+
* Building the report can call the provider (domain status) and decrypt keys, which took 4–9 s per
|
|
4
|
+
* admin page load; within the TTL every reader shares one result. `refresh` requests a fresh build,
|
|
5
|
+
* subject to the optional minimum refresh interval.
|
|
6
|
+
* `invalidate` drops the cached one (a provider switch or a key save). A failed build is never
|
|
7
|
+
* cached, and a build that started before an invalidation is not stored over it.
|
|
8
|
+
*/
|
|
9
|
+
/** Default freshness window for mail status. */
|
|
10
|
+
export declare const MAIL_STATUS_TTL_MS = 30000;
|
|
11
|
+
export interface TtlCache<T> {
|
|
12
|
+
get(optional?: {
|
|
13
|
+
refresh?: boolean;
|
|
14
|
+
}): Promise<T>;
|
|
15
|
+
invalidate(): void;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* @param required.load - Builds a fresh value; its rejection reaches every concurrent caller.
|
|
19
|
+
* @param required.ttlMs - How long a built value is served.
|
|
20
|
+
* @param required.nowMs - Injected clock.
|
|
21
|
+
* @param optional.minRefreshIntervalMs - Ignore forced refresh inside this interval after a build
|
|
22
|
+
* starts; zero preserves unrestricted superseding refreshes. Invalidation resets the interval.
|
|
23
|
+
* @complexity O(1) per call; ordinary reads share one pending load. Explicit refresh may supersede it.
|
|
24
|
+
* @example const cache = createTtlCache({ load, ttlMs: MAIL_STATUS_TTL_MS, nowMs }); await cache.get();
|
|
25
|
+
*/
|
|
26
|
+
export declare function createTtlCache<T>(required: {
|
|
27
|
+
load: () => Promise<T>;
|
|
28
|
+
ttlMs: number;
|
|
29
|
+
nowMs: () => number;
|
|
30
|
+
}, optional?: {
|
|
31
|
+
minRefreshIntervalMs?: number;
|
|
32
|
+
}): TtlCache<T>;
|
|
33
|
+
//# sourceMappingURL=mail-status-cache.d.ts.map
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file A short-TTL, single-flight cache for the mail status report.
|
|
3
|
+
* Building the report can call the provider (domain status) and decrypt keys, which took 4–9 s per
|
|
4
|
+
* admin page load; within the TTL every reader shares one result. `refresh` requests a fresh build,
|
|
5
|
+
* subject to the optional minimum refresh interval.
|
|
6
|
+
* `invalidate` drops the cached one (a provider switch or a key save). A failed build is never
|
|
7
|
+
* cached, and a build that started before an invalidation is not stored over it.
|
|
8
|
+
*/
|
|
9
|
+
/** Default freshness window for mail status. */
|
|
10
|
+
export const MAIL_STATUS_TTL_MS = 30_000;
|
|
11
|
+
/**
|
|
12
|
+
* @param required.load - Builds a fresh value; its rejection reaches every concurrent caller.
|
|
13
|
+
* @param required.ttlMs - How long a built value is served.
|
|
14
|
+
* @param required.nowMs - Injected clock.
|
|
15
|
+
* @param optional.minRefreshIntervalMs - Ignore forced refresh inside this interval after a build
|
|
16
|
+
* starts; zero preserves unrestricted superseding refreshes. Invalidation resets the interval.
|
|
17
|
+
* @complexity O(1) per call; ordinary reads share one pending load. Explicit refresh may supersede it.
|
|
18
|
+
* @example const cache = createTtlCache({ load, ttlMs: MAIL_STATUS_TTL_MS, nowMs }); await cache.get();
|
|
19
|
+
*/
|
|
20
|
+
export function createTtlCache(required, optional = {}) {
|
|
21
|
+
let cached;
|
|
22
|
+
let inFlight;
|
|
23
|
+
let generation = 0;
|
|
24
|
+
let lastBuildAt = -Infinity;
|
|
25
|
+
const start = () => {
|
|
26
|
+
lastBuildAt = required.nowMs();
|
|
27
|
+
const startedIn = generation;
|
|
28
|
+
const started = required.load().then((value) => {
|
|
29
|
+
if (startedIn === generation)
|
|
30
|
+
cached = { value, expiresAt: required.nowMs() + required.ttlMs };
|
|
31
|
+
return value;
|
|
32
|
+
}).finally(() => { if (inFlight === started)
|
|
33
|
+
inFlight = undefined; });
|
|
34
|
+
inFlight = started;
|
|
35
|
+
return started;
|
|
36
|
+
};
|
|
37
|
+
const minRefreshIntervalMs = optional.minRefreshIntervalMs ?? 0;
|
|
38
|
+
return {
|
|
39
|
+
get: async (optional = {}) => {
|
|
40
|
+
if (optional.refresh && required.nowMs() - lastBuildAt >= minRefreshIntervalMs) {
|
|
41
|
+
generation += 1;
|
|
42
|
+
return start();
|
|
43
|
+
}
|
|
44
|
+
if (cached && required.nowMs() < cached.expiresAt)
|
|
45
|
+
return cached.value;
|
|
46
|
+
return inFlight ?? start();
|
|
47
|
+
},
|
|
48
|
+
invalidate: () => {
|
|
49
|
+
generation += 1;
|
|
50
|
+
cached = undefined;
|
|
51
|
+
inFlight = undefined;
|
|
52
|
+
lastBuildAt = -Infinity;
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=mail-status-cache.js.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type EmailSender, type EmailSourceDescription, type EmailSourceField, type EmailSourceRequest, type EmailTemplatePort } from "./email-sender.js";
|
|
2
|
+
import type { EmailAddress } from "./types.js";
|
|
3
|
+
/** Refuses unsupported declarations and undeclared placeholders before activation or rendering.
|
|
4
|
+
* @complexity O(f + d + p) source fields, declarations and placeholders; no I/O. */
|
|
5
|
+
export declare function validateEmailSourceTemplate(required: {
|
|
6
|
+
readonly fields: readonly EmailSourceField[];
|
|
7
|
+
readonly template: {
|
|
8
|
+
readonly fields: readonly string[];
|
|
9
|
+
readonly placeholders: readonly string[];
|
|
10
|
+
};
|
|
11
|
+
}, _optional?: Record<string, never>): void;
|
|
12
|
+
/** A preparation result is suitable for freezing in the sender's existing job, never a new queue. */
|
|
13
|
+
export interface PreparedSenderEmail {
|
|
14
|
+
readonly bindingId: string;
|
|
15
|
+
readonly templateId: string;
|
|
16
|
+
readonly templateVersion: string;
|
|
17
|
+
readonly to: EmailAddress;
|
|
18
|
+
readonly replyTo: EmailAddress;
|
|
19
|
+
readonly subject: string;
|
|
20
|
+
readonly html: string;
|
|
21
|
+
readonly text: string;
|
|
22
|
+
readonly purpose: EmailSourceDescription["purpose"];
|
|
23
|
+
readonly lane: EmailSourceDescription["lane"];
|
|
24
|
+
readonly warnings: readonly string[];
|
|
25
|
+
}
|
|
26
|
+
/** Resolves authoritative event data and the saved binding, then calls the existing renderer.
|
|
27
|
+
* No preview samples, envelope overrides or delivery effects enter this operation.
|
|
28
|
+
* @throws EmailSenderError for ownership, inactive/pre-activation events, readiness, compatibility or missing data; port errors propagate.
|
|
29
|
+
* @complexity O(fields + declarations + placeholders); one describe, resolve and render per event. */
|
|
30
|
+
export declare function prepareSenderEmail(required: EmailSourceRequest & {
|
|
31
|
+
readonly sender: EmailSender;
|
|
32
|
+
readonly templates: EmailTemplatePort;
|
|
33
|
+
readonly eventId: string;
|
|
34
|
+
}, _optional?: Record<string, never>): Promise<PreparedSenderEmail>;
|
|
35
|
+
//# sourceMappingURL=prepare-sender-email.d.ts.map
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { EmailSenderError } from "./email-sender.js";
|
|
2
|
+
/** Refuses unsupported declarations and undeclared placeholders before activation or rendering.
|
|
3
|
+
* @complexity O(f + d + p) source fields, declarations and placeholders; no I/O. */
|
|
4
|
+
export function validateEmailSourceTemplate(required, _optional = {}) {
|
|
5
|
+
const available = new Set(required.fields.map(field => field.token));
|
|
6
|
+
const declared = new Set(required.template.fields);
|
|
7
|
+
if (required.template.fields.some(token => !available.has(token)))
|
|
8
|
+
throw new EmailSenderError({ code: "incompatible" });
|
|
9
|
+
if (required.template.placeholders.some(token => !declared.has(token)))
|
|
10
|
+
throw new EmailSenderError({ code: "incompatible" });
|
|
11
|
+
}
|
|
12
|
+
/** Selects declared values, refusing missing required answers and warning on absent optional ones. */
|
|
13
|
+
function templateValues(required, _optional = {}) {
|
|
14
|
+
const fields = new Map(required.fields.map(field => [field.token, field]));
|
|
15
|
+
const warnings = [];
|
|
16
|
+
const values = Object.fromEntries(required.tokens.map(token => {
|
|
17
|
+
const value = Object.hasOwn(required.values, token) ? required.values[token] : undefined;
|
|
18
|
+
if (typeof value === "string" && value !== "")
|
|
19
|
+
return [token, value];
|
|
20
|
+
if (fields.get(token)?.required)
|
|
21
|
+
throw new EmailSenderError({ code: "missing_value" });
|
|
22
|
+
warnings.push(token);
|
|
23
|
+
return [token, ""];
|
|
24
|
+
}));
|
|
25
|
+
return { values, warnings };
|
|
26
|
+
}
|
|
27
|
+
/** Resolves only a source-permitted recipient; configuration and missing answers fail separately.
|
|
28
|
+
* @complexity O(permitted choices + resolved recipients); no I/O. */
|
|
29
|
+
function recipientAddress({ source, resolved, recipientId }, _optional = {}) {
|
|
30
|
+
if (!source.permittedRecipients.some(choice => choice.id === recipientId))
|
|
31
|
+
throw new EmailSenderError({ code: "configuration" });
|
|
32
|
+
const recipient = resolved.recipients.find(choice => choice.id === recipientId);
|
|
33
|
+
if (!recipient)
|
|
34
|
+
throw new EmailSenderError({ code: "missing_value" });
|
|
35
|
+
return recipient.address;
|
|
36
|
+
}
|
|
37
|
+
/** Resolves authoritative event data and the saved binding, then calls the existing renderer.
|
|
38
|
+
* No preview samples, envelope overrides or delivery effects enter this operation.
|
|
39
|
+
* @throws EmailSenderError for ownership, inactive/pre-activation events, readiness, compatibility or missing data; port errors propagate.
|
|
40
|
+
* @complexity O(fields + declarations + placeholders); one describe, resolve and render per event. */
|
|
41
|
+
export async function prepareSenderEmail(required, _optional = {}) {
|
|
42
|
+
const { sender, templates, workspaceId, principalId, sourceId, eventId } = required;
|
|
43
|
+
const context = { workspaceId, principalId, sourceId };
|
|
44
|
+
const source = await sender.describeSource(context);
|
|
45
|
+
if (source.senderId !== sender.id || source.sourceId !== sourceId)
|
|
46
|
+
throw new EmailSenderError({ code: "ownership" });
|
|
47
|
+
const binding = source.binding;
|
|
48
|
+
if (!binding?.enabled)
|
|
49
|
+
throw new EmailSenderError({ code: "configuration" });
|
|
50
|
+
if (!source.readiness.ready)
|
|
51
|
+
throw new EmailSenderError({ code: "not_ready" });
|
|
52
|
+
const templateContext = { workspaceId, principalId, templateId: binding.templateId };
|
|
53
|
+
const template = await templates.describe(templateContext);
|
|
54
|
+
validateEmailSourceTemplate({ fields: source.fields, template });
|
|
55
|
+
const resolved = await sender.resolveEvent({ ...context, eventId });
|
|
56
|
+
// MOVE: Compare against the latest activation so re-enabling cannot revive a backlog.
|
|
57
|
+
if (!binding.activatedAt || !(Date.parse(resolved.occurredAt) >= Date.parse(binding.activatedAt)))
|
|
58
|
+
throw new EmailSenderError({ code: "configuration" });
|
|
59
|
+
const to = recipientAddress({ source, resolved, recipientId: binding.recipientId });
|
|
60
|
+
const { values, warnings } = templateValues({ fields: source.fields, tokens: template.fields, values: resolved.values });
|
|
61
|
+
const rendered = await templates.render({ ...templateContext, version: template.version, values });
|
|
62
|
+
if (rendered.missingFields.length > 0)
|
|
63
|
+
throw new EmailSenderError({ code: "missing_value" });
|
|
64
|
+
return { bindingId: binding.bindingId, templateId: binding.templateId, templateVersion: template.version, to,
|
|
65
|
+
replyTo: { email: binding.replyTo }, subject: rendered.subject, html: rendered.html, text: rendered.text,
|
|
66
|
+
purpose: source.purpose, lane: source.lane, warnings };
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=prepare-sender-email.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Provider diagnostics cross the host boundary only after Jini's secret scrubber, then a cap.
|
|
2
|
+
* Exact credential values are supplied where available; redaction precedes truncation so a token
|
|
3
|
+
* crossing the length boundary cannot leave a readable prefix. Mail also masks URL token parameters
|
|
4
|
+
* (including fragments) through Jini's caller-owned patterns.
|
|
5
|
+
* @complexity O(n) in diagnostic text for a bounded set of credential values; output <= 1000 chars.
|
|
6
|
+
*/
|
|
7
|
+
export declare function safeMailProviderText({ text }: {
|
|
8
|
+
text: string;
|
|
9
|
+
}, { secrets }?: {
|
|
10
|
+
secrets?: readonly string[];
|
|
11
|
+
}): string;
|
|
12
|
+
/** The credential's string values, so exact redaction also catches keys no pattern recognises.
|
|
13
|
+
* @complexity O(k) credential fields.
|
|
14
|
+
*/
|
|
15
|
+
export declare function credentialSecrets({ credential }: {
|
|
16
|
+
credential: object;
|
|
17
|
+
}, _options?: Record<string, never>): string[];
|
|
18
|
+
//# sourceMappingURL=provider-error.d.ts.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { redactSecrets } from "@jini-ai/core";
|
|
2
|
+
/** Provider diagnostics cross the host boundary only after Jini's secret scrubber, then a cap.
|
|
3
|
+
* Exact credential values are supplied where available; redaction precedes truncation so a token
|
|
4
|
+
* crossing the length boundary cannot leave a readable prefix. Mail also masks URL token parameters
|
|
5
|
+
* (including fragments) through Jini's caller-owned patterns.
|
|
6
|
+
* @complexity O(n) in diagnostic text for a bounded set of credential values; output <= 1000 chars.
|
|
7
|
+
*/
|
|
8
|
+
export function safeMailProviderText({ text }, { secrets = [] } = {}) {
|
|
9
|
+
return redactSecrets({ input: text }, {
|
|
10
|
+
exactSecrets: secrets,
|
|
11
|
+
redactPii: false,
|
|
12
|
+
// Any secret-shaped URL parameter (token, key, secret, signature, code, password), not only `token=`:
|
|
13
|
+
// provider errors echo whole request URLs, and the parameter name varies by provider. The value runs
|
|
14
|
+
// to the next `&` so a trailing `#fragment` is masked with it.
|
|
15
|
+
extraPatterns: [{ name: "url_secret_param", regex: /[?&#;][\w.-]*(?:token|key|secret|signature|sig|code|password|pwd|auth)[\w.-]*=[^&\s"'<>]+/gi }],
|
|
16
|
+
}).slice(0, 1000);
|
|
17
|
+
}
|
|
18
|
+
/** The credential's string values, so exact redaction also catches keys no pattern recognises.
|
|
19
|
+
* @complexity O(k) credential fields.
|
|
20
|
+
*/
|
|
21
|
+
export function credentialSecrets({ credential }, _options = {}) {
|
|
22
|
+
return Object.values(credential).filter((value) => typeof value === "string");
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=provider-error.js.map
|
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
/** Unknown and absent lanes require a durable notification path in production.
|
|
2
2
|
* Local mode and explicit interactive sends proceed without that gate.
|
|
3
3
|
*/
|
|
4
|
-
import type { MailerPort, DeliveryReadinessPolicy } from "./ports.js";
|
|
4
|
+
import type { MailerPort, MailerSendOptions, DeliveryReadinessPolicy } from "./ports.js";
|
|
5
5
|
type RuntimeMode = "production" | "local";
|
|
6
6
|
export interface WrapMailerWithPurposeGateOptions {
|
|
7
7
|
inner: MailerPort;
|
|
8
8
|
mode: RuntimeMode;
|
|
9
9
|
readiness: DeliveryReadinessPolicy;
|
|
10
10
|
}
|
|
11
|
+
export interface PurposeGateRequired {
|
|
12
|
+
mode: RuntimeMode;
|
|
13
|
+
readiness: DeliveryReadinessPolicy;
|
|
14
|
+
options: Pick<MailerSendOptions, "lane" | "sourceContext">;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Ask the delivery gate without delivering: unknown/absent lanes require durable readiness.
|
|
18
|
+
* Invalid modes throw TypeError; readiness errors propagate so callers can report unknown.
|
|
19
|
+
* @complexity O(1), with at most one call to the supplied readiness predicate.
|
|
20
|
+
*/
|
|
21
|
+
export declare function isPurposeGateOpen(required: PurposeGateRequired): boolean;
|
|
11
22
|
/** Validates the mode at wrap time and gates notification sends/batches before delivery.
|
|
12
23
|
* Unknown modes throw TypeError; only explicit local mode bypasses durable readiness.
|
|
13
24
|
* @complexity O(1) policy work per send/batch, before delegating delivery.
|
|
@@ -9,6 +9,25 @@ function resolveLane(options) {
|
|
|
9
9
|
function refusalError(capabilityName) {
|
|
10
10
|
return new Error(`MAILER_SEND_REFUSED_NO_DURABLE_PATH: notification-lane mailer send refused for capability "${capabilityName}" — no durable outbox path registered`);
|
|
11
11
|
}
|
|
12
|
+
function validateMode(mode) {
|
|
13
|
+
if (mode !== "production" && mode !== "local") {
|
|
14
|
+
throw new TypeError('mailer mode must be "production" or "local"');
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Ask the delivery gate without delivering: unknown/absent lanes require durable readiness.
|
|
19
|
+
* Invalid modes throw TypeError; readiness errors propagate so callers can report unknown.
|
|
20
|
+
* @complexity O(1), with at most one call to the supplied readiness predicate.
|
|
21
|
+
*/
|
|
22
|
+
export function isPurposeGateOpen(required) {
|
|
23
|
+
const { mode, readiness, options } = required;
|
|
24
|
+
validateMode(mode);
|
|
25
|
+
if (mode === "local")
|
|
26
|
+
return true;
|
|
27
|
+
if (resolveLane(options) !== "notification")
|
|
28
|
+
return true;
|
|
29
|
+
return readiness.isReady({ capabilityName: options.sourceContext?.module ?? "unknown" });
|
|
30
|
+
}
|
|
12
31
|
// Decorate the existing port rather than inventing a second delivery contract. The caller resolves
|
|
13
32
|
// mode once at composition; never re-read the environment per send and change policy mid-process.
|
|
14
33
|
/** Validates the mode at wrap time and gates notification sends/batches before delivery.
|
|
@@ -18,19 +37,11 @@ function refusalError(capabilityName) {
|
|
|
18
37
|
*/
|
|
19
38
|
export function wrapMailerWithPurposeGate(options) {
|
|
20
39
|
const { inner, mode, readiness } = options;
|
|
21
|
-
// Validate
|
|
22
|
-
|
|
23
|
-
throw new TypeError('mailer mode must be "production" or "local"');
|
|
24
|
-
}
|
|
40
|
+
// Validate at composition: typos must never silently disable durable delivery.
|
|
41
|
+
validateMode(mode);
|
|
25
42
|
function checkGate(sendOptions) {
|
|
26
|
-
if (mode
|
|
27
|
-
|
|
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);
|
|
43
|
+
if (!isPurposeGateOpen({ mode, readiness, options: sendOptions })) {
|
|
44
|
+
throw refusalError(sendOptions.sourceContext?.module ?? "unknown");
|
|
34
45
|
}
|
|
35
46
|
}
|
|
36
47
|
return {
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { HttpClientPort } from "@jini-ai/core/primitives";
|
|
2
|
+
import type { DomainRequirement } from "@jini-ai/core/provider-registry";
|
|
3
|
+
import type { MailAdapterCredential } from "./adapter-module.js";
|
|
4
|
+
import { type MailDomainReadCredentialPort } from "./domain-read.js";
|
|
5
|
+
import type { MailCredentialFact, MailCredentialPort, MailProviderCatalog, MailProviderCheck, MailProviderConfigurationReport, MailProviderSettingPort, MailStatusReport, SenderDomainVerificationPort } from "./engine-contracts.js";
|
|
6
|
+
import type { MailDeliveryLane, MailerPort } from "./ports.js";
|
|
7
|
+
import { type MailProviderModule, type MailProviderSetting } from "./provider.js";
|
|
8
|
+
/** Required host boundaries. HTTP is passed through unchanged; the engine never calls it. */
|
|
9
|
+
export interface ResolveMailerDeps<TRecord extends MailCredentialFact = MailCredentialFact> {
|
|
10
|
+
workspaceId: string;
|
|
11
|
+
mode: "local" | "production";
|
|
12
|
+
providerSetting: MailProviderSettingPort;
|
|
13
|
+
credentials: MailCredentialPort;
|
|
14
|
+
/** Metadata only. Preserve the host's row shape for its projection (e.g. SMTP username presence);
|
|
15
|
+
* the engine only interprets the separate readFact fingerprint. No decryption here. */
|
|
16
|
+
listCredentialFacts: (required: Record<string, never>) => Promise<readonly TRecord[]>;
|
|
17
|
+
loadCatalog: (required: Record<string, never>) => Promise<MailProviderCatalog>;
|
|
18
|
+
httpClient: HttpClientPort;
|
|
19
|
+
/** Host retains provider ABI binding and credential-aware scrubbing of results/throws. */
|
|
20
|
+
createProviderMailer: (required: {
|
|
21
|
+
provider: MailProviderModule;
|
|
22
|
+
credential: MailAdapterCredential;
|
|
23
|
+
}) => MailerPort;
|
|
24
|
+
/** Synchronous safe DTO projection; labels, setup forms and product copy stay in the host. */
|
|
25
|
+
describeProviders: (required: {
|
|
26
|
+
catalog: MailProviderCatalog;
|
|
27
|
+
records: readonly TRecord[];
|
|
28
|
+
setting: MailProviderSetting;
|
|
29
|
+
}) => MailProviderConfigurationReport;
|
|
30
|
+
unconfiguredMessage: (required: {
|
|
31
|
+
lane: MailDeliveryLane;
|
|
32
|
+
}) => string;
|
|
33
|
+
missingCredentialReason: (required: {
|
|
34
|
+
label: string;
|
|
35
|
+
}) => string;
|
|
36
|
+
credentialFailureReason: (required: {
|
|
37
|
+
label: string;
|
|
38
|
+
err: unknown;
|
|
39
|
+
}) => string;
|
|
40
|
+
}
|
|
41
|
+
export interface ResolveMailerOptions {
|
|
42
|
+
/** Delivery policy decorator; identity when absent. */
|
|
43
|
+
wrapProviderMailer?: (required: {
|
|
44
|
+
providerId: string;
|
|
45
|
+
provider: MailProviderModule;
|
|
46
|
+
mailer: MailerPort;
|
|
47
|
+
}) => MailerPort;
|
|
48
|
+
readSenderEmail?: () => Promise<string | undefined>;
|
|
49
|
+
resolveDomainReadCredential?: MailDomainReadCredentialPort;
|
|
50
|
+
warn?: (message: string) => void;
|
|
51
|
+
clock?: {
|
|
52
|
+
nowMs(): number;
|
|
53
|
+
};
|
|
54
|
+
statusTtlMs?: number;
|
|
55
|
+
domainRequirementsTimeoutMs?: number;
|
|
56
|
+
}
|
|
57
|
+
/** The control surface remains stable across host adoption, including invalidate on setting writes. */
|
|
58
|
+
export interface ResolvedMailer {
|
|
59
|
+
/** Safe to use immediately; each send awaits its lane's resolution. */
|
|
60
|
+
mailer: MailerPort;
|
|
61
|
+
/** Resolves once the initial setting + transactional resolution settle (a test seam). */
|
|
62
|
+
ready: Promise<void>;
|
|
63
|
+
/** Refreshes unresolved configuration and awaits the current driver without sending. */
|
|
64
|
+
settle: () => Promise<void>;
|
|
65
|
+
/** Cached briefly; `refresh` rebuilds. Each report carries `checkedAt` and the live `lastError`. */
|
|
66
|
+
status: (optional?: {
|
|
67
|
+
refresh?: boolean;
|
|
68
|
+
}) => Promise<MailStatusReport>;
|
|
69
|
+
/** The host's safe per-provider configuration DTO. Never a secret. */
|
|
70
|
+
describeProviders: () => Promise<MailProviderConfigurationReport>;
|
|
71
|
+
/** Drops cached provider mailers and the cached status; the next send and read resolve fresh. */
|
|
72
|
+
invalidate: () => void;
|
|
73
|
+
laneReady: (required: {
|
|
74
|
+
lane: MailDeliveryLane;
|
|
75
|
+
}) => Promise<boolean>;
|
|
76
|
+
senderDomainVerification: SenderDomainVerificationPort;
|
|
77
|
+
/** Whether `id` may be selected: a registered hosted provider whose saved key resolves. Sends nothing. */
|
|
78
|
+
checkProvider: (required: {
|
|
79
|
+
id: string;
|
|
80
|
+
}) => Promise<MailProviderCheck>;
|
|
81
|
+
/** Null for another workspace, absent sender/provider/key or unsupported verification.
|
|
82
|
+
* Provider failures throw caller-safe ToolInputError; the deadline throws Error. */
|
|
83
|
+
domainRequirements: (required: {
|
|
84
|
+
workspaceId: string;
|
|
85
|
+
}) => Promise<DomainRequirement | null>;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Build a lane router and its control surface. The host binds catalogs, secrets, safe copy and DTOs.
|
|
89
|
+
* Each send reads selection and metadata; boot starts resolution synchronously. Selection faults
|
|
90
|
+
* refuse safely; status/description setup faults propagate and are never cached.
|
|
91
|
+
* @complexity O(providers) cached mailers/flights; status holds one report for its TTL.
|
|
92
|
+
* @example const control = createResolvedMailer(hostPorts, { clock }); await control.ready;
|
|
93
|
+
*/
|
|
94
|
+
export declare function createResolvedMailer<TRecord extends MailCredentialFact = MailCredentialFact>(required: ResolveMailerDeps<TRecord>, optional?: ResolveMailerOptions): ResolvedMailer;
|
|
95
|
+
//# sourceMappingURL=resolve-mailer.d.ts.map
|