@substrat-run/control-plane-api 0.139.0 → 0.141.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 (62) hide show
  1. package/dist/api.d.ts +17 -1
  2. package/dist/api.d.ts.map +1 -1
  3. package/dist/api.js +701 -160
  4. package/dist/api.js.map +1 -1
  5. package/dist/audited-change.d.ts +139 -0
  6. package/dist/audited-change.d.ts.map +1 -0
  7. package/dist/audited-change.js +214 -0
  8. package/dist/audited-change.js.map +1 -0
  9. package/dist/cf-observability.d.ts.map +1 -1
  10. package/dist/cf-observability.js +38 -1
  11. package/dist/cf-observability.js.map +1 -1
  12. package/dist/connect-links.d.ts +29 -0
  13. package/dist/connect-links.d.ts.map +1 -0
  14. package/dist/connect-links.js +139 -0
  15. package/dist/connect-links.js.map +1 -0
  16. package/dist/connect-url.d.ts +20 -1
  17. package/dist/connect-url.d.ts.map +1 -1
  18. package/dist/connect-url.js +1 -1
  19. package/dist/connect-url.js.map +1 -1
  20. package/dist/connection-relay.d.ts +24 -0
  21. package/dist/connection-relay.d.ts.map +1 -1
  22. package/dist/connection-relay.js +31 -12
  23. package/dist/connection-relay.js.map +1 -1
  24. package/dist/email-intent.d.ts +71 -0
  25. package/dist/email-intent.d.ts.map +1 -0
  26. package/dist/email-intent.js +186 -0
  27. package/dist/email-intent.js.map +1 -0
  28. package/dist/field-coverage-read.d.ts +44 -0
  29. package/dist/field-coverage-read.d.ts.map +1 -0
  30. package/dist/field-coverage-read.js +45 -0
  31. package/dist/field-coverage-read.js.map +1 -0
  32. package/dist/field-coverage-tally.d.ts +12 -0
  33. package/dist/field-coverage-tally.d.ts.map +1 -1
  34. package/dist/field-coverage-tally.js +55 -14
  35. package/dist/field-coverage-tally.js.map +1 -1
  36. package/dist/index.d.ts +8 -2
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +7 -3
  39. package/dist/index.js.map +1 -1
  40. package/dist/observability.d.ts +13 -0
  41. package/dist/observability.d.ts.map +1 -1
  42. package/dist/platform-drain.d.ts +12 -2
  43. package/dist/platform-drain.d.ts.map +1 -1
  44. package/dist/platform-drain.js +15 -3
  45. package/dist/platform-drain.js.map +1 -1
  46. package/dist/reconcile.d.ts +17 -2
  47. package/dist/reconcile.d.ts.map +1 -1
  48. package/dist/reconcile.js +35 -1
  49. package/dist/reconcile.js.map +1 -1
  50. package/dist/scope-copy-cleanup.d.ts +76 -0
  51. package/dist/scope-copy-cleanup.d.ts.map +1 -0
  52. package/dist/scope-copy-cleanup.js +228 -0
  53. package/dist/scope-copy-cleanup.js.map +1 -0
  54. package/dist/staff-client.d.ts +3 -1
  55. package/dist/staff-client.d.ts.map +1 -1
  56. package/dist/staff-client.js +2 -0
  57. package/dist/staff-client.js.map +1 -1
  58. package/dist/vertical-client.d.ts +30 -2
  59. package/dist/vertical-client.d.ts.map +1 -1
  60. package/dist/vertical-client.js +100 -22
  61. package/dist/vertical-client.js.map +1 -1
  62. package/package.json +10 -10
@@ -1,5 +1,5 @@
1
1
  import { isPrimaryScope, ulid } from '@substrat-run/kernel';
