@pipeline-builder/api-core 5.2.3 → 5.2.4

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 (81) hide show
  1. package/lib/constants/ai-providers.d.ts +3 -22
  2. package/lib/constants/ai-providers.js +6 -56
  3. package/lib/errors/app-errors.d.ts +14 -0
  4. package/lib/errors/app-errors.js +21 -1
  5. package/lib/helpers/sse-helpers.d.ts +33 -3
  6. package/lib/helpers/sse-helpers.js +91 -19
  7. package/lib/middleware/auth.js +6 -3
  8. package/lib/middleware/deferred-quota-check.d.ts +5 -0
  9. package/lib/middleware/deferred-quota-check.js +33 -0
  10. package/lib/middleware/ecosystem-guard.d.ts +3 -3
  11. package/lib/middleware/ecosystem-guard.js +5 -5
  12. package/lib/middleware/index.d.ts +5 -2
  13. package/lib/middleware/index.js +6 -3
  14. package/lib/middleware/on-behalf.d.ts +37 -0
  15. package/lib/middleware/on-behalf.js +143 -0
  16. package/lib/middleware/permission-gates.d.ts +11 -0
  17. package/lib/middleware/permission-gates.js +7 -1
  18. package/lib/middleware/quota-exempt-routes.d.ts +36 -0
  19. package/lib/middleware/quota-exempt-routes.js +47 -0
  20. package/lib/middleware/route-table.d.ts +104 -0
  21. package/lib/middleware/route-table.js +72 -2
  22. package/lib/middleware/service-tokens.d.ts +8 -0
  23. package/lib/middleware/service-tokens.js +2 -1
  24. package/lib/middleware/step-up.js +11 -1
  25. package/lib/services/ai-call-weight.d.ts +24 -0
  26. package/lib/services/ai-call-weight.js +77 -0
  27. package/lib/services/audit-context.d.ts +105 -0
  28. package/lib/services/audit-context.js +134 -0
  29. package/lib/services/index.d.ts +7 -2
  30. package/lib/services/index.js +7 -2
  31. package/lib/services/org-ai-client.d.ts +61 -0
  32. package/lib/services/org-ai-client.js +112 -0
  33. package/lib/services/quota-increment-batcher.d.ts +81 -0
  34. package/lib/services/quota-increment-batcher.js +166 -0
  35. package/lib/services/quota.d.ts +158 -14
  36. package/lib/services/quota.js +252 -22
  37. package/lib/services/remote-audit-client.d.ts +44 -1
  38. package/lib/services/remote-audit-client.js +106 -20
  39. package/lib/services/version-vuln-policy.d.ts +174 -0
  40. package/lib/services/version-vuln-policy.js +242 -0
  41. package/lib/types/ai-catalog.d.ts +63 -0
  42. package/lib/types/ai-catalog.js +82 -0
  43. package/lib/types/ask-proposals.js +6 -6
  44. package/lib/types/build-catalog.d.ts +154 -0
  45. package/lib/types/build-catalog.js +158 -0
  46. package/lib/types/common.d.ts +37 -3
  47. package/lib/types/common.js +20 -2
  48. package/lib/types/ecosystem-notifications.d.ts +5 -5
  49. package/lib/types/ecosystem-notifications.js +9 -3
  50. package/lib/types/error-codes.d.ts +13 -6
  51. package/lib/types/error-codes.js +18 -8
  52. package/lib/types/feature-flags.d.ts +6 -4
  53. package/lib/types/feature-flags.js +11 -6
  54. package/lib/types/index.d.ts +6 -4
  55. package/lib/types/index.js +12 -5
  56. package/lib/types/metadata-keys.d.ts +6 -0
  57. package/lib/types/metadata-keys.js +4 -1
  58. package/lib/types/permissions.d.ts +344 -3
  59. package/lib/types/permissions.js +596 -21
  60. package/lib/types/pipeline.d.ts +39 -0
  61. package/lib/types/pipeline.js +57 -1
  62. package/lib/types/plugin-health.d.ts +0 -2
  63. package/lib/types/plugin-health.js +1 -11
  64. package/lib/types/plugin-scan.d.ts +192 -42
  65. package/lib/types/plugin-scan.js +159 -43
  66. package/lib/types/quota-catalog.d.ts +247 -0
  67. package/lib/types/quota-catalog.js +122 -0
  68. package/lib/types/quota-tiers.d.ts +118 -0
  69. package/lib/types/quota-tiers.js +153 -2
  70. package/lib/types/remote-audit-actions.d.ts +1 -1
  71. package/lib/types/remote-audit-actions.js +131 -1
  72. package/lib/types/wire-vocabulary.js +8 -1
  73. package/lib/utils/index.d.ts +1 -1
  74. package/lib/utils/index.js +2 -2
  75. package/lib/utils/response.d.ts +35 -2
  76. package/lib/utils/response.js +42 -3
  77. package/lib/validation/ai-schemas.d.ts +27 -0
  78. package/lib/validation/ai-schemas.js +31 -5
  79. package/lib/validation/plugin-schemas.d.ts +15 -0
  80. package/lib/validation/plugin-schemas.js +28 -1
  81. package/package.json +31 -2
