@centient/secrets 0.9.0 → 0.11.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/README.md +424 -8
- package/dist/audit/hmac-chain.d.ts +117 -0
- package/dist/audit/hmac-chain.d.ts.map +1 -0
- package/dist/audit/hmac-chain.js +811 -0
- package/dist/audit/hmac-chain.js.map +1 -0
- package/dist/audit/ocsf.d.ts +108 -0
- package/dist/audit/ocsf.d.ts.map +1 -0
- package/dist/audit/ocsf.js +204 -0
- package/dist/audit/ocsf.js.map +1 -0
- package/dist/cli/hidden-prompt.d.ts.map +1 -1
- package/dist/cli/hidden-prompt.js +55 -10
- package/dist/cli/hidden-prompt.js.map +1 -1
- package/dist/cli/messages.d.ts +40 -8
- package/dist/cli/messages.d.ts.map +1 -1
- package/dist/cli/messages.js +34 -8
- package/dist/cli/messages.js.map +1 -1
- package/dist/cli/secrets-cli.d.ts.map +1 -1
- package/dist/cli/secrets-cli.js +191 -32
- package/dist/cli/secrets-cli.js.map +1 -1
- package/dist/client.d.ts +26 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +129 -0
- package/dist/client.js.map +1 -0
- package/dist/compatibility.d.ts +199 -0
- package/dist/compatibility.d.ts.map +1 -0
- package/dist/compatibility.js +419 -0
- package/dist/compatibility.js.map +1 -0
- package/dist/crypto/darwin-keychain.d.ts +394 -0
- package/dist/crypto/darwin-keychain.d.ts.map +1 -0
- package/dist/crypto/darwin-keychain.js +793 -0
- package/dist/crypto/darwin-keychain.js.map +1 -0
- package/dist/crypto/vault-common.d.ts +55 -12
- package/dist/crypto/vault-common.d.ts.map +1 -1
- package/dist/crypto/vault-common.js +74 -71
- package/dist/crypto/vault-common.js.map +1 -1
- package/dist/environment/EnvironmentManager.d.ts.map +1 -1
- package/dist/environment/EnvironmentManager.js +5 -10
- package/dist/environment/EnvironmentManager.js.map +1 -1
- package/dist/index.d.ts +37 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +38 -6
- package/dist/index.js.map +1 -1
- package/dist/key-providers/keychain-provider.d.ts +30 -2
- package/dist/key-providers/keychain-provider.d.ts.map +1 -1
- package/dist/key-providers/keychain-provider.js +29 -5
- package/dist/key-providers/keychain-provider.js.map +1 -1
- package/dist/key-providers/onepassword-provider.d.ts +65 -5
- package/dist/key-providers/onepassword-provider.d.ts.map +1 -1
- package/dist/key-providers/onepassword-provider.js +159 -67
- package/dist/key-providers/onepassword-provider.js.map +1 -1
- package/dist/key-providers/op-cli.d.ts +78 -0
- package/dist/key-providers/op-cli.d.ts.map +1 -0
- package/dist/key-providers/op-cli.js +123 -0
- package/dist/key-providers/op-cli.js.map +1 -0
- package/dist/key-providers/passphrase-provider.d.ts +20 -0
- package/dist/key-providers/passphrase-provider.d.ts.map +1 -1
- package/dist/key-providers/passphrase-provider.js +49 -0
- package/dist/key-providers/passphrase-provider.js.map +1 -1
- package/dist/key-providers/probe.d.ts +46 -0
- package/dist/key-providers/probe.d.ts.map +1 -0
- package/dist/key-providers/probe.js +70 -0
- package/dist/key-providers/probe.js.map +1 -0
- package/dist/key-providers/types.d.ts +51 -1
- package/dist/key-providers/types.d.ts.map +1 -1
- package/dist/platform/agent-detect.d.ts +39 -3
- package/dist/platform/agent-detect.d.ts.map +1 -1
- package/dist/platform/agent-detect.js +55 -9
- package/dist/platform/agent-detect.js.map +1 -1
- package/dist/platform/session-context.d.ts +77 -0
- package/dist/platform/session-context.d.ts.map +1 -0
- package/dist/platform/session-context.js +83 -0
- package/dist/platform/session-context.js.map +1 -0
- package/dist/policies/audit-trail.d.ts +25 -0
- package/dist/policies/audit-trail.d.ts.map +1 -0
- package/dist/policies/audit-trail.js +31 -0
- package/dist/policies/audit-trail.js.map +1 -0
- package/dist/secret-state.d.ts +54 -0
- package/dist/secret-state.d.ts.map +1 -0
- package/dist/secret-state.js +2 -0
- package/dist/secret-state.js.map +1 -0
- package/dist/sinks/index.d.ts +9 -0
- package/dist/sinks/index.d.ts.map +1 -0
- package/dist/sinks/index.js +5 -0
- package/dist/sinks/index.js.map +1 -0
- package/dist/sinks/jsonl.d.ts +16 -0
- package/dist/sinks/jsonl.d.ts.map +1 -0
- package/dist/sinks/jsonl.js +51 -0
- package/dist/sinks/jsonl.js.map +1 -0
- package/dist/sinks/otel.d.ts +22 -0
- package/dist/sinks/otel.d.ts.map +1 -0
- package/dist/sinks/otel.js +155 -0
- package/dist/sinks/otel.js.map +1 -0
- package/dist/sinks/queue.d.ts +12 -0
- package/dist/sinks/queue.d.ts.map +1 -0
- package/dist/sinks/queue.js +89 -0
- package/dist/sinks/queue.js.map +1 -0
- package/dist/sinks/syslog.d.ts +17 -0
- package/dist/sinks/syslog.d.ts.map +1 -0
- package/dist/sinks/syslog.js +103 -0
- package/dist/sinks/syslog.js.map +1 -0
- package/dist/sinks/types.d.ts +27 -0
- package/dist/sinks/types.d.ts.map +1 -0
- package/dist/sinks/types.js +18 -0
- package/dist/sinks/types.js.map +1 -0
- package/dist/vault/policy.d.ts +64 -34
- package/dist/vault/policy.d.ts.map +1 -1
- package/dist/vault/policy.js +109 -67
- package/dist/vault/policy.js.map +1 -1
- package/dist/vault/session-vault-errors.d.ts +72 -4
- package/dist/vault/session-vault-errors.d.ts.map +1 -1
- package/dist/vault/session-vault-errors.js +95 -4
- package/dist/vault/session-vault-errors.js.map +1 -1
- package/dist/vault/session-vault.d.ts +145 -2
- package/dist/vault/session-vault.d.ts.map +1 -1
- package/dist/vault/session-vault.js +548 -72
- package/dist/vault/session-vault.js.map +1 -1
- package/dist/vault/sidecar.d.ts +9 -0
- package/dist/vault/sidecar.d.ts.map +1 -1
- package/dist/vault/sidecar.js +72 -3
- package/dist/vault/sidecar.js.map +1 -1
- package/dist/vault/types.d.ts +58 -10
- package/dist/vault/types.d.ts.map +1 -1
- package/dist/vault/types.js.map +1 -1
- package/dist/vault/vault-env.d.ts +43 -50
- package/dist/vault/vault-env.d.ts.map +1 -1
- package/dist/vault/vault-env.js +141 -62
- package/dist/vault/vault-env.js.map +1 -1
- package/dist/vault/vault-gpg.d.ts +6 -1
- package/dist/vault/vault-gpg.d.ts.map +1 -1
- package/dist/vault/vault-gpg.js +10 -8
- package/dist/vault/vault-gpg.js.map +1 -1
- package/dist/vault/vault-libsecret.d.ts +6 -1
- package/dist/vault/vault-libsecret.d.ts.map +1 -1
- package/dist/vault/vault-libsecret.js +16 -14
- package/dist/vault/vault-libsecret.js.map +1 -1
- package/dist/vault/vault-onepassword.d.ts +119 -0
- package/dist/vault/vault-onepassword.d.ts.map +1 -0
- package/dist/vault/vault-onepassword.js +271 -0
- package/dist/vault/vault-onepassword.js.map +1 -0
- package/dist/vault/vault-utils.d.ts +37 -3
- package/dist/vault/vault-utils.d.ts.map +1 -1
- package/dist/vault/vault-utils.js +65 -3
- package/dist/vault/vault-utils.js.map +1 -1
- package/dist/vault/vault-windows.d.ts +7 -2
- package/dist/vault/vault-windows.d.ts.map +1 -1
- package/dist/vault/vault-windows.js +21 -16
- package/dist/vault/vault-windows.js.map +1 -1
- package/dist/vault/vault.d.ts +120 -9
- package/dist/vault/vault.d.ts.map +1 -1
- package/dist/vault/vault.js +222 -139
- package/dist/vault/vault.js.map +1 -1
- package/package.json +8 -2
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The compatibility floor: what an OLD `@centient/secrets` silently does wrong,
|
|
3
|
+
* published as data so a consumer can grade itself (#214).
|
|
4
|
+
*
|
|
5
|
+
* ## Why this exists
|
|
6
|
+
*
|
|
7
|
+
* Five consumers pin this package at four disjoint minors (`^0.5.0` … `0.10.0`)
|
|
8
|
+
* with no negotiation surface between them and the provider. Several releases
|
|
9
|
+
* closed defects whose only symptom, at the old version, is **silence**: a
|
|
10
|
+
* denied credential operation that leaves no audit trace, a keychain write that
|
|
11
|
+
* reports success into a keychain this context's reader cannot see, a malformed
|
|
12
|
+
* key that stores on one backend and reads as absent on the next. A consumer
|
|
13
|
+
* running such a version has no way to learn that from the package — the
|
|
14
|
+
* behaviour looks fine and the logs look clean.
|
|
15
|
+
*
|
|
16
|
+
* So the provider publishes the list. {@link COMPATIBILITY_FLOOR} names each
|
|
17
|
+
* closed gap, the version that closed it, and the symptom it presented before
|
|
18
|
+
* then; {@link assessCompatibility} turns an installed version into a grade a
|
|
19
|
+
* `doctor` command can print.
|
|
20
|
+
*
|
|
21
|
+
* ## What this is NOT
|
|
22
|
+
*
|
|
23
|
+
* **Never a boot-time refusal.** Nothing here runs at import, nothing throws
|
|
24
|
+
* from a constructor, and no vault path consults it. A package that refuses to
|
|
25
|
+
* load because the caller is old converts a documentation problem into an
|
|
26
|
+
* outage. Every function in this module is pure, total, and advisory: malformed
|
|
27
|
+
* input comes back as a typed `"unknown"` grade carrying the reason (P2, No
|
|
28
|
+
* Silent Degradation; P10, Honest Uncertainty), never as a throw.
|
|
29
|
+
*
|
|
30
|
+
* ## What a consumer does with it
|
|
31
|
+
*
|
|
32
|
+
* ```ts
|
|
33
|
+
* import { assessCompatibility, SECRETS_PACKAGE_VERSION } from "@centient/secrets";
|
|
34
|
+
*
|
|
35
|
+
* const assessment = assessCompatibility(SECRETS_PACKAGE_VERSION);
|
|
36
|
+
* if (assessment.grade !== "ok") {
|
|
37
|
+
* for (const gap of assessment.openGaps) {
|
|
38
|
+
* report(`${gap.severity}: ${gap.symptom} (fixed in ${gap.closedIn}, ${gap.reference})`);
|
|
39
|
+
* }
|
|
40
|
+
* }
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* `SECRETS_PACKAGE_VERSION` is the version of the build the consumer actually
|
|
44
|
+
* imported, so the assessment describes the code in that tree — not whatever is
|
|
45
|
+
* latest on the registry. A consumer auditing a *manifest* range instead (`the
|
|
46
|
+
* pin says ^0.6.0 — what does that admit at worst?`) uses {@link assessPin}.
|
|
47
|
+
*
|
|
48
|
+
* Issue: centient-labs/centient-sdk#214 (findings C6-27, C6-29, C6-61).
|
|
49
|
+
*/
|
|
50
|
+
/**
|
|
51
|
+
* The version of THIS build of `@centient/secrets`.
|
|
52
|
+
*
|
|
53
|
+
* Kept in step with `package.json` two ways: `scripts/sync-claudemd-versions.mjs`
|
|
54
|
+
* rewrites it during `pnpm run version-packages` (the same step that syncs the
|
|
55
|
+
* CLAUDE.md package table), and `tests/compatibility.test.ts` fails the build if
|
|
56
|
+
* the two ever disagree. A stale constant here would misgrade every consumer, so
|
|
57
|
+
* drift is a test failure rather than a silent wrong answer.
|
|
58
|
+
*/
|
|
59
|
+
export declare const SECRETS_PACKAGE_VERSION = "0.11.0";
|
|
60
|
+
/**
|
|
61
|
+
* The oldest version {@link COMPATIBILITY_FLOOR} can honestly describe.
|
|
62
|
+
*
|
|
63
|
+
* `0.5.0` is the oldest release any current consumer pins, and the oldest one
|
|
64
|
+
* whose behaviour the table's entries were written against. Below it the table
|
|
65
|
+
* is not wrong so much as **incomplete** — returning its short list for a 0.4.x
|
|
66
|
+
* install would understate what is open — so {@link assessCompatibility} grades
|
|
67
|
+
* such a version `"unsupported"` and says so, rather than quietly reporting the
|
|
68
|
+
* gaps it happens to know about.
|
|
69
|
+
*
|
|
70
|
+
* This is an epistemic limit on the table, not a support policy and not a gate:
|
|
71
|
+
* an `"unsupported"` grade still returns every gap the table does know about.
|
|
72
|
+
*/
|
|
73
|
+
export declare const MIN_ASSESSABLE_VERSION = "0.5.0";
|
|
74
|
+
/** How badly the gap bites a consumer still running below `closedIn`. */
|
|
75
|
+
export type GapSeverity = "high" | "medium" | "low";
|
|
76
|
+
/**
|
|
77
|
+
* One known behavioural gap, keyed by the version that closed it.
|
|
78
|
+
*
|
|
79
|
+
* A gap is **open** for an installed version `v` exactly when
|
|
80
|
+
* `v < closedIn` — there is no upper bound, because a fix that shipped is not
|
|
81
|
+
* un-shipped. Everything here is value-free public metadata: no credential
|
|
82
|
+
* material, no host paths, nothing that changes at runtime.
|
|
83
|
+
*/
|
|
84
|
+
export interface CompatibilityGap {
|
|
85
|
+
/** Stable machine-readable id. Never renamed — consumers may suppress by id. */
|
|
86
|
+
readonly id: string;
|
|
87
|
+
/** The first release in which the gap is closed. A valid `major.minor.patch`. */
|
|
88
|
+
readonly closedIn: string;
|
|
89
|
+
/** How badly this bites a consumer still below `closedIn`. */
|
|
90
|
+
readonly severity: GapSeverity;
|
|
91
|
+
/** One line, consumer-facing: what the OLD version does wrong. */
|
|
92
|
+
readonly symptom: string;
|
|
93
|
+
/** Issue or PR that closed it, `owner/repo#n`. */
|
|
94
|
+
readonly reference: string;
|
|
95
|
+
/**
|
|
96
|
+
* What the consumer must change when it adopts the fix, when adopting is not
|
|
97
|
+
* a pure no-op. Absent means "bump and nothing else". This is the field that
|
|
98
|
+
* turns the table from a scorecard into a bump plan.
|
|
99
|
+
*/
|
|
100
|
+
readonly adoptionNote?: string;
|
|
101
|
+
/** The C6 audit finding this gap corresponds to, where one exists. */
|
|
102
|
+
readonly finding?: string;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The published floor — deeply frozen at module initialization (every field on
|
|
106
|
+
* a {@link CompatibilityGap} is a primitive, so freezing each entry plus the
|
|
107
|
+
* array is a complete freeze). `assessCompatibility()` defaults to this same
|
|
108
|
+
* object, so a runtime mutation — a JS consumer, a test helper, a cast past
|
|
109
|
+
* the `readonly` type — would silently change every later assessment in the
|
|
110
|
+
* process. Frozen, the mutation throws at the mutation site instead
|
|
111
|
+
* (mb:f/7e54ba; the same no-silent-degradation stance as the rest of this
|
|
112
|
+
* module).
|
|
113
|
+
*/
|
|
114
|
+
export declare const COMPATIBILITY_FLOOR: readonly CompatibilityGap[];
|
|
115
|
+
/** The grade a consumer's installed version earns against the floor. */
|
|
116
|
+
export type CompatibilityGrade =
|
|
117
|
+
/** At or above every known fix. Nothing in the table is open. */
|
|
118
|
+
"ok"
|
|
119
|
+
/** Assessable, but one or more known gaps are open. `openGaps` lists them. */
|
|
120
|
+
| "degraded"
|
|
121
|
+
/** Below {@link MIN_ASSESSABLE_VERSION}: the table cannot claim to be complete. */
|
|
122
|
+
| "unsupported"
|
|
123
|
+
/** The version could not be parsed. `reason` says why. Never a throw. */
|
|
124
|
+
| "unknown";
|
|
125
|
+
/**
|
|
126
|
+
* The result of grading one version against the floor. Every field is derived
|
|
127
|
+
* and value-free; the input is echoed back so a caller can see what was graded
|
|
128
|
+
* (P6, Single Source of Truth — echo interpreted parameters).
|
|
129
|
+
*/
|
|
130
|
+
export interface CompatibilityAssessment {
|
|
131
|
+
readonly grade: CompatibilityGrade;
|
|
132
|
+
/** The version as given, echoed verbatim — including unparseable input. */
|
|
133
|
+
readonly installedVersion: string;
|
|
134
|
+
/** The version of the build that produced this assessment. */
|
|
135
|
+
readonly currentVersion: string;
|
|
136
|
+
/** The floor below which the table is incomplete. */
|
|
137
|
+
readonly minAssessableVersion: string;
|
|
138
|
+
/**
|
|
139
|
+
* Gaps open at `installedVersion`, oldest fix first. Empty for `"ok"` and for
|
|
140
|
+
* `"unknown"` — and for `"unknown"` that emptiness means `not assessed`, which
|
|
141
|
+
* is why the grade, not the list length, is what a caller branches on.
|
|
142
|
+
*/
|
|
143
|
+
readonly openGaps: readonly CompatibilityGap[];
|
|
144
|
+
/** The worst severity among `openGaps`, or `null` when there are none. */
|
|
145
|
+
readonly highestSeverity: GapSeverity | null;
|
|
146
|
+
/** One line suitable for a doctor's status column. */
|
|
147
|
+
readonly summary: string;
|
|
148
|
+
/** Why the grade is `"unknown"` or `"unsupported"`. Absent otherwise. */
|
|
149
|
+
readonly reason?: string;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Grade an installed version against the compatibility floor.
|
|
153
|
+
*
|
|
154
|
+
* Pure and total: it reads no environment, touches no keychain, and **never
|
|
155
|
+
* throws** — a version string it cannot parse comes back as
|
|
156
|
+
* `grade: "unknown"` naming the problem. Totality is enforced here rather than
|
|
157
|
+
* assumed of the parser: input is narrowed by this module's own guard, so a JS
|
|
158
|
+
* caller handing it `null`, `undefined`, a number or an object gets the same
|
|
159
|
+
* typed `"unknown"` result as a misspelt version, never an exception. Call it
|
|
160
|
+
* from a `doctor`, a startup log line, or a CI check; do not call it from a
|
|
161
|
+
* code path that would refuse to run on the answer.
|
|
162
|
+
*
|
|
163
|
+
* @param installedVersion - the version actually installed in the consumer's
|
|
164
|
+
* tree. Import {@link SECRETS_PACKAGE_VERSION} for that; a leading `v` and
|
|
165
|
+
* surrounding whitespace are tolerated.
|
|
166
|
+
* @param floor - the gap table to grade against. Defaults to
|
|
167
|
+
* {@link COMPATIBILITY_FLOOR}; injectable so a test can grade a fixture table
|
|
168
|
+
* without pinning itself to the shipped one.
|
|
169
|
+
*/
|
|
170
|
+
export declare function assessCompatibility(installedVersion: string, floor?: readonly CompatibilityGap[]): CompatibilityAssessment;
|
|
171
|
+
/**
|
|
172
|
+
* The lowest version a dependency range admits, or `null` when the range form
|
|
173
|
+
* is one this helper does not understand.
|
|
174
|
+
*
|
|
175
|
+
* Deliberately narrow: it recognises the forms that actually appear in the
|
|
176
|
+
* consumers' manifests — `^x.y.z`, `~x.y.z`, `>=x.y.z` and a bare `x.y.z` — and
|
|
177
|
+
* returns `null` for everything else rather than guessing. A guess here would
|
|
178
|
+
* grade the wrong version and report a clean bill of health for a pin nobody
|
|
179
|
+
* checked.
|
|
180
|
+
*
|
|
181
|
+
* The accepted grammar is matched whole and enumerated on
|
|
182
|
+
* {@link SUPPORTED_RANGE}; nothing outside it is partially interpreted. Total:
|
|
183
|
+
* a non-string, an empty string, and every unsupported form all return `null`.
|
|
184
|
+
*/
|
|
185
|
+
export declare function lowestVersionInRange(range: string): string | null;
|
|
186
|
+
/**
|
|
187
|
+
* Grade a manifest **pin** by its worst admissible version.
|
|
188
|
+
*
|
|
189
|
+
* `assessCompatibility` answers "what is open in the build I imported"; this
|
|
190
|
+
* answers "what could be open in any build this pin admits". A `^0.6.0` pin can
|
|
191
|
+
* resolve to any 0.6.x, so the version worth grading is its base — which is also
|
|
192
|
+
* the one a lockfile that has never been refreshed is most likely holding.
|
|
193
|
+
*
|
|
194
|
+
* Returns `grade: "unknown"` carrying the reason for a range form
|
|
195
|
+
* {@link lowestVersionInRange} does not recognise — including a non-string a JS
|
|
196
|
+
* caller slipped past the signature. Never throws.
|
|
197
|
+
*/
|
|
198
|
+
export declare function assessPin(pin: string, floor?: readonly CompatibilityGap[]): CompatibilityAssessment;
|
|
199
|
+
//# sourceMappingURL=compatibility.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compatibility.d.ts","sourceRoot":"","sources":["../src/compatibility.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AAIH;;;;;;;;GAQG;AACH,eAAO,MAAM,uBAAuB,WAAW,CAAC;AAEhD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,sBAAsB,UAAU,CAAC;AAE9C,yEAAyE;AACzE,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;AAQpD;;;;;;;GAOG;AACH,MAAM,WAAW,gBAAgB;IAC/B,gFAAgF;IAChF,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,iFAAiF;IACjF,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,8DAA8D;IAC9D,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC;IAC/B,kEAAkE;IAClE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,kDAAkD;IAClD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,sEAAsE;IACtE,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AA+ED;;;;;;;;;GASG;AACH,eAAO,MAAM,mBAAmB,EAAE,SAAS,gBAAgB,EAE1D,CAAC;AA4DF,wEAAwE;AACxE,MAAM,MAAM,kBAAkB;AAC5B,iEAAiE;AAC/D,IAAI;AACN,8EAA8E;GAC5E,UAAU;AACZ,mFAAmF;GACjF,aAAa;AACf,yEAAyE;GACvE,SAAS,CAAC;AAEd;;;;GAIG;AACH,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,KAAK,EAAE,kBAAkB,CAAC;IACnC,2EAA2E;IAC3E,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,8DAA8D;IAC9D,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,qDAAqD;IACrD,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAC;IACtC;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC/C,0EAA0E;IAC1E,QAAQ,CAAC,eAAe,EAAE,WAAW,GAAG,IAAI,CAAC;IAC7C,sDAAsD;IACtD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,yEAAyE;IACzE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,mBAAmB,CACjC,gBAAgB,EAAE,MAAM,EACxB,KAAK,GAAE,SAAS,gBAAgB,EAAwB,GACvD,uBAAuB,CAwEzB;AA0FD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGjE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,SAAS,CACvB,GAAG,EAAE,MAAM,EACX,KAAK,GAAE,SAAS,gBAAgB,EAAwB,GACvD,uBAAuB,CAezB"}
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The compatibility floor: what an OLD `@centient/secrets` silently does wrong,
|
|
3
|
+
* published as data so a consumer can grade itself (#214).
|
|
4
|
+
*
|
|
5
|
+
* ## Why this exists
|
|
6
|
+
*
|
|
7
|
+
* Five consumers pin this package at four disjoint minors (`^0.5.0` … `0.10.0`)
|
|
8
|
+
* with no negotiation surface between them and the provider. Several releases
|
|
9
|
+
* closed defects whose only symptom, at the old version, is **silence**: a
|
|
10
|
+
* denied credential operation that leaves no audit trace, a keychain write that
|
|
11
|
+
* reports success into a keychain this context's reader cannot see, a malformed
|
|
12
|
+
* key that stores on one backend and reads as absent on the next. A consumer
|
|
13
|
+
* running such a version has no way to learn that from the package — the
|
|
14
|
+
* behaviour looks fine and the logs look clean.
|
|
15
|
+
*
|
|
16
|
+
* So the provider publishes the list. {@link COMPATIBILITY_FLOOR} names each
|
|
17
|
+
* closed gap, the version that closed it, and the symptom it presented before
|
|
18
|
+
* then; {@link assessCompatibility} turns an installed version into a grade a
|
|
19
|
+
* `doctor` command can print.
|
|
20
|
+
*
|
|
21
|
+
* ## What this is NOT
|
|
22
|
+
*
|
|
23
|
+
* **Never a boot-time refusal.** Nothing here runs at import, nothing throws
|
|
24
|
+
* from a constructor, and no vault path consults it. A package that refuses to
|
|
25
|
+
* load because the caller is old converts a documentation problem into an
|
|
26
|
+
* outage. Every function in this module is pure, total, and advisory: malformed
|
|
27
|
+
* input comes back as a typed `"unknown"` grade carrying the reason (P2, No
|
|
28
|
+
* Silent Degradation; P10, Honest Uncertainty), never as a throw.
|
|
29
|
+
*
|
|
30
|
+
* ## What a consumer does with it
|
|
31
|
+
*
|
|
32
|
+
* ```ts
|
|
33
|
+
* import { assessCompatibility, SECRETS_PACKAGE_VERSION } from "@centient/secrets";
|
|
34
|
+
*
|
|
35
|
+
* const assessment = assessCompatibility(SECRETS_PACKAGE_VERSION);
|
|
36
|
+
* if (assessment.grade !== "ok") {
|
|
37
|
+
* for (const gap of assessment.openGaps) {
|
|
38
|
+
* report(`${gap.severity}: ${gap.symptom} (fixed in ${gap.closedIn}, ${gap.reference})`);
|
|
39
|
+
* }
|
|
40
|
+
* }
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* `SECRETS_PACKAGE_VERSION` is the version of the build the consumer actually
|
|
44
|
+
* imported, so the assessment describes the code in that tree — not whatever is
|
|
45
|
+
* latest on the registry. A consumer auditing a *manifest* range instead (`the
|
|
46
|
+
* pin says ^0.6.0 — what does that admit at worst?`) uses {@link assessPin}.
|
|
47
|
+
*
|
|
48
|
+
* Issue: centient-labs/centient-sdk#214 (findings C6-27, C6-29, C6-61).
|
|
49
|
+
*/
|
|
50
|
+
import { compareSemver, formatSemver, tryParseSemver } from "@centient/cli-utils";
|
|
51
|
+
/**
|
|
52
|
+
* The version of THIS build of `@centient/secrets`.
|
|
53
|
+
*
|
|
54
|
+
* Kept in step with `package.json` two ways: `scripts/sync-claudemd-versions.mjs`
|
|
55
|
+
* rewrites it during `pnpm run version-packages` (the same step that syncs the
|
|
56
|
+
* CLAUDE.md package table), and `tests/compatibility.test.ts` fails the build if
|
|
57
|
+
* the two ever disagree. A stale constant here would misgrade every consumer, so
|
|
58
|
+
* drift is a test failure rather than a silent wrong answer.
|
|
59
|
+
*/
|
|
60
|
+
export const SECRETS_PACKAGE_VERSION = "0.11.0";
|
|
61
|
+
/**
|
|
62
|
+
* The oldest version {@link COMPATIBILITY_FLOOR} can honestly describe.
|
|
63
|
+
*
|
|
64
|
+
* `0.5.0` is the oldest release any current consumer pins, and the oldest one
|
|
65
|
+
* whose behaviour the table's entries were written against. Below it the table
|
|
66
|
+
* is not wrong so much as **incomplete** — returning its short list for a 0.4.x
|
|
67
|
+
* install would understate what is open — so {@link assessCompatibility} grades
|
|
68
|
+
* such a version `"unsupported"` and says so, rather than quietly reporting the
|
|
69
|
+
* gaps it happens to know about.
|
|
70
|
+
*
|
|
71
|
+
* This is an epistemic limit on the table, not a support policy and not a gate:
|
|
72
|
+
* an `"unsupported"` grade still returns every gap the table does know about.
|
|
73
|
+
*/
|
|
74
|
+
export const MIN_ASSESSABLE_VERSION = "0.5.0";
|
|
75
|
+
const SEVERITY_RANK = {
|
|
76
|
+
low: 0,
|
|
77
|
+
medium: 1,
|
|
78
|
+
high: 2,
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Every known behavioural gap this package has closed, oldest fix first.
|
|
82
|
+
*
|
|
83
|
+
* Ordering is part of the contract (ascending `closedIn`, then `id`) so that
|
|
84
|
+
* the list a consumer prints reads as an upgrade path rather than an unordered
|
|
85
|
+
* bag; `tests/compatibility.test.ts` pins both the ordering and that every
|
|
86
|
+
* `closedIn` parses as a semver.
|
|
87
|
+
*
|
|
88
|
+
* Entries are added when a release closes a defect whose pre-fix symptom is
|
|
89
|
+
* silent. A fix a consumer would notice on its own does not need a row here.
|
|
90
|
+
*/
|
|
91
|
+
const FLOOR_TABLE = [
|
|
92
|
+
{
|
|
93
|
+
id: "per-consumer-vault-key",
|
|
94
|
+
closedIn: "0.8.0",
|
|
95
|
+
severity: "high",
|
|
96
|
+
symptom: "Every consumer on the host encrypts its vault with the same `centient-vault`/`vault-key` master key: `KeychainProvider` hardcodes the item and `openVault()` resolves its provider internally, so a consumer cannot name or inject a key of its own.",
|
|
97
|
+
reference: "centient-labs/centient-sdk#80",
|
|
98
|
+
adoptionNote: "Additive: pass `openVault({ keyProvider })` or `{ keychain: { service, account } }`. Defaults are unchanged, so an existing vault keeps opening until the consumer opts in — the bump alone does not isolate the key.",
|
|
99
|
+
finding: "C6-28",
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: "denied-operation-audit",
|
|
103
|
+
closedIn: "0.9.0",
|
|
104
|
+
severity: "high",
|
|
105
|
+
symptom: "A policy-denied credential operation leaves no audit trace: the `before` hook throws and no `after` event fires, so denied reads, writes, deletes and enumerations are simply absent from the audit trail.",
|
|
106
|
+
reference: "centient-labs/centient-sdk#120",
|
|
107
|
+
adoptionNote: "`SecretsEventType` gains four `*_rejected` members. A consumer that exhaustively switches on the event type must handle them; one that only matches the prior types is unaffected.",
|
|
108
|
+
finding: "C6-29",
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
id: "libsecret-fallback-silence",
|
|
112
|
+
closedIn: "0.9.0",
|
|
113
|
+
severity: "medium",
|
|
114
|
+
symptom: "On Linux, the libsecret D-Bus enumeration falls back to `secret-tool search` — which briefly materializes secret values on stdout — inside a bare `catch {}`, so a security-relevant degradation happens with no signal at all.",
|
|
115
|
+
reference: "centient-labs/centient-sdk#121",
|
|
116
|
+
finding: "C6-29",
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
id: "credential-key-grammar",
|
|
120
|
+
closedIn: "0.10.0",
|
|
121
|
+
severity: "high",
|
|
122
|
+
symptom: "The documented credential-key grammar is enforced nowhere on the shared path, so a key like `Auth_Token` stores successfully on the macOS keychain and env backends and is unreadable on every other one — and the enforcing backends return `null`, which is indistinguishable from `no such credential`.",
|
|
123
|
+
reference: "centient-labs/centient-sdk#168",
|
|
124
|
+
adoptionNote: "Breaking behaviour change: `storeCredential`/`getCredential`/`deleteCredential`/`listCredentials` now throw `InvalidCredentialKeyError` for a non-conforming key instead of dispatching it. Audit operator-supplied keys before bumping. `listCredentials(prefix)` is validated against the looser prefix grammar, so `\"soma.anthropic.\"` still works.",
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
id: "sessionless-keychain-read",
|
|
128
|
+
closedIn: "0.10.0",
|
|
129
|
+
severity: "high",
|
|
130
|
+
symptom: "On macOS, keychain writes preauthorize no reader (`-T \"\"`) and reads walk the session search list, so a context with no security session — a launchd system-domain daemon, an SSH login — cannot read its own credentials, and a locked login-keychain copy shadows a readable System-keychain one. Every such failure collapses to `null`, reported as `nothing configured`.",
|
|
131
|
+
reference: "centient-labs/centient-sdk#188",
|
|
132
|
+
adoptionNote: "Consumers carrying a private fork of this fix should reconcile deliberately rather than bundling it into a routine bump: the two postures can differ. See centient-labs/centient-sdk#214 (C6-31).",
|
|
133
|
+
finding: "C6-31",
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
id: "sessionless-write-reachability",
|
|
137
|
+
closedIn: "0.10.0",
|
|
138
|
+
severity: "medium",
|
|
139
|
+
symptom: "A sessionless keychain write that falls back to the default keychain for want of privileges is reported as `true` by the boolean accessors, even though this context's reader cannot see it there — a write recorded as a success the reader will never experience.",
|
|
140
|
+
reference: "centient-labs/centient-sdk#188",
|
|
141
|
+
adoptionNote: "Behaviour change on an unchanged signature: from 0.10.0 the boolean means `landed where this context's reader looks`, so a sessionless fallback write now returns `false` where it used to return `true`. Code that branches on `storeCredential`'s boolean should be tested against the new meaning before bumping, or read the discriminated `writeKeychainCredential()` result, which carries the destination and a value-free promotion command.",
|
|
142
|
+
finding: "C6-61",
|
|
143
|
+
},
|
|
144
|
+
];
|
|
145
|
+
/**
|
|
146
|
+
* The published floor — deeply frozen at module initialization (every field on
|
|
147
|
+
* a {@link CompatibilityGap} is a primitive, so freezing each entry plus the
|
|
148
|
+
* array is a complete freeze). `assessCompatibility()` defaults to this same
|
|
149
|
+
* object, so a runtime mutation — a JS consumer, a test helper, a cast past
|
|
150
|
+
* the `readonly` type — would silently change every later assessment in the
|
|
151
|
+
* process. Frozen, the mutation throws at the mutation site instead
|
|
152
|
+
* (mb:f/7e54ba; the same no-silent-degradation stance as the rest of this
|
|
153
|
+
* module).
|
|
154
|
+
*/
|
|
155
|
+
export const COMPATIBILITY_FLOOR = Object.freeze(FLOOR_TABLE.map((gap) => Object.freeze({ ...gap })));
|
|
156
|
+
/**
|
|
157
|
+
* The one place caller input is narrowed to a usable string.
|
|
158
|
+
*
|
|
159
|
+
* Every exported function here is documented as total, and a JS caller — or a
|
|
160
|
+
* `JSON.parse`d manifest, or an `any` at a module boundary — can hand any of
|
|
161
|
+
* them `null`, a number, or an object regardless of what the TypeScript
|
|
162
|
+
* signature says. Rather than trusting each downstream parser to guard for
|
|
163
|
+
* itself (`tryParseSemver` does today; that is an implementation detail of
|
|
164
|
+
* another package, not a contract this module may rest its never-throws
|
|
165
|
+
* promise on), both entry points funnel through this guard, so the two paths
|
|
166
|
+
* cannot drift apart the way they had (`[mb:f/f8ecdf]`).
|
|
167
|
+
*
|
|
168
|
+
* Returns the trimmed string, or `null` for a non-string and for a string that
|
|
169
|
+
* is empty or whitespace-only — inputs no grammar below could accept anyway.
|
|
170
|
+
*/
|
|
171
|
+
function asInputString(value) {
|
|
172
|
+
if (typeof value !== "string")
|
|
173
|
+
return null;
|
|
174
|
+
const trimmed = value.trim();
|
|
175
|
+
return trimmed === "" ? null : trimmed;
|
|
176
|
+
}
|
|
177
|
+
/** `null`/`array`/`number`/… — `typeof` with the two cases it gets wrong fixed. */
|
|
178
|
+
function typeNameOf(value) {
|
|
179
|
+
if (value === null)
|
|
180
|
+
return "null";
|
|
181
|
+
if (Array.isArray(value))
|
|
182
|
+
return "array";
|
|
183
|
+
return typeof value;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Render any value as a string without ever throwing.
|
|
187
|
+
*
|
|
188
|
+
* `String(value)` is not total: an object with a null prototype, or one whose
|
|
189
|
+
* `toString`/`Symbol.toPrimitive` throws, throws from the conversion itself —
|
|
190
|
+
* which would turn *reporting* a bad input into exactly the exception this
|
|
191
|
+
* module promises never to raise.
|
|
192
|
+
*/
|
|
193
|
+
function stringifySafely(value) {
|
|
194
|
+
if (value === null)
|
|
195
|
+
return "null";
|
|
196
|
+
if (value === undefined)
|
|
197
|
+
return "undefined";
|
|
198
|
+
try {
|
|
199
|
+
return String(value);
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
return `[unstringifiable ${typeof value}]`;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/** The input as the `installedVersion` echo field, whose declared type is `string`. */
|
|
206
|
+
function echoInput(value) {
|
|
207
|
+
return typeof value === "string" ? value : stringifySafely(value);
|
|
208
|
+
}
|
|
209
|
+
/** The input as it appears in a `reason`: quoted when a string, named when not. */
|
|
210
|
+
function describeInput(value) {
|
|
211
|
+
if (typeof value === "string")
|
|
212
|
+
return `'${value}'`;
|
|
213
|
+
if (value === null || value === undefined)
|
|
214
|
+
return stringifySafely(value);
|
|
215
|
+
return `a ${typeNameOf(value)} (${stringifySafely(value)})`;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Grade an installed version against the compatibility floor.
|
|
219
|
+
*
|
|
220
|
+
* Pure and total: it reads no environment, touches no keychain, and **never
|
|
221
|
+
* throws** — a version string it cannot parse comes back as
|
|
222
|
+
* `grade: "unknown"` naming the problem. Totality is enforced here rather than
|
|
223
|
+
* assumed of the parser: input is narrowed by this module's own guard, so a JS
|
|
224
|
+
* caller handing it `null`, `undefined`, a number or an object gets the same
|
|
225
|
+
* typed `"unknown"` result as a misspelt version, never an exception. Call it
|
|
226
|
+
* from a `doctor`, a startup log line, or a CI check; do not call it from a
|
|
227
|
+
* code path that would refuse to run on the answer.
|
|
228
|
+
*
|
|
229
|
+
* @param installedVersion - the version actually installed in the consumer's
|
|
230
|
+
* tree. Import {@link SECRETS_PACKAGE_VERSION} for that; a leading `v` and
|
|
231
|
+
* surrounding whitespace are tolerated.
|
|
232
|
+
* @param floor - the gap table to grade against. Defaults to
|
|
233
|
+
* {@link COMPATIBILITY_FLOOR}; injectable so a test can grade a fixture table
|
|
234
|
+
* without pinning itself to the shipped one.
|
|
235
|
+
*/
|
|
236
|
+
export function assessCompatibility(installedVersion, floor = COMPATIBILITY_FLOOR) {
|
|
237
|
+
const base = {
|
|
238
|
+
// Echoed through the guard so the declared `string` type of this field is
|
|
239
|
+
// true at runtime too, for a caller that got here holding a non-string.
|
|
240
|
+
installedVersion: echoInput(installedVersion),
|
|
241
|
+
currentVersion: SECRETS_PACKAGE_VERSION,
|
|
242
|
+
minAssessableVersion: MIN_ASSESSABLE_VERSION,
|
|
243
|
+
};
|
|
244
|
+
const candidate = asInputString(installedVersion);
|
|
245
|
+
const installed = candidate === null ? null : tryParseSemver(candidate);
|
|
246
|
+
if (installed === null) {
|
|
247
|
+
const reason = `${describeInput(installedVersion)} is not a parseable semantic version`;
|
|
248
|
+
return {
|
|
249
|
+
...base,
|
|
250
|
+
grade: "unknown",
|
|
251
|
+
openGaps: [],
|
|
252
|
+
highestSeverity: null,
|
|
253
|
+
reason,
|
|
254
|
+
summary: `@centient/secrets compatibility not assessed: ${reason}`,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
// A gap with an unparseable `closedIn` cannot be positioned against the
|
|
258
|
+
// installed version. Reporting it as closed would understate the risk, so it
|
|
259
|
+
// is reported as OPEN: over-reporting is recoverable, silence is not.
|
|
260
|
+
const openGaps = floor.filter((gap) => {
|
|
261
|
+
const closedIn = tryParseSemver(gap.closedIn);
|
|
262
|
+
return closedIn === null || compareSemver(installed, closedIn) < 0;
|
|
263
|
+
});
|
|
264
|
+
const highestSeverity = openGaps.reduce((worst, gap) => worst === null || SEVERITY_RANK[gap.severity] > SEVERITY_RANK[worst] ? gap.severity : worst, null);
|
|
265
|
+
const minAssessable = tryParseSemver(MIN_ASSESSABLE_VERSION);
|
|
266
|
+
const belowFloor = minAssessable !== null && compareSemver(installed, minAssessable) < 0;
|
|
267
|
+
if (belowFloor) {
|
|
268
|
+
const reason = `${base.installedVersion} is below ${MIN_ASSESSABLE_VERSION}, the oldest version this table describes; ` +
|
|
269
|
+
`the ${openGaps.length} gap(s) listed are what is known, not necessarily all that is open`;
|
|
270
|
+
return {
|
|
271
|
+
...base,
|
|
272
|
+
grade: "unsupported",
|
|
273
|
+
openGaps,
|
|
274
|
+
highestSeverity,
|
|
275
|
+
reason,
|
|
276
|
+
summary: `@centient/secrets ${base.installedVersion} is older than this table covers (below ${MIN_ASSESSABLE_VERSION}); upgrade to ${SECRETS_PACKAGE_VERSION}`,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
if (openGaps.length === 0) {
|
|
280
|
+
return {
|
|
281
|
+
...base,
|
|
282
|
+
grade: "ok",
|
|
283
|
+
openGaps,
|
|
284
|
+
highestSeverity: null,
|
|
285
|
+
summary: `@centient/secrets ${base.installedVersion} has no known open compatibility gaps`,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
const worstFix = openGaps[openGaps.length - 1].closedIn;
|
|
289
|
+
return {
|
|
290
|
+
...base,
|
|
291
|
+
grade: "degraded",
|
|
292
|
+
openGaps,
|
|
293
|
+
highestSeverity,
|
|
294
|
+
summary: `@centient/secrets ${base.installedVersion} has ${openGaps.length} known open gap(s) (highest severity: ${highestSeverity}); all are closed by ${worstFix}`,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* The complete grammar {@link lowestVersionInRange} resolves, anchored.
|
|
299
|
+
*
|
|
300
|
+
* Anchored on purpose. The previous implementation asked only whether the
|
|
301
|
+
* string *started with* `^`, `~` or `>=` and then handed everything after the
|
|
302
|
+
* prefix to the parser, so a composite range like `>=0.6.0 <0.8.0` was rejected
|
|
303
|
+
* only as a side effect of its tail failing to parse. Any parser lenience —
|
|
304
|
+
* ignored build metadata, a tolerated trailing token — would silently reduce a
|
|
305
|
+
* two-sided range to its lower bound and grade a version the pin does not pin
|
|
306
|
+
* (`[mb:f/a6210f]`). Matching the WHOLE range means an unsupported form is
|
|
307
|
+
* rejected by the grammar, never by luck.
|
|
308
|
+
*
|
|
309
|
+
* Accepted, and nothing else:
|
|
310
|
+
*
|
|
311
|
+
* | Form | Example |
|
|
312
|
+
* |---|---|
|
|
313
|
+
* | caret | `^0.6.0` |
|
|
314
|
+
* | tilde | `~0.9.1` |
|
|
315
|
+
* | inclusive lower bound | `>=0.8.0` |
|
|
316
|
+
* | exact | `0.10.0` |
|
|
317
|
+
*
|
|
318
|
+
* The version component is a full `major.minor.patch`, optionally with a
|
|
319
|
+
* conventional leading `v` and a `-prerelease` suffix (kept, not rounded off —
|
|
320
|
+
* see below). Horizontal whitespace is allowed after the operator and around
|
|
321
|
+
* the whole range, and nowhere else.
|
|
322
|
+
*
|
|
323
|
+
* Rejected, each as `null`: composite ranges (`>=0.6.0 <0.8.0`), `||` unions,
|
|
324
|
+
* hyphen ranges (`1.0.0 - 2.0.0`), wildcards (`*`, `""`), x-ranges (`1.x`,
|
|
325
|
+
* `1.2.x`), partial versions (`1.2`), upper bounds and exclusive lower bounds
|
|
326
|
+
* (`<0.9.0`, `>0.9.0`), protocol pins (`workspace:*`, `npm:…`, `file:…`, a git
|
|
327
|
+
* URL), build metadata (`^1.2.3+build` — it is *discarded* by the parser rather
|
|
328
|
+
* than interpreted, which is exactly the ignored-suffix case), and anything
|
|
329
|
+
* carrying a trailing token of any kind.
|
|
330
|
+
*/
|
|
331
|
+
const SUPPORTED_RANGE = /^(\^|~|>=)?[ \t]*(v?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)$/;
|
|
332
|
+
/** One line naming the whole accepted grammar, for a `reason` a consumer reads. */
|
|
333
|
+
const RANGE_GRAMMAR_HELP = "expected exactly '^x.y.z', '~x.y.z', '>=x.y.z' or 'x.y.z' " +
|
|
334
|
+
"(a leading 'v' and a '-prerelease' suffix are allowed; composite ranges, '||' unions, " +
|
|
335
|
+
"hyphen ranges, wildcards, x-ranges, protocol pins and build metadata are not)";
|
|
336
|
+
/**
|
|
337
|
+
* The single strict resolver behind {@link lowestVersionInRange} and
|
|
338
|
+
* {@link assessPin} — the former discards the reason, the latter grades on it.
|
|
339
|
+
*
|
|
340
|
+
* Two independent checks, both of which must pass: {@link SUPPORTED_RANGE}
|
|
341
|
+
* validates the *shape* of the entire trimmed range, and `tryParseSemver`
|
|
342
|
+
* validates the *contents* of the captured version (per-identifier pre-release
|
|
343
|
+
* rules live there, and stay there). Neither is asked to stand in for the other.
|
|
344
|
+
*/
|
|
345
|
+
function resolveRange(range) {
|
|
346
|
+
const candidate = asInputString(range);
|
|
347
|
+
if (candidate === null) {
|
|
348
|
+
return {
|
|
349
|
+
ok: false,
|
|
350
|
+
reason: `${describeInput(range)} is not a range form this helper resolves (${RANGE_GRAMMAR_HELP})`,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
const match = SUPPORTED_RANGE.exec(candidate);
|
|
354
|
+
if (match === null) {
|
|
355
|
+
return {
|
|
356
|
+
ok: false,
|
|
357
|
+
reason: `'${candidate}' is not a range form this helper resolves (${RANGE_GRAMMAR_HELP})`,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
const parsed = tryParseSemver(match[2]);
|
|
361
|
+
if (parsed === null) {
|
|
362
|
+
return {
|
|
363
|
+
ok: false,
|
|
364
|
+
reason: `'${candidate}' has a version component that is not a parseable semantic version (${RANGE_GRAMMAR_HELP})`,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
// Re-serialized rather than returned verbatim, so a leading `v` or stray
|
|
368
|
+
// whitespace is normalized away — but the pre-release suffix is KEPT.
|
|
369
|
+
// Dropping it would round `^1.0.0-rc.1` up to `1.0.0` and credit the consumer
|
|
370
|
+
// for a release it is not running.
|
|
371
|
+
return { ok: true, version: formatSemver(parsed) };
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* The lowest version a dependency range admits, or `null` when the range form
|
|
375
|
+
* is one this helper does not understand.
|
|
376
|
+
*
|
|
377
|
+
* Deliberately narrow: it recognises the forms that actually appear in the
|
|
378
|
+
* consumers' manifests — `^x.y.z`, `~x.y.z`, `>=x.y.z` and a bare `x.y.z` — and
|
|
379
|
+
* returns `null` for everything else rather than guessing. A guess here would
|
|
380
|
+
* grade the wrong version and report a clean bill of health for a pin nobody
|
|
381
|
+
* checked.
|
|
382
|
+
*
|
|
383
|
+
* The accepted grammar is matched whole and enumerated on
|
|
384
|
+
* {@link SUPPORTED_RANGE}; nothing outside it is partially interpreted. Total:
|
|
385
|
+
* a non-string, an empty string, and every unsupported form all return `null`.
|
|
386
|
+
*/
|
|
387
|
+
export function lowestVersionInRange(range) {
|
|
388
|
+
const resolved = resolveRange(range);
|
|
389
|
+
return resolved.ok ? resolved.version : null;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Grade a manifest **pin** by its worst admissible version.
|
|
393
|
+
*
|
|
394
|
+
* `assessCompatibility` answers "what is open in the build I imported"; this
|
|
395
|
+
* answers "what could be open in any build this pin admits". A `^0.6.0` pin can
|
|
396
|
+
* resolve to any 0.6.x, so the version worth grading is its base — which is also
|
|
397
|
+
* the one a lockfile that has never been refreshed is most likely holding.
|
|
398
|
+
*
|
|
399
|
+
* Returns `grade: "unknown"` carrying the reason for a range form
|
|
400
|
+
* {@link lowestVersionInRange} does not recognise — including a non-string a JS
|
|
401
|
+
* caller slipped past the signature. Never throws.
|
|
402
|
+
*/
|
|
403
|
+
export function assessPin(pin, floor = COMPATIBILITY_FLOOR) {
|
|
404
|
+
const resolved = resolveRange(pin);
|
|
405
|
+
if (!resolved.ok) {
|
|
406
|
+
return {
|
|
407
|
+
grade: "unknown",
|
|
408
|
+
installedVersion: echoInput(pin),
|
|
409
|
+
currentVersion: SECRETS_PACKAGE_VERSION,
|
|
410
|
+
minAssessableVersion: MIN_ASSESSABLE_VERSION,
|
|
411
|
+
openGaps: [],
|
|
412
|
+
highestSeverity: null,
|
|
413
|
+
reason: resolved.reason,
|
|
414
|
+
summary: `@centient/secrets compatibility not assessed: ${resolved.reason}`,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
return assessCompatibility(resolved.version, floor);
|
|
418
|
+
}
|
|
419
|
+
//# sourceMappingURL=compatibility.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compatibility.js","sourceRoot":"","sources":["../src/compatibility.ts"],"names":[],"mappings":"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"}
|