2
- import { connectionId, connectionRelayRequest, } from '@substrat-run/contracts';
2
+ import { instant, connectionId, connectionRelayRequest, } from '@substrat-run/contracts';
3
3
  /**
4
4
  * The connection relay (connections.md §3.5.2) — the credential half of #574's routed
5
5
  * connector dispatch. A hosted CP-less vertical's tenant admin holds a provider credential
@@ -76,21 +76,39 @@ export async function relayConnectionUpsert(host, actor, body, options = {}) {
76
76
  throw new ConnectionRelayError(PREVIEW_CONNECTIONS_REFUSAL, 403);
77
77
  }
78
78
  const vertical = rec.vertical;
79
- // Ask the provider before touching the store (#605). Deliberately here, ahead of every
80
- // write below: a refused rotation must leave the live credential exactly as it was.
81
- const probe = await options.probeCandidate?.(input.provider, input.secret);
82
- if (probe && !probe.ok && probe.refused) {
83
- throw new ConnectionRelayError(probe.error ?? `${input.provider} refused these credentials`, 422, probe);
84
- }
85
79
  // `listConnections` already excludes revoked rows; expired/errored rows are still the
86
80
  // live row for this key — rotation is what revives them. The account leg matches
87
81
  // exactly (absent matches absent): a multi-account provider's second account is a new
88
- // connection, not a rotation of the first.
82
+ // connection, not a rotation of the first. Read before the probe because a provider that
83
+ // finishes its own credential (#2100) needs the one this upsert would replace.
89
84
  const live = (await host.admin.listConnections(actor, {
90
85
  tenantId: input.tenantId,
91
86
  vertical,
92
87
  provider: input.provider,
93
88
  })).filter((c) => c.externalAccountRef === (input.externalAccountRef ?? null));
89
+ // #2100: the platform's half of the credential, made here — before the probe, so what is
90
+ // probed is exactly what is stored.
91
+ let secret = input.secret;
92
+ let expiresAt = input.expiresAt;
93
+ const prepare = options.prepareCandidate?.(input.provider);
94
+ if (prepare) {
95
+ const previous = live.length === 1
96
+ ? (await host.admin.openConnection(input.tenantId, vertical, input.provider, input.externalAccountRef))?.secret
97
+ : undefined;
98
+ const prepared = await prepare(input.secret, previous);
99
+ secret = prepared.secret;
100
+ // Parsed: a provider's date becomes the same branded instant a caller's would be. `null` is
101
+ // the provider clearing an expiry whose credential it dropped (a certificate → a client secret).
102
+ expiresAt =
103
+ input.expiresAt ??
104
+ (prepared.expiresAt === undefined || prepared.expiresAt === null ? prepared.expiresAt : instant.parse(prepared.expiresAt));
105
+ }
106
+ // Ask the provider before touching the store (#605). Deliberately here, ahead of every
107
+ // write below: a refused rotation must leave the live credential exactly as it was.
108
+ const probe = await options.probeCandidate?.(input.provider, secret);
109
+ if (probe && !probe.ok && probe.refused) {
110
+ throw new ConnectionRelayError(probe.error ?? `${input.provider} refused these credentials`, 422, probe);
111
+ }
94
112
  let id;
95
113
  let created;
96
114
  if (live.length === 0) {
@@ -103,9 +121,10 @@ export async function relayConnectionUpsert(host, actor, body, options = {}) {
103
121
  label: input.label ?? input.provider,
104
122
  externalAccountRef: input.externalAccountRef,
105
123
  scopes: input.scopes,
106
- // Whatever the caller supplies; no shipped connector does (connections.md §3.6), so today this is undefined and the row reads "not reported".
107
- expiresAt: input.expiresAt,
108
- secret: input.secret,
124
+ // Whatever the caller supplies, else what the provider's own preparation knows (#2100: a
125
+ // generated certificate's end). Undefined reads "not reported" (connections.md §3.6).
126
+ expiresAt: expiresAt ?? undefined,
127
+ secret,
109
128
  // §3.5.1 — the authorizing tenant principal, proven by the vertical's own
110
129
  // `ctx.check` before the secret ever left the operation.
111
130
  createdBy: input.createdBy,
@@ -114,7 +133,7 @@ export async function relayConnectionUpsert(host, actor, body, options = {}) {
114
133
  }
115
134
  else if (live.length === 1) {
116
135
  id = live[0].id;
117
- await host.admin.updateConnectionSecret(actor, id, input.secret, input.expiresAt, {
136
+ await host.admin.updateConnectionSecret(actor, id, secret, expiresAt, {
118
137
  rotatedBy: input.createdBy,
119
138
  });
120
139
  created = false;
@@ -1 +1 @@
1
- {"version":3,"file":"connection-relay.js","sourceRoot":"","sources":["../src/connection-relay.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"connection-relay.js","sourceRoot":"","sources":["../src/connection-relay.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,71 @@
1
+ import { type SendEmailRequest, type TenantId, type ScopeId } from '@substrat-run/contracts';
2
+ import type { PlatformRequestHandler } from './platform-drain.js';
3
+ /**
4
+ * The drain's handler for `send-email` (#2102): one mail a vertical requested inside an
5
+ * operation, delivered after that operation committed.
6
+ *
7
+ * It routes as the synchronous relay does — no `from` sends as the platform, a `from` sends
8
+ * through the tenant connection whose `MailSender` covers that address — and it retries what a
9
+ * provider deferred: a `429`, a `5xx`, or a failure with no status at all. Any other 4xx is a
10
+ * refusal and is final. Each outcome that ends the request is written into the scope as an
11
+ * event, with the provider's message id when it gives one.
12
+ *
13
+ * **Retries wait.** The drain runs on every kick and every sweep, and a kick follows any
14
+ * operation that requested something — so a throttled mailbox would otherwise be retried as
15
+ * fast as the vertical writes. A failed attempt records the earliest time to try again in the
16
+ * intent's `result` (`Retry-After` when the provider gave one, else a doubling backoff), and a
17
+ * pass before then defers it without trying, which counts no attempt. Within one pass, a
18
+ * sender that answered `429` is not tried again: the rest of its mail defers to the next pass.
19
+ *
20
+ * **At least once.** A send the provider accepted whose settle is then lost is sent again on
21
+ * the next pass. Mail has no idempotency key a provider honours across all of them, so this is
22
+ * the honest guarantee; the alternative — settle before sending — loses mail instead.
23
+ */
24
+ /** Attempts before a send that keeps failing transiently dead-letters. Well under the drain's ceiling. */
25
+ export declare const MAX_EMAIL_SEND_ATTEMPTS = 10;
26
+ /** The first retry's wait when the provider named none; doubled per attempt. */
27
+ export declare const EMAIL_RETRY_BASE_MS = 30000;
28
+ /** The longest wait between attempts the backoff alone will choose. */
29
+ export declare const EMAIL_RETRY_MAX_MS: number;
30
+ /** What a sender returns. `messageId` when the provider gives one. */
31
+ export interface EmailSent {
32
+ messageId?: string;
33
+ }
34
+ export interface SendEmailDeps {
35
+ /** Does this vertical hold the `emailSender` grant? Read per pass, so a revoke takes effect. */
36
+ mayEmail(vertical: string): Promise<boolean>;
37
+ /**
38
+ * Send as the platform's own address. A refusal throws an error carrying `status` (and
39
+ * `retryAfter` seconds for a throttle), read structurally.
40
+ */
41
+ sendAsPlatform(mail: Omit<SendEmailRequest, 'from' | 'attachments' | 'about'>): Promise<EmailSent>;
42
+ /**
43
+ * Send as a tenant address, through the connection that covers it. Same error contract; a
44
+ * wrapper that rewords a provider's failure keeps the original as `cause`.
45
+ */
46
+ sendAsTenant(input: {
47
+ tenantId: TenantId;
48
+ scopeId: ScopeId;
49
+ vertical: string;
50
+ from: string;
51
+ fromName?: string;
52
+ to: string;
53
+ subject: string;
54
+ html: string;
55
+ text: string;
56
+ attachmentIds: string[];
57
+ }): Promise<EmailSent>;
58
+ now?(): Date;
59
+ maxAttempts?: number;
60
+ }
61
+ /** One handler per drain pass: the throttles it learns last only as long as the pass. */
62
+ export declare function sendEmailHandler(deps: SendEmailDeps): PlatformRequestHandler;
63
+ /**
64
+ * The status that decides a retry: the provider's, when a wrapper kept it as `cause`, else the
65
+ * wrapper's own. A tenant send that failed in the provider surfaces as a 502 around the
66
+ * provider's answer — the 502 says "the provider failed", the cause says whether it will again.
67
+ */
68
+ export declare function sendFailureStatus(e: unknown): number | undefined;
69
+ /** `retryAfter` (seconds) wherever along the cause chain a sender put it; 0 when nobody did. */
70
+ export declare function retryAfterSecondsOf(e: unknown): number;
71
+ //# sourceMappingURL=email-intent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email-intent.d.ts","sourceRoot":"","sources":["../src/email-intent.ts"],"names":[],"mappings":"AAAA,OAAO,EAUL,KAAK,gBAAgB,EACrB,KAAK,QAAQ,EACb,KAAK,OAAO,EACb,MAAM,yBAAyB,CAAC;AAEjC,OAAO,KAAK,EAAE,sBAAsB,EAA0B,MAAM,qBAAqB,CAAC;AAG1F;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,0GAA0G;AAC1G,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAC1C,gFAAgF;AAChF,eAAO,MAAM,mBAAmB,QAAS,CAAC;AAC1C,uEAAuE;AACvE,eAAO,MAAM,kBAAkB,QAAc,CAAC;AAE9C,sEAAsE;AACtE,MAAM,WAAW,SAAS;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,gGAAgG;IAChG,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC7C;;;OAGG;IACH,cAAc,CAAC,IAAI,EAAE,IAAI,CAAC,gBAAgB,EAAE,MAAM,GAAG,aAAa,GAAG,OAAO,CAAC,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;IACnG;;;OAGG;IACH,YAAY,CAAC,KAAK,EAAE;QAClB,QAAQ,EAAE,QAAQ,CAAC;QACnB,OAAO,EAAE,OAAO,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,EAAE,EAAE,MAAM,CAAC;QACX,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;QACb,aAAa,EAAE,MAAM,EAAE,CAAC;KACzB,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;IACvB,GAAG,CAAC,IAAI,IAAI,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAOD,yFAAyF;AACzF,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,aAAa,GAAG,sBAAsB,CAgG5E;AAoDD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAEhE;AAED,gGAAgG;AAChG,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,CAQtD"}
@@ -0,0 +1,186 @@
1
+ import { EMAIL_DEAD_LETTERED, EMAIL_REFUSED, EMAIL_SENT, PLATFORM_REQUEST_ENTITY_TYPE, sendEmailRequest, } from '@substrat-run/contracts';
2
+ import { attributeFailure } from './failure-attribution.js';
3
+ import { isTerminalDispatchFailure, providerErrorStatus } from './provider-error.js';
4
+ /**
5
+ * The drain's handler for `send-email` (#2102): one mail a vertical requested inside an
6
+ * operation, delivered after that operation committed.
7
+ *
8
+ * It routes as the synchronous relay does — no `from` sends as the platform, a `from` sends
9
+ * through the tenant connection whose `MailSender` covers that address — and it retries what a
10
+ * provider deferred: a `429`, a `5xx`, or a failure with no status at all. Any other 4xx is a
11
+ * refusal and is final. Each outcome that ends the request is written into the scope as an
12
+ * event, with the provider's message id when it gives one.
13
+ *
14
+ * **Retries wait.** The drain runs on every kick and every sweep, and a kick follows any
15
+ * operation that requested something — so a throttled mailbox would otherwise be retried as
16
+ * fast as the vertical writes. A failed attempt records the earliest time to try again in the
17
+ * intent's `result` (`Retry-After` when the provider gave one, else a doubling backoff), and a
18
+ * pass before then defers it without trying, which counts no attempt. Within one pass, a
19
+ * sender that answered `429` is not tried again: the rest of its mail defers to the next pass.
20
+ *
21
+ * **At least once.** A send the provider accepted whose settle is then lost is sent again on
22
+ * the next pass. Mail has no idempotency key a provider honours across all of them, so this is
23
+ * the honest guarantee; the alternative — settle before sending — loses mail instead.
24
+ */
25
+ /** Attempts before a send that keeps failing transiently dead-letters. Well under the drain's ceiling. */
26
+ export const MAX_EMAIL_SEND_ATTEMPTS = 10;
27
+ /** The first retry's wait when the provider named none; doubled per attempt. */
28
+ export const EMAIL_RETRY_BASE_MS = 30_000;
29
+ /** The longest wait between attempts the backoff alone will choose. */
30
+ export const EMAIL_RETRY_MAX_MS = 30 * 60_000;
31
+ /** One handler per drain pass: the throttles it learns last only as long as the pass. */
32
+ export function sendEmailHandler(deps) {
33
+ const throttledUntil = new Map();
34
+ const now = () => deps.now?.() ?? new Date();
35
+ const maxAttempts = deps.maxAttempts ?? MAX_EMAIL_SEND_ATTEMPTS;
36
+ return async (ctx, request) => {
37
+ const attempts = request.attempts + 1;
38
+ const parsed = sendEmailRequest.safeParse(request.payload);
39
+ if (!parsed.success) {
40
+ // Module code goes through `requestEmail`, which parses first; this is a row written some
41
+ // other way. Final: the same bytes fail the same parse on every pass.
42
+ return over(request, EMAIL_REFUSED, null, 'platform', attempts, {
43
+ error: `not sent: the send-email payload is invalid (${parsed.error.issues[0]?.message ?? 'unknown'})`,
44
+ failure: { origin: 'platform', code: 'validation_failed', permission: null },
45
+ });
46
+ }
47
+ const mail = parsed.data;
48
+ const about = mail.about ?? null;
49
+ const sender = mail.from === undefined ? 'platform' : 'tenant';
50
+ const waitUntil = retryAtOf(request);
51
+ if (waitUntil !== null && waitUntil > now().getTime())
52
+ return { status: 'deferred' };
53
+ const key = mail.from === undefined ? 'platform' : `tenant:${mail.from.toLowerCase()}`;
54
+ if ((throttledUntil.get(key) ?? 0) > now().getTime())
55
+ return { status: 'deferred' };
56
+ if (!(await deps.mayEmail(ctx.vertical))) {
57
+ return over(request, EMAIL_REFUSED, about, sender, attempts, {
58
+ error: `not sent: vertical '${ctx.vertical}' does not hold the email-sender capability — staff ` +
59
+ 'grants it in the console (setVerticalEmailSender)',
60
+ failure: { origin: 'platform', code: 'permission_denied', permission: null },
61
+ });
62
+ }
63
+ let sent;
64
+ try {
65
+ sent =
66
+ mail.from === undefined
67
+ ? await deps.sendAsPlatform({
68
+ to: mail.to,
69
+ subject: mail.subject,
70
+ html: mail.html,
71
+ text: mail.text,
72
+ ...(mail.fromName ? { fromName: mail.fromName } : {}),
73
+ })
74
+ : await deps.sendAsTenant({
75
+ tenantId: ctx.tenantId,
76
+ scopeId: ctx.scopeId,
77
+ vertical: ctx.vertical,
78
+ from: mail.from,
79
+ ...(mail.fromName ? { fromName: mail.fromName } : {}),
80
+ to: mail.to,
81
+ subject: mail.subject,
82
+ html: mail.html,
83
+ text: mail.text,
84
+ attachmentIds: (mail.attachments ?? []).map((a) => a.attachmentId),
85
+ });
86
+ }
87
+ catch (e) {
88
+ const status = sendFailureStatus(e);
89
+ const failure = attributeFailure(innermost(e));
90
+ const error = e instanceof Error ? e.message : String(e);
91
+ if (status !== undefined && isTerminalDispatchFailure({ status })) {
92
+ return over(request, EMAIL_REFUSED, about, sender, attempts, { error, failure, status });
93
+ }
94
+ if (attempts >= maxAttempts) {
95
+ return over(request, EMAIL_DEAD_LETTERED, about, sender, attempts, {
96
+ error: `gave up after ${attempts} attempts — last error: ${error}`,
97
+ failure,
98
+ status,
99
+ });
100
+ }
101
+ const retryAfterMs = retryAfterSecondsOf(e) * 1000;
102
+ if (status === 429)
103
+ throttledUntil.set(key, now().getTime() + Math.max(retryAfterMs, 1));
104
+ const backoff = Math.min(EMAIL_RETRY_BASE_MS * 2 ** (attempts - 1), EMAIL_RETRY_MAX_MS);
105
+ const retry = { retryAt: new Date(now().getTime() + Math.max(retryAfterMs, backoff)).toISOString() };
106
+ return { status: 'pending', result: retry, error, failure };
107
+ }
108
+ return {
109
+ status: 'done',
110
+ result: { sent: true, sender, messageId: sent.messageId ?? null },
111
+ event: {
112
+ type: EMAIL_SENT,
113
+ entity: entityOf(request, about),
114
+ payload: {
115
+ request: request.id,
116
+ about,
117
+ sender,
118
+ messageId: sent.messageId ?? null,
119
+ attempts,
120
+ code: null,
121
+ status: null,
122
+ },
123
+ },
124
+ };
125
+ };
126
+ }
127
+ /** A terminal outcome and the event that says so. */
128
+ function over(request, type, about, sender, attempts, why) {
129
+ return {
130
+ status: 'failed',
131
+ error: why.error,
132
+ failure: why.failure,
133
+ event: {
134
+ type,
135
+ entity: entityOf(request, about),
136
+ payload: {
137
+ request: request.id,
138
+ about,
139
+ sender,
140
+ messageId: null,
141
+ attempts,
142
+ code: why.failure.code,
143
+ status: why.status ?? null,
144
+ },
145
+ },
146
+ };
147
+ }
148
+ const entityOf = (request, about) => about ?? { entityType: PLATFORM_REQUEST_ENTITY_TYPE, entityId: request.id };
149
+ function retryAtOf(request) {
150
+ const at = request.result?.retryAt;
151
+ if (typeof at !== 'string')
152
+ return null;
153
+ const ms = Date.parse(at);
154
+ return Number.isFinite(ms) ? ms : null;
155
+ }
156
+ /** The error a wrapper kept as `cause`, followed to the bottom: the provider's own, if any. */
157
+ function innermost(e) {
158
+ let current = e;
159
+ for (let depth = 0; depth < 5; depth++) {
160
+ const cause = current?.cause;
161
+ if (cause === undefined || cause === null)
162
+ return current;
163
+ current = cause;
164
+ }
165
+ return current;
166
+ }
167
+ /**
168
+ * The status that decides a retry: the provider's, when a wrapper kept it as `cause`, else the
169
+ * wrapper's own. A tenant send that failed in the provider surfaces as a 502 around the
170
+ * provider's answer — the 502 says "the provider failed", the cause says whether it will again.
171
+ */
172
+ export function sendFailureStatus(e) {
173
+ return providerErrorStatus(innermost(e)) ?? providerErrorStatus(e);
174
+ }
175
+ /** `retryAfter` (seconds) wherever along the cause chain a sender put it; 0 when nobody did. */
176
+ export function retryAfterSecondsOf(e) {
177
+ let current = e;
178
+ for (let depth = 0; depth < 6 && current !== null && typeof current === 'object'; depth++) {
179
+ const value = current.retryAfter;
180
+ if (typeof value === 'number' && Number.isFinite(value) && value > 0)
181
+ return value;
182
+ current = current.cause;
183
+ }
184
+ return 0;
185
+ }
186
+ //# sourceMappingURL=email-intent.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email-intent.js","sourceRoot":"","sources":["../src/email-intent.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,44 @@
1
+ import { type FieldCoverageScope } from './field-coverage-tally.js';
2
+ export interface FieldCoverageWindow {
3
+ since: string;
4
+ until: string;
5
+ }
6
+ /** One row is a count of sampled SERIALISATIONS, with its provenance beside the numbers. */
7
+ export interface ObservedFieldRow {
8
+ field: string;
9
+ present: number;
10
+ empty: number;
11
+ absent: number;
12
+ /** Present or null; absent optional fields do not enter this denominator. */
13
+ eligible: number;
14
+ sampleRate: number | null;
15
+ window: FieldCoverageWindow;
16
+ }
17
+ export interface FieldCoverageRead {
18
+ tenantId: string;
19
+ vertical: string;
20
+ versionId: string | null;
21
+ source: 'vertical-asserted';
22
+ window: FieldCoverageWindow;
23
+ /** Empty means the router armed no request in this window. */
24
+ groups: Array<{
25
+ sampleRate: number | null;
26
+ armedRequests: number;
27
+ operations: Array<{
28
+ operation: string;
29
+ responses: number;
30
+ fields: ObservedFieldRow[];
31
+ }>;
32
+ /** Zero present and zero null in an armed sample: a candidate, never a proof of no use. */
33
+ notObservedReturned: Array<{
34
+ operation: string;
35
+ field: string;
36
+ }>;
37
+ refused: number;
38
+ }>;
39
+ }
40
+ /** The only public read shape over the raw report: declared names, counts, and no request detail. */
41
+ export declare function readFieldCoverage(reports: Iterable<unknown>, routerLines: Iterable<unknown>, scope: FieldCoverageScope & {
42
+ versionId?: string;
43
+ }, window: FieldCoverageWindow): FieldCoverageRead;
44
+ //# sourceMappingURL=field-coverage-read.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"field-coverage-read.d.ts","sourceRoot":"","sources":["../src/field-coverage-read.ts"],"names":[],"mappings":"AAAA,OAAO,EAA4B,KAAK,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAE9F,MAAM,WAAW,mBAAmB;IAAG,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE;AAErE,4FAA4F;AAC5F,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,6EAA6E;IAC7E,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,MAAM,EAAE,mBAAmB,CAAC;CAC7B;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,MAAM,EAAE,mBAAmB,CAAC;IAC5B,MAAM,EAAE,mBAAmB,CAAC;IAC5B,8DAA8D;IAC9D,MAAM,EAAE,KAAK,CAAC;QACZ,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;QAC1B,aAAa,EAAE,MAAM,CAAC;QACtB,UAAU,EAAE,KAAK,CAAC;YAAE,SAAS,EAAE,MAAM,CAAC;YAAC,SAAS,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,gBAAgB,EAAE,CAAA;SAAE,CAAC,CAAC;QACxF,2FAA2F;QAC3F,mBAAmB,EAAE,KAAK,CAAC;YAAE,SAAS,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;QACjE,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC,CAAC;CACJ;AAED,qGAAqG;AACrG,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,QAAQ,CAAC,OAAO,CAAC,EAC1B,WAAW,EAAE,QAAQ,CAAC,OAAO,CAAC,EAC9B,KAAK,EAAE,kBAAkB,GAAG;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,EAClD,MAAM,EAAE,mBAAmB,GAC1B,iBAAiB,CA0CnB"}
@@ -0,0 +1,45 @@
1
+ import { tallyFieldCoverageByRate } from './field-coverage-tally.js';
2
+ /** The only public read shape over the raw report: declared names, counts, and no request detail. */
3
+ export function readFieldCoverage(reports, routerLines, scope, window) {
4
+ const groups = tallyFieldCoverageByRate(reports, routerLines, scope).map((group) => {
5
+ const byOperation = new Map(group.operations.map((op) => [op.operation, op]));
6
+ const declared = scope.declared ?? {};
7
+ const operations = Object.entries(declared).map(([operation, fields]) => {
8
+ const observed = byOperation.get(operation);
9
+ const byField = new Map(observed?.fields.map((f) => [f.field, f]) ?? []);
10
+ return {
11
+ operation,
12
+ responses: observed?.responses ?? 0,
13
+ fields: fields.map((field) => {
14
+ const count = byField.get(field);
15
+ const present = count?.present ?? 0;
16
+ const empty = count?.empty ?? 0;
17
+ return {
18
+ field, present, empty, absent: count?.absent ?? 0,
19
+ eligible: present + empty,
20
+ sampleRate: group.sampleRate,
21
+ window,
22
+ };
23
+ }),
24
+ };
25
+ });
26
+ return {
27
+ sampleRate: group.sampleRate,
28
+ armedRequests: group.armedRequests,
29
+ operations,
30
+ notObservedReturned: operations.flatMap((op) => op.responses > 0
31
+ ? op.fields.filter((f) => f.eligible === 0).map((f) => ({ operation: op.operation, field: f.field }))
32
+ : []),
33
+ refused: group.refused,
34
+ };
35
+ });
36
+ return {
37
+ tenantId: scope.tenantId,
38
+ vertical: scope.vertical,
39
+ versionId: scope.versionId ?? null,
40
+ source: 'vertical-asserted',
41
+ window,
42
+ groups,
43
+ };
44
+ }
45
+ //# sourceMappingURL=field-coverage-read.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"field-coverage-read.js","sourceRoot":"","sources":["../src/field-coverage-read.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAA2B,MAAM,2BAA2B,CAAC;AAiC9F,qGAAqG;AACrG,MAAM,UAAU,iBAAiB,CAC/B,OAA0B,EAC1B,WAA8B,EAC9B,KAAkD,EAClD,MAA2B;IAE3B,MAAM,MAAM,GAAG,wBAAwB,CAAC,OAAO,EAAE,WAAW,EAAE,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;QACjF,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;QAC9E,MAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ,IAAI,EAAE,CAAC;QACtC,MAAM,UAAU,GAAG,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,EAAE,MAAM,CAAC,EAAE,EAAE;YACtE,MAAM,QAAQ,GAAG,WAAW,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;YAC5C,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;YACzE,OAAO;gBACL,SAAS;gBACT,SAAS,EAAE,QAAQ,EAAE,SAAS,IAAI,CAAC;gBACnC,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAoB,EAAE;oBAC7C,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;oBACjC,MAAM,OAAO,GAAG,KAAK,EAAE,OAAO,IAAI,CAAC,CAAC;oBACpC,MAAM,KAAK,GAAG,KAAK,EAAE,KAAK,IAAI,CAAC,CAAC;oBAChC,OAAO;wBACL,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,IAAI,CAAC;wBACjD,QAAQ,EAAE,OAAO,GAAG,KAAK;wBACzB,UAAU,EAAE,KAAK,CAAC,UAAU;wBAC5B,MAAM;qBACP,CAAC;gBACJ,CAAC,CAAC;aACH,CAAC;QACJ,CAAC,CAAC,CAAC;QACH,OAAO;YACL,UAAU,EAAE,KAAK,CAAC,UAAU;YAC5B,aAAa,EAAE,KAAK,CAAC,aAAa;YAClC,UAAU;YACV,mBAAmB,EAAE,UAAU,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,CAC7C,EAAE,CAAC,SAAS,GAAG,CAAC;gBACd,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,SAAS,EAAE,EAAE,CAAC,SAAS,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;gBACrG,CAAC,CAAC,EAAE,CAAC;YACT,OAAO,EAAE,KAAK,CAAC,OAAO;SACvB,CAAC;IACJ,CAAC,CAAC,CAAC;IACH,OAAO;QACL,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,SAAS,EAAE,KAAK,CAAC,SAAS,IAAI,IAAI;QAClC,MAAM,EAAE,mBAAmB;QAC3B,MAAM;QACN,MAAM;KACP,CAAC;AACJ,CAAC"}
@@ -27,12 +27,22 @@ export interface FieldCoverageTally {
27
27
  */
28
28
  refused: number;
29
29
  }
