@opensip-cli/output 0.1.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.
- package/LICENSE +202 -0
- package/NOTICE +8 -0
- package/README.md +31 -0
- package/dist/format/baseline-diff.d.ts +37 -0
- package/dist/format/baseline-diff.d.ts.map +1 -0
- package/dist/format/baseline-diff.js +96 -0
- package/dist/format/baseline-diff.js.map +1 -0
- package/dist/format/signal-json.d.ts +17 -0
- package/dist/format/signal-json.d.ts.map +1 -0
- package/dist/format/signal-json.js +3 -0
- package/dist/format/signal-json.js.map +1 -0
- package/dist/format/signal-sarif.d.ts +50 -0
- package/dist/format/signal-sarif.d.ts.map +1 -0
- package/dist/format/signal-sarif.js +107 -0
- package/dist/format/signal-sarif.js.map +1 -0
- package/dist/format/signal-table.d.ts +44 -0
- package/dist/format/signal-table.d.ts.map +1 -0
- package/dist/format/signal-table.js +84 -0
- package/dist/format/signal-table.js.map +1 -0
- package/dist/format/types.d.ts +20 -0
- package/dist/format/types.d.ts.map +1 -0
- package/dist/format/types.js +2 -0
- package/dist/format/types.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/sink/cloud-signal-sink.d.ts +10 -0
- package/dist/sink/cloud-signal-sink.d.ts.map +1 -0
- package/dist/sink/cloud-signal-sink.js +94 -0
- package/dist/sink/cloud-signal-sink.js.map +1 -0
- package/dist/sink/entitlement.d.ts +28 -0
- package/dist/sink/entitlement.d.ts.map +1 -0
- package/dist/sink/entitlement.js +109 -0
- package/dist/sink/entitlement.js.map +1 -0
- package/dist/sink/http-egress.d.ts +49 -0
- package/dist/sink/http-egress.d.ts.map +1 -0
- package/dist/sink/http-egress.js +158 -0
- package/dist/sink/http-egress.js.map +1 -0
- package/dist/sink/repo-identity.d.ts +4 -0
- package/dist/sink/repo-identity.d.ts.map +1 -0
- package/dist/sink/repo-identity.js +32 -0
- package/dist/sink/repo-identity.js.map +1 -0
- package/dist/sink/resolve-signal-sink.d.ts +17 -0
- package/dist/sink/resolve-signal-sink.d.ts.map +1 -0
- package/dist/sink/resolve-signal-sink.js +87 -0
- package/dist/sink/resolve-signal-sink.js.map +1 -0
- package/package.json +48 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview signal-table formatter (ADR-0011, Phase 2 Task 2.5).
|
|
3
|
+
*
|
|
4
|
+
* Derives the terminal-table view-model purely from the envelope's `units` +
|
|
5
|
+
* `signals` — one row per unit — so tools stop pre-computing `TableRow[]` on
|
|
6
|
+
* their `*DoneResult` (today fitness builds rows in
|
|
7
|
+
* `result-builders.ts:buildFitDoneResult`). The Ink `ResultsTable` (cli-ui)
|
|
8
|
+
* consumes these rows; this layer does no Ink and no IO (formatter-purity
|
|
9
|
+
* contract).
|
|
10
|
+
*
|
|
11
|
+
* Unlike json/sarif, the table is structured, not a single string — so this
|
|
12
|
+
* exports a row/summary view-model builder rather than a `Formatter`
|
|
13
|
+
* (`(envelope) => string`). The Ink renderer (cli-ui) is the string side.
|
|
14
|
+
*
|
|
15
|
+
* The envelope carries only what a flat `Signal[]` cannot express (ran,
|
|
16
|
+
* errored, timing). Two further per-unit facts a flat list cannot express —
|
|
17
|
+
* fitness's `filesValidated`/`ignoredCount` — ride on {@link UnitResult} as
|
|
18
|
+
* optional fields; this formatter surfaces them as the optional
|
|
19
|
+
* `validated`/`ignored` row columns when present (graph/sim omit them → the
|
|
20
|
+
* renderer leaves the column blank). This keeps ONE shared table formatter
|
|
21
|
+
* (ADR-0011, Phase 6, decision B) rather than a fitness-specific rich view.
|
|
22
|
+
*/
|
|
23
|
+
import { formatDuration, isErrorSignal } from '@opensip-cli/core';
|
|
24
|
+
/** Group a run's signals by their `source` (the emitting unit's slug). */
|
|
25
|
+
function groupSignalsBySource(signals) {
|
|
26
|
+
const bySource = new Map();
|
|
27
|
+
for (const signal of signals) {
|
|
28
|
+
const bucket = bySource.get(signal.source);
|
|
29
|
+
if (bucket)
|
|
30
|
+
bucket.push(signal);
|
|
31
|
+
else
|
|
32
|
+
bySource.set(signal.source, [signal]);
|
|
33
|
+
}
|
|
34
|
+
return bySource;
|
|
35
|
+
}
|
|
36
|
+
/** Status for a unit: ERROR when it errored, else PASS/FAIL from `unit.passed`. */
|
|
37
|
+
function rowStatus(unit) {
|
|
38
|
+
if (unit.error !== undefined)
|
|
39
|
+
return 'ERROR';
|
|
40
|
+
return unit.passed ? 'PASS' : 'FAIL';
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Build one {@link SignalTableRow} per unit, attributing signals to units by
|
|
44
|
+
* `signal.source === unit.slug`. Pure: no IO, no clock.
|
|
45
|
+
*/
|
|
46
|
+
export function formatSignalTableRows(envelope) {
|
|
47
|
+
const bySource = groupSignalsBySource(envelope.signals);
|
|
48
|
+
return envelope.units.map((unit) => {
|
|
49
|
+
const unitSignals = bySource.get(unit.slug) ?? [];
|
|
50
|
+
let errors = 0;
|
|
51
|
+
let warnings = 0;
|
|
52
|
+
for (const signal of unitSignals) {
|
|
53
|
+
if (isErrorSignal(signal))
|
|
54
|
+
errors += 1;
|
|
55
|
+
else
|
|
56
|
+
warnings += 1;
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
unit: unit.slug,
|
|
60
|
+
status: rowStatus(unit),
|
|
61
|
+
errors,
|
|
62
|
+
warnings,
|
|
63
|
+
duration: formatDuration(unit.durationMs),
|
|
64
|
+
durationMs: unit.durationMs,
|
|
65
|
+
error: unit.error,
|
|
66
|
+
validated: unit.filesValidated,
|
|
67
|
+
itemType: unit.itemType,
|
|
68
|
+
ignored: unit.ignoredCount,
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/** Build the aggregate summary line from the envelope's verdict. Pure. */
|
|
73
|
+
export function formatSignalTableSummary(envelope) {
|
|
74
|
+
const { summary } = envelope.verdict;
|
|
75
|
+
const durationMs = envelope.units.reduce((total, unit) => total + unit.durationMs, 0);
|
|
76
|
+
return {
|
|
77
|
+
passed: summary.passed,
|
|
78
|
+
failed: summary.failed,
|
|
79
|
+
totalErrors: summary.errors,
|
|
80
|
+
totalWarnings: summary.warnings,
|
|
81
|
+
durationMs,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=signal-table.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signal-table.js","sourceRoot":"","sources":["../../src/format/signal-table.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AA0ClE,0EAA0E;AAC1E,SAAS,oBAAoB,CAAC,OAA0B;IACtD,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAoB,CAAC;IAC7C,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAC3C,IAAI,MAAM;YAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;;YAC3B,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;IAC7C,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,mFAAmF;AACnF,SAAS,SAAS,CAAC,IAAgB;IACjC,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS;QAAE,OAAO,OAAO,CAAC;IAC7C,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC;AACvC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CAAC,QAAwB;IAC5D,MAAM,QAAQ,GAAG,oBAAoB,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;IAExD,OAAO,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;QACjC,MAAM,WAAW,GAAG,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QAClD,IAAI,MAAM,GAAG,CAAC,CAAC;QACf,IAAI,QAAQ,GAAG,CAAC,CAAC;QACjB,KAAK,MAAM,MAAM,IAAI,WAAW,EAAE,CAAC;YACjC,IAAI,aAAa,CAAC,MAAM,CAAC;gBAAE,MAAM,IAAI,CAAC,CAAC;;gBAClC,QAAQ,IAAI,CAAC,CAAC;QACrB,CAAC;QACD,OAAO;YACL,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,MAAM,EAAE,SAAS,CAAC,IAAI,CAAC;YACvB,MAAM;YACN,QAAQ;YACR,QAAQ,EAAE,cAAc,CAAC,IAAI,CAAC,UAAU,CAAC;YACzC,UAAU,EAAE,IAAI,CAAC,UAAU;YAC3B,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,SAAS,EAAE,IAAI,CAAC,cAAc;YAC9B,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,OAAO,EAAE,IAAI,CAAC,YAAY;SAC3B,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,wBAAwB,CAAC,QAAwB;IAC/D,MAAM,EAAE,OAAO,EAAE,GAAG,QAAQ,CAAC,OAAO,CAAC;IACrC,MAAM,UAAU,GAAG,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,KAAK,GAAG,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC;IACtF,OAAO;QACL,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,WAAW,EAAE,OAAO,CAAC,MAAM;QAC3B,aAAa,EAAE,OAAO,CAAC,QAAQ;QAC/B,UAAU;KACX,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview The shared formatter contract (ADR-0011).
|
|
3
|
+
*
|
|
4
|
+
* A `Formatter` is a pure `(envelope) => string` transform — one per target
|
|
5
|
+
* format (json, sarif, table). It is modelled on graph's `Renderer`
|
|
6
|
+
* (`graph/engine/src/render/types.ts`, `(signals, context) => string`) but
|
|
7
|
+
* keyed on the universal {@link SignalEnvelope} so every tool shares one set
|
|
8
|
+
* of formatters.
|
|
9
|
+
*
|
|
10
|
+
* Formatter-purity contract: a formatter performs NO IO — no
|
|
11
|
+
* `process.stdout`, no network, no `Date.now()`/`randomUUID`. The run id and
|
|
12
|
+
* timestamp arrive on the envelope, so a fixed envelope renders to a fixed
|
|
13
|
+
* string (snapshot-testable with zero mocks). All effects live in
|
|
14
|
+
* `@opensip-cli/output/sink`; a sink may import a formatter, never the
|
|
15
|
+
* reverse.
|
|
16
|
+
*/
|
|
17
|
+
import type { SignalEnvelope } from '@opensip-cli/contracts';
|
|
18
|
+
/** Pure `(envelope) => string` formatter. The shared output transform contract. */
|
|
19
|
+
export type Formatter = (envelope: SignalEnvelope) => string;
|
|
20
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/format/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAE7D,mFAAmF;AACnF,MAAM,MAAM,SAAS,GAAG,CAAC,QAAQ,EAAE,cAAc,KAAK,MAAM,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/format/types.ts"],"names":[],"mappings":""}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @opensip-cli/output — the tool-run output layer.
|
|
3
|
+
*
|
|
4
|
+
* Renamed from `@opensip-cli/reporting` (Phase 2, ADR-0011): the package no
|
|
5
|
+
* longer just reports to cloud — it owns all machine formatting + delivery.
|
|
6
|
+
* It depends on core + contracts only.
|
|
7
|
+
*/
|
|
8
|
+
export type { Formatter } from './format/types.js';
|
|
9
|
+
export { formatSignalJson } from './format/signal-json.js';
|
|
10
|
+
export { formatSignalSarif, buildOpenSipSarif, type SarifDriver } from './format/signal-sarif.js';
|
|
11
|
+
export { diffBaseline } from './format/baseline-diff.js';
|
|
12
|
+
export type { GateCompareResult, BaselineDiffRow } from './format/baseline-diff.js';
|
|
13
|
+
export { formatSignalTableRows, formatSignalTableSummary, type SignalTableRow, type SignalTableSummary, } from './format/signal-table.js';
|
|
14
|
+
export { checkEntitlement, invalidateEntitlement, type EntitlementResult, type EntitlementSource, type CheckEntitlementInput, } from './sink/entitlement.js';
|
|
15
|
+
export { createCloudSignalSink, type CloudSignalSinkOptions } from './sink/cloud-signal-sink.js';
|
|
16
|
+
export { postChunked, type EgressResult, type RetryPolicy, type PostChunkedArgs, } from './sink/http-egress.js';
|
|
17
|
+
export { resolveSignalSink, DEFAULT_CLOUD_ENDPOINT, type ResolveSignalSinkInput, } from './sink/resolve-signal-sink.js';
|
|
18
|
+
export { resolveRepoIdentity } from './sink/repo-identity.js';
|
|
19
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,YAAY,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,KAAK,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAElG,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,YAAY,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AACpF,OAAO,EACL,qBAAqB,EACrB,wBAAwB,EACxB,KAAK,cAAc,EACnB,KAAK,kBAAkB,GACxB,MAAM,0BAA0B,CAAC;AAGlC,OAAO,EACL,gBAAgB,EAChB,qBAAqB,EACrB,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EACtB,KAAK,qBAAqB,GAC3B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,qBAAqB,EAAE,KAAK,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACjG,OAAO,EACL,WAAW,EACX,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,eAAe,GACrB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,iBAAiB,EACjB,sBAAsB,EACtB,KAAK,sBAAsB,GAC5B,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @opensip-cli/output — the tool-run output layer.
|
|
3
|
+
*
|
|
4
|
+
* Renamed from `@opensip-cli/reporting` (Phase 2, ADR-0011): the package no
|
|
5
|
+
* longer just reports to cloud — it owns all machine formatting + delivery.
|
|
6
|
+
* It depends on core + contracts only.
|
|
7
|
+
*/
|
|
8
|
+
export { formatSignalJson } from './format/signal-json.js';
|
|
9
|
+
export { formatSignalSarif, buildOpenSipSarif } from './format/signal-sarif.js';
|
|
10
|
+
// Pure baseline diff — the generic net-new ratchet (ADR-0036).
|
|
11
|
+
export { diffBaseline } from './format/baseline-diff.js';
|
|
12
|
+
export { formatSignalTableRows, formatSignalTableSummary, } from './format/signal-table.js';
|
|
13
|
+
// --- sink/ — effectful delivery (file/cloud egress) ---
|
|
14
|
+
export { checkEntitlement, invalidateEntitlement, } from './sink/entitlement.js';
|
|
15
|
+
export { createCloudSignalSink } from './sink/cloud-signal-sink.js';
|
|
16
|
+
export { postChunked, } from './sink/http-egress.js';
|
|
17
|
+
export { resolveSignalSink, DEFAULT_CLOUD_ENDPOINT, } from './sink/resolve-signal-sink.js';
|
|
18
|
+
export { resolveRepoIdentity } from './sink/repo-identity.js';
|
|
19
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAoB,MAAM,0BAA0B,CAAC;AAClG,+DAA+D;AAC/D,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAEzD,OAAO,EACL,qBAAqB,EACrB,wBAAwB,GAGzB,MAAM,0BAA0B,CAAC;AAElC,yDAAyD;AACzD,OAAO,EACL,gBAAgB,EAChB,qBAAqB,GAItB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,qBAAqB,EAA+B,MAAM,6BAA6B,CAAC;AACjG,OAAO,EACL,WAAW,GAIZ,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,iBAAiB,EACjB,sBAAsB,GAEvB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { SignalSink } from '@opensip-cli/core';
|
|
2
|
+
/** Construction options for the OpenSIP Cloud signal sink: target endpoint, API key, and an injectable `fetch` for tests. */
|
|
3
|
+
export interface CloudSignalSinkOptions {
|
|
4
|
+
readonly endpoint: string;
|
|
5
|
+
readonly apiKey: string;
|
|
6
|
+
readonly fetchImpl?: typeof fetch;
|
|
7
|
+
}
|
|
8
|
+
/** Build the OpenSIP Cloud signal sink. Selection/gating happens at the CLI; this just emits. */
|
|
9
|
+
export declare function createCloudSignalSink(opts: CloudSignalSinkOptions): SignalSink;
|
|
10
|
+
//# sourceMappingURL=cloud-signal-sink.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cloud-signal-sink.d.ts","sourceRoot":"","sources":["../../src/sink/cloud-signal-sink.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAiD,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAQnG,6HAA6H;AAC7H,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CACnC;AAiCD,iGAAiG;AACjG,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,sBAAsB,GAAG,UAAU,CAiE9E"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenSIP Cloud SignalSink (ADR-0008).
|
|
3
|
+
*
|
|
4
|
+
* The Strategy implementation that POSTs a run's `SignalBatch` to OpenSIP
|
|
5
|
+
* Cloud, chunked, via the shared `postChunked` transport with the best-effort
|
|
6
|
+
* policy. Wraps the sync in a `signal-sync` span and returns
|
|
7
|
+
* `{ accepted, authRejected }`; it NEVER throws (the non-blocking invariant) —
|
|
8
|
+
* the CLI uses `accepted` for the "Sent N signals" line and `authRejected` to
|
|
9
|
+
* bust the entitlement cache.
|
|
10
|
+
*/
|
|
11
|
+
import { logger, withSpanAsync } from '@opensip-cli/core';
|
|
12
|
+
import { postChunked } from './http-egress.js';
|
|
13
|
+
const MODULE_TAG = 'cloud-signal-sink';
|
|
14
|
+
const MAX_SIGNALS_PER_CHUNK = 500;
|
|
15
|
+
// Best-effort policy: try a little harder than reportToCloud, but bound the
|
|
16
|
+
// whole sync so a throttling server can never hang the CLI.
|
|
17
|
+
const POLICY = { maxAttempts: 4, overallDeadlineMs: 120_000, honorRetryAfter: true };
|
|
18
|
+
function chunkBatch(batch, size) {
|
|
19
|
+
const groups = [];
|
|
20
|
+
for (let i = 0; i < batch.signals.length; i += size) {
|
|
21
|
+
groups.push(batch.signals.slice(i, i + size));
|
|
22
|
+
}
|
|
23
|
+
return groups.map((signals, chunkIndex) => ({
|
|
24
|
+
schemaVersion: batch.schemaVersion,
|
|
25
|
+
tool: batch.tool,
|
|
26
|
+
recipe: batch.recipe,
|
|
27
|
+
repo: batch.repo,
|
|
28
|
+
runId: batch.runId,
|
|
29
|
+
createdAt: batch.createdAt,
|
|
30
|
+
chunkIndex,
|
|
31
|
+
chunkCount: groups.length,
|
|
32
|
+
signals,
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
/** Build the OpenSIP Cloud signal sink. Selection/gating happens at the CLI; this just emits. */
|
|
36
|
+
export function createCloudSignalSink(opts) {
|
|
37
|
+
return {
|
|
38
|
+
async emit(batch) {
|
|
39
|
+
try {
|
|
40
|
+
if (batch.signals.length === 0)
|
|
41
|
+
return { accepted: 0, authRejected: false };
|
|
42
|
+
const url = opts.endpoint.endsWith('/signals') ? opts.endpoint : `${opts.endpoint}/signals`;
|
|
43
|
+
const chunks = chunkBatch(batch, MAX_SIGNALS_PER_CHUNK);
|
|
44
|
+
const result = await withSpanAsync('reporting', 'signal-sync', async (span) => {
|
|
45
|
+
const r = await postChunked({
|
|
46
|
+
url,
|
|
47
|
+
apiKey: opts.apiKey,
|
|
48
|
+
chunks,
|
|
49
|
+
idempotencyKeyFor: (i) => `${batch.runId}:${i}`,
|
|
50
|
+
// @fitness-ignore-next-line null-safety -- `i` is the in-range index of the chunks array being posted; chunks[i] is always defined
|
|
51
|
+
timeoutFor: (_chunk, i) => Math.min(120_000, 30_000 + chunks[i].signals.length * 50),
|
|
52
|
+
policy: POLICY,
|
|
53
|
+
evtPrefix: 'cli.signal-sync',
|
|
54
|
+
fetchImpl: opts.fetchImpl,
|
|
55
|
+
});
|
|
56
|
+
span.setAttributes({
|
|
57
|
+
tool: batch.tool,
|
|
58
|
+
runId: batch.runId,
|
|
59
|
+
'signal.count': batch.signals.length,
|
|
60
|
+
'chunk.count': chunks.length,
|
|
61
|
+
throttled: r.throttled,
|
|
62
|
+
outcome: r.outcome,
|
|
63
|
+
});
|
|
64
|
+
return r;
|
|
65
|
+
}, { tool: batch.tool, runId: batch.runId });
|
|
66
|
+
const accepted = chunks.reduce((n, c, i) => (result.chunkResults[i] ? n + c.signals.length : n), 0);
|
|
67
|
+
logger.info({
|
|
68
|
+
evt: 'cli.signal-sync.done',
|
|
69
|
+
module: MODULE_TAG,
|
|
70
|
+
accepted,
|
|
71
|
+
outcome: result.outcome,
|
|
72
|
+
authRejected: result.authRejected,
|
|
73
|
+
});
|
|
74
|
+
return {
|
|
75
|
+
accepted,
|
|
76
|
+
authRejected: result.authRejected,
|
|
77
|
+
// Surface a transport-level total failure so the root can tell the
|
|
78
|
+
// user their signals did not ship (still non-blocking, ADR-0008).
|
|
79
|
+
...(accepted === 0 ? { skippedReason: 'error' } : {}),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
// Defense in depth — postChunked never throws, but emit MUST NOT either.
|
|
84
|
+
logger.warn({
|
|
85
|
+
evt: 'cli.signal-sync.error',
|
|
86
|
+
module: MODULE_TAG,
|
|
87
|
+
error: error instanceof Error ? error.message : String(error),
|
|
88
|
+
});
|
|
89
|
+
return { accepted: 0, authRejected: false, skippedReason: 'error' };
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=cloud-signal-sink.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cloud-signal-sink.js","sourceRoot":"","sources":["../../src/sink/cloud-signal-sink.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Where the decision came from — also the metric dimension. */
|
|
2
|
+
export type EntitlementSource = 'cache' | 'network' | 'fail-closed';
|
|
3
|
+
export interface EntitlementResult {
|
|
4
|
+
readonly entitled: boolean;
|
|
5
|
+
readonly source: EntitlementSource;
|
|
6
|
+
}
|
|
7
|
+
/** Input to {@link checkEntitlement}. `now`/`fetchImpl`/`cacheDir` are injectable for tests. */
|
|
8
|
+
export interface CheckEntitlementInput {
|
|
9
|
+
readonly apiKey: string;
|
|
10
|
+
readonly endpoint: string;
|
|
11
|
+
readonly now: number;
|
|
12
|
+
readonly cacheDir: string;
|
|
13
|
+
readonly fetchImpl?: typeof fetch;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Resolve entitlement: cache hit → use it; else a single network check; any
|
|
17
|
+
* failure → fail-closed. A clear `401`/`403` or `{ entitled: false }` is a real
|
|
18
|
+
* negative (cached briefly); a transport/format failure is fail-closed and not
|
|
19
|
+
* cached as confident.
|
|
20
|
+
*/
|
|
21
|
+
export declare function checkEntitlement(input: CheckEntitlementInput): Promise<EntitlementResult>;
|
|
22
|
+
/** Delete the cached entitlement for a key — called after a 401/403 at emit so a
|
|
23
|
+
* revoked plan re-checks on the next run rather than waiting out the TTL. */
|
|
24
|
+
export declare function invalidateEntitlement(input: {
|
|
25
|
+
apiKey: string;
|
|
26
|
+
cacheDir: string;
|
|
27
|
+
}): Promise<void>;
|
|
28
|
+
//# sourceMappingURL=entitlement.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entitlement.d.ts","sourceRoot":"","sources":["../../src/sink/entitlement.ts"],"names":[],"mappings":"AA0BA,gEAAgE;AAChE,MAAM,MAAM,iBAAiB,GAAG,OAAO,GAAG,SAAS,GAAG,aAAa,CAAC;AACpE,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;CACpC;AAOD,gGAAgG;AAChG,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CACnC;AAuCD;;;;;GAKG;AACH,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAgC/F;AAED;8EAC8E;AAC9E,wBAAsB,qBAAqB,CAAC,KAAK,EAAE;IACjD,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB,GAAG,OAAO,CAAC,IAAI,CAAC,CAMhB"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// @fitness-ignore-file error-handling-quality -- best-effort entitlement cache + check (ADR-0008): a missing/corrupt/unwritable cache and an unreachable endpoint all degrade to "not entitled" / a re-check next run, never throwing on the cloud hot path.
|
|
2
|
+
// @fitness-ignore-file unbounded-memory -- reads a tiny tool-generated entitlement cache file (a single boolean + timestamp).
|
|
3
|
+
/**
|
|
4
|
+
* Entitlement check for OpenSIP Cloud signal sync (ADR-0008).
|
|
5
|
+
*
|
|
6
|
+
* Answers "is this API key entitled to store signals?" with a locally cached
|
|
7
|
+
* result (so it is not a network round-trip every run) and a **fail-closed**
|
|
8
|
+
* default: on any ambiguity — no key, unreachable endpoint, non-2xx, malformed
|
|
9
|
+
* body — the answer is `entitled: false` and no positive cache entry is
|
|
10
|
+
* written. Data leaves the machine only on a clear positive signal.
|
|
11
|
+
*
|
|
12
|
+
* The ingestion/entitlement endpoint lives in the parent `opensip` repo and
|
|
13
|
+
* does not exist yet; this client codes against the agreed contract and is
|
|
14
|
+
* fully testable by injecting `fetchImpl` + `now` + a temp `cacheDir`.
|
|
15
|
+
*/
|
|
16
|
+
import { createHash } from 'node:crypto';
|
|
17
|
+
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
18
|
+
import { dirname, join } from 'node:path';
|
|
19
|
+
import { logger } from '@opensip-cli/core';
|
|
20
|
+
const MODULE_TAG = 'entitlement';
|
|
21
|
+
const POSITIVE_TTL_MS = 6 * 60 * 60 * 1000; // 6h — re-check entitled keys infrequently
|
|
22
|
+
const NEGATIVE_TTL_MS = 5 * 60 * 1000; // 5m — a real "no" caches briefly to avoid hammering
|
|
23
|
+
const REQUEST_TIMEOUT_MS = 10_000;
|
|
24
|
+
function cacheFileFor(cacheDir, apiKey) {
|
|
25
|
+
// Key the cache by a hash of the API key, never the key itself.
|
|
26
|
+
const hash = createHash('sha256').update(apiKey).digest('hex').slice(0, 16);
|
|
27
|
+
return join(cacheDir, `entitlement-${hash}.json`);
|
|
28
|
+
}
|
|
29
|
+
async function readCache(file, now) {
|
|
30
|
+
try {
|
|
31
|
+
const entry = JSON.parse(await readFile(file, 'utf8'));
|
|
32
|
+
const ttl = entry.entitled ? POSITIVE_TTL_MS : NEGATIVE_TTL_MS;
|
|
33
|
+
if (now - entry.checkedAt < ttl)
|
|
34
|
+
return entry.entitled;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
/* missing or corrupt cache → treat as a miss */
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
async function writeCache(file, entitled, now) {
|
|
42
|
+
try {
|
|
43
|
+
await mkdir(dirname(file), { recursive: true });
|
|
44
|
+
await writeFile(file, JSON.stringify({ entitled, checkedAt: now }));
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
/* cache write is best-effort — a failure just means we re-check next run */
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function log(result) {
|
|
51
|
+
// Structured event doubles as the metric signal (labelled by source); never the key.
|
|
52
|
+
logger.info({
|
|
53
|
+
evt: 'cli.signal-sync.entitlement',
|
|
54
|
+
module: MODULE_TAG,
|
|
55
|
+
entitled: result.entitled,
|
|
56
|
+
source: result.source,
|
|
57
|
+
});
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Resolve entitlement: cache hit → use it; else a single network check; any
|
|
62
|
+
* failure → fail-closed. A clear `401`/`403` or `{ entitled: false }` is a real
|
|
63
|
+
* negative (cached briefly); a transport/format failure is fail-closed and not
|
|
64
|
+
* cached as confident.
|
|
65
|
+
*/
|
|
66
|
+
export async function checkEntitlement(input) {
|
|
67
|
+
const { apiKey, endpoint, now, cacheDir } = input;
|
|
68
|
+
if (!apiKey)
|
|
69
|
+
return log({ entitled: false, source: 'fail-closed' });
|
|
70
|
+
const file = cacheFileFor(cacheDir, apiKey);
|
|
71
|
+
const cached = await readCache(file, now);
|
|
72
|
+
if (cached !== undefined)
|
|
73
|
+
return log({ entitled: cached, source: 'cache' });
|
|
74
|
+
const fetchImpl = input.fetchImpl ?? fetch;
|
|
75
|
+
try {
|
|
76
|
+
const url = endpoint.endsWith('/entitlements') ? endpoint : `${endpoint}/entitlements`;
|
|
77
|
+
const res = await fetchImpl(url, {
|
|
78
|
+
method: 'GET',
|
|
79
|
+
headers: { 'X-API-Key': apiKey },
|
|
80
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
81
|
+
});
|
|
82
|
+
if (res.status === 401 || res.status === 403) {
|
|
83
|
+
await writeCache(file, false, now); // real negative
|
|
84
|
+
return log({ entitled: false, source: 'network' });
|
|
85
|
+
}
|
|
86
|
+
if (!res.ok)
|
|
87
|
+
return log({ entitled: false, source: 'fail-closed' }); // 5xx/other → don't trust, don't cache
|
|
88
|
+
const body = (await res.json().catch(() => null));
|
|
89
|
+
if (!body || typeof body.entitled !== 'boolean') {
|
|
90
|
+
return log({ entitled: false, source: 'fail-closed' });
|
|
91
|
+
}
|
|
92
|
+
await writeCache(file, body.entitled, now);
|
|
93
|
+
return log({ entitled: body.entitled, source: 'network' });
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return log({ entitled: false, source: 'fail-closed' });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/** Delete the cached entitlement for a key — called after a 401/403 at emit so a
|
|
100
|
+
* revoked plan re-checks on the next run rather than waiting out the TTL. */
|
|
101
|
+
export async function invalidateEntitlement(input) {
|
|
102
|
+
try {
|
|
103
|
+
await rm(cacheFileFor(input.cacheDir, input.apiKey), { force: true });
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
/* best-effort */
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=entitlement.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entitlement.js","sourceRoot":"","sources":["../../src/sink/entitlement.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** Per-caller retry/throttle policy. */
|
|
2
|
+
export interface RetryPolicy {
|
|
3
|
+
/** Max attempts per chunk. */
|
|
4
|
+
readonly maxAttempts: number;
|
|
5
|
+
/** Whole-batch wall-clock budget; once exceeded, stop and return partial. */
|
|
6
|
+
readonly overallDeadlineMs: number;
|
|
7
|
+
/** Parse + honor `Retry-After` on `429`/`503`. */
|
|
8
|
+
readonly honorRetryAfter: boolean;
|
|
9
|
+
}
|
|
10
|
+
/** Structured outcome of a chunked POST. Never thrown — always returned. */
|
|
11
|
+
export interface EgressResult {
|
|
12
|
+
/** Count of chunks the server acked with 2xx. */
|
|
13
|
+
readonly acceptedChunks: number;
|
|
14
|
+
/** Per-chunk success, indexed by ordinal (lets callers sum item counts). */
|
|
15
|
+
readonly chunkResults: readonly boolean[];
|
|
16
|
+
readonly outcome: 'ok' | 'partial' | 'failed';
|
|
17
|
+
/** Saw a 401/403 — caller should bust any auth/entitlement cache. */
|
|
18
|
+
readonly authRejected: boolean;
|
|
19
|
+
/** Saw a 429. */
|
|
20
|
+
readonly throttled: boolean;
|
|
21
|
+
/** Stopped early because the overall deadline elapsed. */
|
|
22
|
+
readonly deadlineExceeded: boolean;
|
|
23
|
+
readonly errors: readonly string[];
|
|
24
|
+
}
|
|
25
|
+
/** Arguments for posting pre-chunked SARIF/signal bodies to a cloud receiver, one POST per body, under the retry policy. */
|
|
26
|
+
export interface PostChunkedArgs {
|
|
27
|
+
readonly url: string;
|
|
28
|
+
readonly apiKey?: string;
|
|
29
|
+
/** JSON-serializable bodies, one POST each. */
|
|
30
|
+
readonly chunks: readonly unknown[];
|
|
31
|
+
/** Stable idempotency key for the chunk at `ordinal` (same across its retries). */
|
|
32
|
+
readonly idempotencyKeyFor: (ordinal: number) => string;
|
|
33
|
+
/** Per-chunk request timeout in ms. */
|
|
34
|
+
readonly timeoutFor: (chunk: unknown, ordinal: number) => number;
|
|
35
|
+
readonly policy: RetryPolicy;
|
|
36
|
+
/** Log event prefix, e.g. `cli.report` or `cli.signal-sync`. */
|
|
37
|
+
readonly evtPrefix: string;
|
|
38
|
+
readonly fetchImpl?: typeof fetch;
|
|
39
|
+
/** Injectable clock/sleep for deterministic tests. */
|
|
40
|
+
readonly now?: () => number;
|
|
41
|
+
readonly sleep?: (ms: number) => Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* POST each chunk with bounded per-chunk retries on `429`/`5xx`/transport
|
|
45
|
+
* errors, honoring `Retry-After`, an overall deadline, and stable idempotency
|
|
46
|
+
* keys. Returns an {@link EgressResult}; never throws.
|
|
47
|
+
*/
|
|
48
|
+
export declare function postChunked(args: PostChunkedArgs): Promise<EgressResult>;
|
|
49
|
+
//# sourceMappingURL=http-egress.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http-egress.d.ts","sourceRoot":"","sources":["../../src/sink/http-egress.ts"],"names":[],"mappings":"AAiBA,wCAAwC;AACxC,MAAM,WAAW,WAAW;IAC1B,8BAA8B;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,6EAA6E;IAC7E,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,kDAAkD;IAClD,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;CACnC;AAED,4EAA4E;AAC5E,MAAM,WAAW,YAAY;IAC3B,iDAAiD;IACjD,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,4EAA4E;IAC5E,QAAQ,CAAC,YAAY,EAAE,SAAS,OAAO,EAAE,CAAC;IAC1C,QAAQ,CAAC,OAAO,EAAE,IAAI,GAAG,SAAS,GAAG,QAAQ,CAAC;IAC9C,qEAAqE;IACrE,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,iBAAiB;IACjB,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,0DAA0D;IAC1D,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;IACnC,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;CACpC;AAED,4HAA4H;AAC5H,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,+CAA+C;IAC/C,QAAQ,CAAC,MAAM,EAAE,SAAS,OAAO,EAAE,CAAC;IACpC,mFAAmF;IACnF,QAAQ,CAAC,iBAAiB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;IACxD,uCAAuC;IACvC,QAAQ,CAAC,UAAU,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;IACjE,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,gEAAgE;IAChE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IAClC,sDAAsD;IACtD,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAChD;AAyBD;;;;GAIG;AAEH,wBAAsB,WAAW,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,YAAY,CAAC,CAwH9E"}
|