@@ -1,6 +1,107 @@
1
- import type { Response } from 'express';
2
- import type { QuotaType, QuotaCheckResult } from '../types/common.js';
1
+ import type { Request, Response } from 'express';
2
+ import { type MeteredIncrementGroup } from './quota-increment-batcher.js';
3
+ import type { PrincipalType, QuotaType, QuotaCheckResult } from '../types/common.js';
4
+ import { ErrorCode } from '../types/error-codes.js';
3
5
  import { type QuotaTier } from '../types/quota-tiers.js';
6
+ /**
7
+ * Budget for one `check` of a COUNTER quota (`QUOTA_CHECK_TIMEOUT_MS`, default
8
+ * 300), with no retry.
9
+ *
10
+ * The check is a read gate that FAILS OPEN, run ahead of the request it gates:
11
+ * the old 5 s timeout × 2 attempts meant a slow quota service added up to 10 s
12
+ * to every metered request and then let it through anyway. A short budget turns
13
+ * the same incident into a ≤ 300 ms fail-open (counted on
14
+ * `quota_fail_open_total`, which alerts) — and with the per-pod check cache in
15
+ * front (api-server `checkQuota`) a healthy service is asked at most once per
16
+ * org/type per cache TTL.
17
+ *
18
+ * `storageBytes` keeps the client timeout and one retry: it is a MEASURED read
19
+ * (the quota service walks the org's registry namespace on a cold cache) behind
20
+ * the registry's fail-CLOSED push gate, where a premature timeout refuses a push.
21
+ */
22
+ export declare const QUOTA_CHECK_TIMEOUT_MS: number;
23
+ /** Who / what spent (or got back) a quota unit. All fields optional. */
24
+ export interface QuotaUsageAttribution {
25
+ principalType?: PrincipalType;
26
+ /** The user id or service-account id behind the call (the token's `sub`). */
27
+ principalId?: string;
28
+ /** The revocable credential: the access key the token derives from, else its `jti`. */
29
+ keyId?: string;
30
+ /** `METHOD /route/:pattern` — the matched route PATTERN, never the raw URL (no ids, no query). */
31
+ route?: string;
32
+ /** The pipeline the call was made for (`:pipelineId` param or `X-Pipeline-Id` header). */
33
+ pipelineId?: string;
34
+ requestId?: string;
35
+ /**
36
+ * AI calls only: which product surface spent the unit — `ask`, `agent`,
37
+ * `agent-tool:<name>`, `pipeline-generate`, `plugin-generate`,
38
+ * `report-summary`. Set by api-server's `withAiCallReservation`.
39
+ */
40
+ feature?: string;
41
+ /** AI calls only: the model id the reservation was resolved to (never a key). */
42
+ model?: string;
43
+ /** AI calls only: whose key served the call — `platform`, `org` or `request` (BYOK). */
44
+ keySource?: AiKeySource;
45
+ }
46
+ /** Whose key serves an AI call (mirrors ai-core `KeySource`). Only `platform` spends `aiCalls`. */
47
+ export type AiKeySource = 'platform' | 'org' | 'request';
48
+ /** Token counts an AI call reports after it settles (see {@link QuotaService.reportUsage}). */
49
+ export interface QuotaTokenUsage {
50
+ tokensIn: number;
51
+ tokensOut: number;
52
+ }
53
+ /**
54
+ * Attribution for the request being served: the verified principal, the matched
55
+ * route pattern, the pipeline (`:pipelineId` param or a sanitized
56
+ * `X-Pipeline-Id` header) and the request id. Read off `req` directly rather than
57
+ * the ambient audit context because the metering middleware runs in a `finish`
58
+ * listener, where the request's async scope is no longer guaranteed.
59
+ */
60
+ export declare function quotaAttributionFromRequest(req: Request): QuotaUsageAttribution;
61
+ /**
62
+ * The quota type a metered request actually spends.
63
+ *
64
+ * `apiCalls` is the PEOPLE's budget — an interactive session. A request made by
65
+ * a NON-INTERACTIVE credential spends `automationCalls` instead, never both, so
66
+ * a fleet of self-mutating pipelines can't exhaust the console for the humans
67
+ * in the same org:
68
+ * - an org SERVICE ACCOUNT (`principalType: 'service_account'` —
69
+ * platform-automation, pipeline-register, reporting-ingest, registry-push);
70
+ * - a PERSONAL ACCESS KEY: the exchanged key token (`token_use: 'api_key'`)
71
+ * and anything minted while holding one (`parentKeyId`). A PAT in a CI job
72
+ * is automation whoever owns it — billing it as a person let one script
73
+ * lock its owner's teammates out of the console. CI should still use a
74
+ * service-account key (it survives the person leaving); either way the
75
+ * traffic now lands on `automationCalls`.
76
+ * Every other type is returned unchanged: an AI call or a pipeline creation
77
+ * costs the same whoever makes it.
78
+ *
79
+ * `meterQuotaOnSuccess` and `checkQuota` call this themselves, so a service
80
+ * mounts both with `'apiCalls'` and the split happens per request.
81
+ */
82
+ export declare function meteredQuotaTypeFor(req: Pick<Request, 'user'>, quotaType: QuotaType): QuotaType;
83
+ /** Why a request is not metered/checked against apiCalls (see {@link callQuotaExemption}). */
84
+ export type CallQuotaExemption = 'service' | 'impersonation' | 'on_behalf';
85
+ /**
86
+ * Whether the apiCalls/automationCalls meter AND check skip this (verified)
87
+ * request, and why. One function for both halves, so the gate never refuses a
88
+ * call the meter would not bill:
89
+ * - `service` — an internal service principal: peer traffic is caused by a
90
+ * user action the originating service already metered;
91
+ * - `impersonation` — a sysadmin impersonation session (`impersonatorId`).
92
+ * Vendor staff looking at a tenant's console must neither spend the tenant's
93
+ * budget nor be locked out by it while diagnosing an exhausted org; the
94
+ * session is audited on its own trail;
95
+ * - `on_behalf` — a user token FORWARDED by an internal service as part of a
96
+ * request already metered where it arrived (the ask agent's tools,
97
+ * pipeline's placeholder-plugin confirm), proven by a signed marker the
98
+ * user cannot forge (middleware/on-behalf.ts).
99
+ * Applies to the CALL budgets only: reservation-gated quotas (pipelines,
100
+ * plugins …) are charged whoever asks. `aiCalls` mirrors the impersonation half
101
+ * on its own (api-server `withAiCallReservation` skips the slot when the
102
+ * session is an impersonation; the call is still audited).
103
+ */
104
+ export declare function callQuotaExemption(req: Request): CallQuotaExemption | undefined;
4
105
  /**
5
106
  * Service-principal `Authorization` header for a quota call made on behalf of
6
107
  * `orgId` by the CURRENT service (`SERVICE_NAME`, the same identity the health
@@ -30,6 +131,16 @@ export interface QuotaReserveResult {
30
131
  * 503 instead of a misleading 429 "quota exceeded".
31
132
  */