30
+ /** Counts for router-armed requests at one effective rate. Null means an older router did not record it. */
31
+ export interface RatedFieldCoverageTally extends FieldCoverageTally {
32
+ sampleRate: number | null;
33
+ armedRequests: number;
34
+ }
30
35
  /** What the tally is for. Every field is the CALLER's resolution, never a line's. */
31
36
  export interface FieldCoverageScope {
32
37
  /** The tenant, forced from the principal the way every tenant read forces it. */
33
38
  tenantId: string;
34
39
  /** The app's vertical slug, as the router resolves it onto its line (`vertical`). */
35
40
  vertical: string;
41
+ /**
42
+ * The one installed app (`scopeId` on the router's line), when the read is for a single
43
+ * scope. Omitted, every scope of this vertical in the tenant counts.
44
+ */
45
+ scopeId?: string;
36
46
  /** The app's script family stems. Empty names no script, so nothing counts. */
37
47
  services: readonly string[];
38
48
  /**
@@ -55,4 +65,6 @@ export declare const FIELD_COVERAGE_OPERATIONS_MAX = 1000;
55
65
  * the tally is the thing a view trusts, and a query is one forgotten parameter from fleet-wide.
56
66
  */
57
67
  export declare function tallyFieldCoverage(events: Iterable<unknown>, routerEvents: Iterable<unknown>, scope: FieldCoverageScope): FieldCoverageTally;
68
+ /** Partition by the router's own rate before tallying, so a changed rate is never averaged away. */
69
+ export declare function tallyFieldCoverageByRate(events: Iterable<unknown>, routerEvents: Iterable<unknown>, scope: FieldCoverageScope): RatedFieldCoverageTally[];
58
70
  //# sourceMappingURL=field-coverage-tally.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"field-coverage-tally.d.ts","sourceRoot":"","sources":["../src/field-coverage-tally.ts"],"names":[],"mappings":"AAuDA,8EAA8E;AAC9E,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,4BAA4B;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,yBAAyB;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,mBAAmB;IACnB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,mFAAmF;AACnF,MAAM,WAAW,sBAAsB;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,mFAAmF;IACnF,MAAM,EAAE,kBAAkB,EAAE,CAAC;CAC9B;AAED,MAAM,WAAW,kBAAkB;IACjC,8FAA8F;IAC9F,QAAQ,EAAE,MAAM,CAAC;IACjB,yBAAyB;IACzB,UAAU,EAAE,sBAAsB,EAAE,CAAC;IACrC;;;;OAIG;IACH,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,qFAAqF;AACrF,MAAM,WAAW,kBAAkB;IACjC,iFAAiF;IACjF,QAAQ,EAAE,MAAM,CAAC;IACjB,qFAAqF;IACrF,QAAQ,EAAE,MAAM,CAAC;IACjB,+EAA+E;IAC/E,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B;;;OAGG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC,CAAC;CACxD;AAgCD;;;GAGG;AACH,eAAO,MAAM,6BAA6B,OAAO,CAAC;AAyBlD;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,QAAQ,CAAC,OAAO,CAAC,EACzB,YAAY,EAAE,QAAQ,CAAC,OAAO,CAAC,EAC/B,KAAK,EAAE,kBAAkB,GACxB,kBAAkB,CAqGpB"}
1
+ {"version":3,"file":"field-coverage-tally.d.ts","sourceRoot":"","sources":["../src/field-coverage-tally.ts"],"names":[],"mappings":"AAuDA,8EAA8E;AAC9E,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,4BAA4B;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,yBAAyB;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,mBAAmB;IACnB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,mFAAmF;AACnF,MAAM,WAAW,sBAAsB;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,mFAAmF;IACnF,MAAM,EAAE,kBAAkB,EAAE,CAAC;CAC9B;AAED,MAAM,WAAW,kBAAkB;IACjC,8FAA8F;IAC9F,QAAQ,EAAE,MAAM,CAAC;IACjB,yBAAyB;IACzB,UAAU,EAAE,sBAAsB,EAAE,CAAC;IACrC;;;;OAIG;IACH,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,4GAA4G;AAC5G,MAAM,WAAW,uBAAwB,SAAQ,kBAAkB;IACjE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,qFAAqF;AACrF,MAAM,WAAW,kBAAkB;IACjC,iFAAiF;IACjF,QAAQ,EAAE,MAAM,CAAC;IACjB,qFAAqF;IACrF,QAAQ,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+EAA+E;IAC/E,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B;;;OAGG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC,CAAC;CACxD;AA0CD;;;GAGG;AACH,eAAO,MAAM,6BAA6B,OAAO,CAAC;AAyBlD;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,QAAQ,CAAC,OAAO,CAAC,EACzB,YAAY,EAAE,QAAQ,CAAC,OAAO,CAAC,EAC/B,KAAK,EAAE,kBAAkB,GACxB,kBAAkB,CAqGpB;AAED,oGAAoG;AACpG,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,QAAQ,CAAC,OAAO,CAAC,EACzB,YAAY,EAAE,QAAQ,CAAC,OAAO,CAAC,EAC/B,KAAK,EAAE,kBAAkB,GACxB,uBAAuB,EAAE,CA2B3B"}
@@ -66,23 +66,33 @@ function serviceOf(event) {
66
66
  function dispatchIdsFor(routerEvents, scope) {
67
67
  const ids = new Set();
68
68
  for (const event of routerEvents) {
69
- if (event === null || typeof event !== 'object')
70
- continue;
71
- const service = serviceOf(event);
72
- if (typeof service !== 'string' || !ROUTER_SCRIPT_NAMES.includes(service))
73
- continue;
74
- const source = event['source'];
75
- if (source === null || typeof source !== 'object')
76
- continue;
77
- const line = source;
78
- if (line['router'] !== 'request' || line['tenantId'] !== scope.tenantId || line['vertical'] !== scope.vertical)
79
- continue;
80
- const id = line[FIELD_COVERAGE_ID_FIELD];
81
- if (typeof id === 'string' && DISPATCH_ID.test(id))
82
- ids.add(id);
69
+ const vouched = routerDispatch(event, scope);
70
+ if (vouched)
71
+ ids.add(vouched.id);
83
72
  }
84
73
  return ids;
85
74
  }
75
+ /**
76
+ * One router request line that vouches for a dispatch in `scope`, or `undefined`. Tenant, app
77
+ * and scope are read off the ROUTER's line only, never off the vertical's report.
78
+ */
79
+ function routerDispatch(event, scope) {
80
+ if (event === null || typeof event !== 'object')
81
+ return undefined;
82
+ const service = serviceOf(event);
83
+ if (typeof service !== 'string' || !ROUTER_SCRIPT_NAMES.includes(service))
84
+ return undefined;
85
+ const source = event['source'];
86
+ if (source === null || typeof source !== 'object')
87
+ return undefined;
88
+ const line = source;
89
+ if (line['router'] !== 'request' || line['tenantId'] !== scope.tenantId || line['vertical'] !== scope.vertical)
90
+ return undefined;
91
+ if (scope.scopeId !== undefined && line['scopeId'] !== scope.scopeId)
92
+ return undefined;
93
+ const id = line[FIELD_COVERAGE_ID_FIELD];
94
+ return typeof id === 'string' && DISPATCH_ID.test(id) ? { id, line } : undefined;
95
+ }
86
96
  /**
87
97
  * How many distinct operations one tally keeps. Past it, a report on a new operation is
88
98
  * refused: a vertical cannot grow the answer without bound by inventing names.
@@ -231,4 +241,35 @@ export function tallyFieldCoverage(events, routerEvents, scope) {
231
241
  refused,
232
242
  };
233
243
  }
244
+ /** Partition by the router's own rate before tallying, so a changed rate is never averaged away. */
245
+ export function tallyFieldCoverageByRate(events, routerEvents, scope) {
246
+ const reports = [...events];
247
+ const groups = new Map();
248
+ const ids = new Map();
249
+ const assigned = new Set();
250
+ for (const event of routerEvents) {
251
+ const vouched = routerDispatch(event, scope);
252
+ if (!vouched)
253
+ continue;
254
+ const { id, line } = vouched;
255
+ // A duplicate router line cannot assign one response to several rates.
256
+ if (assigned.has(id))
257
+ continue;
258
+ assigned.add(id);
259
+ // The rate comes only from this router line, never from the vertical's report.
260
+ const candidate = line['fieldCoverageRate'];
261
+ const rate = typeof candidate === 'number' && Number.isFinite(candidate) && candidate > 0 && candidate <= 1
262
+ ? candidate : null;
263
+ const group = groups.get(rate) ?? [];
264
+ group.push(event);
265
+ groups.set(rate, group);
266
+ const known = ids.get(rate) ?? new Set();
267
+ known.add(id);
268
+ ids.set(rate, known);
269
+ }
270
+ return [...groups].map(([sampleRate, vouched]) => {
271
+ const counted = tallyFieldCoverage(reports, vouched, scope);
272
+ return { ...counted, sampleRate, armedRequests: ids.get(sampleRate).size };
273
+ });
274
+ }
234
275
  //# sourceMappingURL=field-coverage-tally.js.map