@jini-ai/platform 0.6.12 → 0.6.15-rc.2

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 (37) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +54 -0
  3. package/dist/asset-cache.js +27 -13
  4. package/dist/command.d.ts +1 -1
  5. package/dist/command.js +11 -8
  6. package/dist/download.js +26 -32
  7. package/dist/fetch-with-timeout.d.ts +77 -0
  8. package/dist/fetch-with-timeout.js +148 -2
  9. package/dist/fs/directory-user-config.js +2 -1
  10. package/dist/fs/directory-workspace.js +3 -1
  11. package/dist/fs/guarded-reader.d.ts +19 -47
  12. package/dist/fs/guarded-reader.js +82 -13
  13. package/dist/index.d.ts +2 -2
  14. package/dist/index.js +1 -1
  15. package/dist/mail/adapter-module.d.ts +18 -0
  16. package/dist/mail/adapter-module.js +13 -1
  17. package/dist/mail/email-sender-registry.d.ts +15 -0
  18. package/dist/mail/email-sender-registry.js +22 -0
  19. package/dist/mail/email-sender.d.ts +109 -0
  20. package/dist/mail/email-sender.js +10 -0
  21. package/dist/mail/index.d.ts +16 -2
  22. package/dist/mail/index.js +9 -1
  23. package/dist/mail/mail-status-cache.d.ts +33 -0
  24. package/dist/mail/mail-status-cache.js +56 -0
  25. package/dist/mail/prepare-sender-email.d.ts +35 -0
  26. package/dist/mail/prepare-sender-email.js +68 -0
  27. package/dist/mail/provider.js +4 -1
  28. package/dist/mail/resolve-mailer.d.ts +95 -0
  29. package/dist/mail/resolve-mailer.js +348 -0
  30. package/dist/mail/senders.d.ts +5 -0
  31. package/dist/mail/senders.js +5 -0
  32. package/dist/secrets/credential-sets/vault.repo.memory.d.ts +1 -4
  33. package/dist/secrets/credential-sets/vault.repo.memory.js +3 -1
  34. package/dist/secrets/credential-sets/vault.service.d.ts +17 -9
  35. package/dist/secrets/credential-sets/vault.service.js +18 -9
  36. package/dist/secrets/credential-sets/vault.types.d.ts +10 -4
  37. package/package.json +8 -2
@@ -1,8 +1,10 @@
1
1
  export type * from "./types.js";
2
2
  export type * from "./ports.js";
3
3
  export type * from "./adapter-module.js";
4
- export { wrapMailerWithPurposeGate } from "./purpose-scoped-mailer.js";
5
- export type { WrapMailerWithPurposeGateOptions } from "./purpose-scoped-mailer.js";
4
+ export { createMailAdapterContext } from "./adapter-module.js";
5
+ export type * from "./engine-contracts.js";
6
+ export { wrapMailerWithPurposeGate, isPurposeGateOpen } from "./purpose-scoped-mailer.js";
7
+ export type { WrapMailerWithPurposeGateOptions, PurposeGateRequired } from "./purpose-scoped-mailer.js";
6
8
  export { isMailDeliveryAvailable } from "./delivery-availability.js";
7
9
  export { createMailSenderIdentityResolver, NON_DELIVERING_MAIL_DRIVERS, SenderIdentityNotConfiguredError } from "./sender-identity.js";
8
10
  export type { ConfiguredMailSender, CreateMailSenderIdentityResolverOptional, MailSenderIdentityPort } from "./sender-identity.js";