32
133
  unavailable?: boolean;
134
+ /**
135
+ * Set when an `aiCalls` reservation was refused by one of the org's OPTIONAL
136
+ * AI controls rather than the period cap: {@link ErrorCode.AI_MEMBER_SHARE_EXCEEDED}
137
+ * (the caller's per-member share is spent) or
138
+ * {@link ErrorCode.AI_TOKEN_BUDGET_EXCEEDED} (the org's per-period token
139
+ * budget is spent). Paired with `exceeded: true` and the quota service's
140
+ * `message`; {@link sendQuotaReserveDenied} relays both.
141
+ */
142
+ code?: AiLimitCode;
143
+ message?: string;
33
144
  quota: {
34
145
  type: QuotaType;
35
146
  limit: number;
@@ -38,14 +149,32 @@ export interface QuotaReserveResult {
38
149
  resetAt?: string;
39
150
  };
40
151
  }
152
+ /** The 429 codes an `aiCalls` reservation answers when an optional org AI control refuses it. */
153
+ export type AiLimitCode = ErrorCode.AI_MEMBER_SHARE_EXCEEDED | ErrorCode.AI_TOKEN_BUDGET_EXCEEDED;
41
154
  /**
42
155
  * Quota service client interface.
43
156
  */
44
157
  export interface QuotaService {
45
158
  /** Check if quota is available (fail-open on error). */
46
159
  check(orgId: string, quotaType: QuotaType, authHeader: string, requestId?: string): Promise<QuotaCheckResult>;
47
- /** Increment quota usage. Returns a promise so callers can optionally handle errors. */
48
- increment(orgId: string, quotaType: QuotaType, authHeader: string, amount?: number, requestId?: string): Promise<void>;
160
+ /**
161
+ * Increment quota usage. Returns a promise so callers can optionally handle
162
+ * errors. `attribution` defaults to the ambient request context.
163
+ */
164
+ increment(orgId: string, quotaType: QuotaType, authHeader: string, amount?: number, attribution?: QuotaUsageAttribution): Promise<void>;
165
+ /**
166
+ * METERED increment of a pre-aggregated batch (the per-pod batcher's flush,
167
+ * see `quota-increment-batcher.ts`): the sum of `groups` is applied with
168
+ * PARTIAL admission — the counter moves by as much as fits under the cap and
169
+ * the remainder is recorded as `overshoot` — and each group's attribution is
170
+ * written to the usage ledger. Never a 429. Resolves with what was counted,
171
+ * or `null` when the quota service did not answer ok (counted on
172
+ * `quota_increment_failures_total`).
173
+ */
174
+ incrementMetered(orgId: string, quotaType: QuotaType, authHeader: string, groups: MeteredIncrementGroup[]): Promise<{
175
+ counted: number;
176
+ overshoot: number;
177
+ } | null>;
49
178
  /**
50
179
  * Atomic reserve the same atomic check+increment as `increment`, but
51
180
  * parses the response so callers can see whether the slot was actually
@@ -56,7 +185,7 @@ export interface QuotaService {
56
185
  * unreachable, timeout, open circuit, non-ok, or a non-quota 429 — unless
57
186
  * `QUOTA_RESERVE_FAIL_OPEN=true` (see the policy note at the top of this file).
58
187
  */
59
- reserve(orgId: string, quotaType: QuotaType, authHeader: string, amount?: number, requestId?: string): Promise<QuotaReserveResult>;
188
+ reserve(orgId: string, quotaType: QuotaType, authHeader: string, amount?: number, attribution?: QuotaUsageAttribution): Promise<QuotaReserveResult>;
60
189
  /**
61
190
  * Roll back a previously reserved slot. Fire-and-forget — never throws.
62
191
  * Pass `resetAtSnapshot` (the `quota.resetAt` observed when the slot was
@@ -64,7 +193,16 @@ export interface QuotaService {
64
193
  * between reserve and rollback, the server skips the decrement so it doesn't
65
194
  * steal capacity from the new period.
66
195
  */
67
- decrement(orgId: string, quotaType: QuotaType, authHeader: string, amount?: number, resetAtSnapshot?: string, requestId?: string): Promise<void>;
196
+ decrement(orgId: string, quotaType: QuotaType, authHeader: string, amount?: number, resetAtSnapshot?: string, attribution?: QuotaUsageAttribution): Promise<void>;
197
+ /**
198
+ * Record what an AI call COST after it settled — its token counts — in the
199
+ * usage ledger, without moving any counter (a zero-unit ledger row carrying
200
+ * `tokensIn` / `tokensOut` under the call's attribution). Also how a call that
201
+ * spent NO `aiCalls` unit (a BYOK key, an impersonation session, a weight-0
202
+ * model) still shows up in the usage breakdown. Fire-and-forget — never throws.
203
+ * Optional so test doubles need not implement it.
204
+ */
205
+ reportUsage?(orgId: string, quotaType: QuotaType, authHeader: string, tokens: QuotaTokenUsage, attribution?: QuotaUsageAttribution): Promise<void>;
68
206
  /**
69
207
  * Get the org's quota `QuotaTier`. Used by the plugin-build queue partitioning
70
208
  * to route a build to the right per-tier queue. Fail-open returns `DEFAULT_TIER`
@@ -116,20 +254,26 @@ export interface QuotaServiceConfig {
116
254
  */
117
255
  export declare function createQuotaService(config?: QuotaServiceConfig): QuotaService;
118
256
  /**
119
- * Fire-and-forget quota METERING increment with standardized error logging.
257
+ * Fire-and-forget quota METERING increment.
258
+ *
259
+ * Buffered: the unit joins the pod's per-(org, type) aggregate and is sent with
260
+ * the next flush (`QUOTA_INCREMENT_FLUSH_MS`, see quota-increment-batcher.ts)
261
+ * as one metered increment — partial admission against the cap, the excess
262
+ * recorded as `overshoot`. The counter therefore lags by at most one flush
263
+ * interval. Only the request meter uses this; reservations stay synchronous.
120
264
  *
121
265
  * Authenticates as the calling SERVICE ({@link getQuotaServiceAuthHeader}), never
122
- * with the end user's token: `/quotas/:orgId/increment` is service-principal /
123
- * system-admin only, so a forwarded user JWT was always rejected (403) and the
124
- * metered quota (e.g. `apiCalls`, and the `api_pack` bundle raising it) never
125
- * moved.
266
+ * with the end user's token: `/quotas/:orgId/increment` is service-principal
267
+ * only, so a forwarded user JWT was always rejected (403) and the metered quota
268
+ * (e.g. `apiCalls`, and the `api_pack` bundle raising it) never moved.
126
269
  *
127
270
  * @param quotaService - Quota service client
128
271
  * @param orgId - Organization ID whose counter is incremented
129
272
  * @param quotaType - Quota type to increment
130
273
  * @param logWarn - Logging function for warnings
274
+ * @param attribution - Who spent the unit (defaults to the ambient request context)
131
275
  */
132
- export declare function incrementQuota(quotaService: QuotaService, orgId: string, quotaType: QuotaType, logWarn: (message: string, data?: unknown) => void): void;
276
+ export declare function incrementQuota(quotaService: QuotaService, orgId: string, quotaType: QuotaType, logWarn: (message: string, data?: unknown) => void, attribution?: QuotaUsageAttribution): void;
133
277
  /**
134
278
  * Atomic reserve helper for the "reserve + commit / rollback" pattern.
135
279
  * Use for expensive resources (pipelines, plugins, AI calls) where the
@@ -151,7 +295,7 @@ export declare function incrementQuota(quotaService: QuotaService, orgId: string
151
295
  * }
152
296
  * ```
153
297
  */
154
- export declare function reserveQuota(quotaService: QuotaService, orgId: string, quotaType: QuotaType, authHeader: string, amount?: number, requestId?: string): Promise<QuotaReserveResult>;
298
+ export declare function reserveQuota(quotaService: QuotaService, orgId: string, quotaType: QuotaType, authHeader: string, amount?: number, attribution?: QuotaUsageAttribution): Promise<QuotaReserveResult>;
155
299
  /**
156
300
  * Answer a DENIED reservation (`reservation.exceeded === true`).
157
301
  *
@@ -166,4 +310,4 @@ export declare function sendQuotaReserveDenied(res: Response, quotaType: QuotaTy
166
310
  * Logs on failure but never throws the action that needed the rollback
167
311
  * has already failed, no point compounding the error.
168
312
  */
169
- export declare function decrementQuota(quotaService: QuotaService, orgId: string, quotaType: QuotaType, authHeader: string, logWarn: (message: string, data?: unknown) => void, amount?: number, resetAtSnapshot?: string): void;
313
+ export declare function decrementQuota(quotaService: QuotaService, orgId: string, quotaType: QuotaType, authHeader: string, logWarn: (message: string, data?: unknown) => void, amount?: number, resetAtSnapshot?: string, attribution?: QuotaUsageAttribution): void;