@@ -15,4 +17,16 @@ export type { MailDeliveryPolicy, MailDeliveryPolicyDeps, MailDeliveryPolicyOpti
15
17
  export { createMailFeedbackIngest, suppressionScopeFor } from "./feedback.js";
16
18
  export type { MailFeedbackInboxPort, MailFeedbackIngestDeps, MailFeedbackIngestOptions, MailFeedbackResponse } from "./feedback.js";
17
19
  export { createSmtpMailProvider, createConsoleMailProvider, parseSmtpEndpoint } from "./builtin-providers.js";
20
+ export { safeMailProviderText, credentialSecrets } from "./provider-error.js";
21
+ export { readMailDomain, isDomainReadPermissionRefused } from "./domain-read.js";
22
+ export type { MailDomainReadCredentialPort } from "./domain-read.js";
23
+ export { createSenderDomainVerification } from "./sender-domain-verification.js";
24
+ export type { SenderDomainVerificationDeps } from "./sender-domain-verification.js";
25
+ export { createMailPolicyWrapper } from "./delivery-wrapper.js";
26
+ export type { MailPolicyWrapperDeps, MailPolicyWrapperOptions, MailPolicyWrapperInput } from "./delivery-wrapper.js";
27
+ export { createResolvedMailer } from "./resolve-mailer.js";
28
+ export type { ResolveMailerDeps, ResolveMailerOptions, ResolvedMailer } from "./resolve-mailer.js";
29
+ export { createTtlCache, MAIL_STATUS_TTL_MS } from "./mail-status-cache.js";
30
+ export type { TtlCache } from "./mail-status-cache.js";
31
+ export * from "./senders.js";
18
32
  //# sourceMappingURL=index.d.ts.map
@@ -1,4 +1,5 @@
1
- export { wrapMailerWithPurposeGate } from "./purpose-scoped-mailer.js";
1
+ export { createMailAdapterContext } from "./adapter-module.js";
2
+ export { wrapMailerWithPurposeGate, isPurposeGateOpen } from "./purpose-scoped-mailer.js";
2
3
  export { isMailDeliveryAvailable } from "./delivery-availability.js";
3
4
  export { createMailSenderIdentityResolver, NON_DELIVERING_MAIL_DRIVERS, SenderIdentityNotConfiguredError } from "./sender-identity.js";
4
5
  export { createMailProviderRegistry, validateMailProvider, initialMailProviderSetting, computeMailStatus, MAIL_PROVIDER_SETTING_KEY } from "./provider.js";
@@ -6,4 +7,11 @@ export { createMailLaneLimiter } from "./rate-limit.js";
6
7
  export { createMailDeliveryPolicy, createPolicyMailer, laneForPurpose, hasOneClickUnsubscribe, mailPayloadHash, MAIL_RATE_LIMITED, isMailRateLimited } from "./policy.js";
7
8
  export { createMailFeedbackIngest, suppressionScopeFor } from "./feedback.js";
8
9
  export { createSmtpMailProvider, createConsoleMailProvider, parseSmtpEndpoint } from "./builtin-providers.js";
10
+ export { safeMailProviderText, credentialSecrets } from "./provider-error.js";
11
+ export { readMailDomain, isDomainReadPermissionRefused } from "./domain-read.js";
12
+ export { createSenderDomainVerification } from "./sender-domain-verification.js";
13
+ export { createMailPolicyWrapper } from "./delivery-wrapper.js";
14
+ export { createResolvedMailer } from "./resolve-mailer.js";
15
+ export { createTtlCache, MAIL_STATUS_TTL_MS } from "./mail-status-cache.js";
16
+ export * from "./senders.js";
9
17
  //# 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
@@ -21,11 +21,14 @@ export function validateMailProvider({ provider }) {
21
21
  // A capability claim without the code behind it is how Resend once advertised feedback with no route.
22
22
  if (caps.feedback !== (provider.feedback !== undefined))
23
23
  return "capabilities.feedback must match the presence of a feedback parser";
24
- const hasDomain = typeof provider.domainRequirements === "function" && typeof provider.domainStatus === "function";
24
+ const hasDomain = hasDomainVerification(provider);
25
25
  if (caps.domainVerification !== hasDomain)
26
26
  return "capabilities.domainVerification must match domainRequirements + domainStatus";
27
27
  return undefined;
28
28
  }
29
+ function hasDomainVerification(provider) {
30
+ return typeof provider.domainRequirements === "function" && typeof provider.domainStatus === "function";
31
+ }
29
32
  /** Mail registry over the shared keyed registry; built-ins throw on a bad registration, plugins are refused. */
30
33
  export function createMailProviderRegistry(_required = {}, optional = {}) {
31
34
  const seed = (optional.builtins ?? []).map((provider) => ({ id: provider.id, provider, label: provider.label }));
@@ -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