imbas-verify 0.0.0-stage → 0.1.1
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/CHANGELOG.md +35 -0
- package/FORMAT.md +155 -0
- package/LICENSE +17 -0
- package/NOTICE +21 -0
- package/README.md +77 -2
- package/args.d.ts +46 -0
- package/args.js +113 -0
- package/attestation.d.ts +178 -0
- package/attestation.js +199 -0
- package/describe.d.ts +8 -0
- package/describe.js +15 -0
- package/dsse.d.ts +130 -0
- package/dsse.js +211 -0
- package/key.d.ts +132 -0
- package/key.js +142 -0
- package/package.json +43 -4
- package/read-file.d.ts +33 -0
- package/read-file.js +25 -0
- package/record.d.ts +127 -0
- package/record.js +1 -0
- package/retirement.d.ts +337 -0
- package/retirement.js +521 -0
- package/statement.d.ts +60 -0
- package/statement.js +56 -0
- package/verify-cli.d.ts +2 -0
- package/verify-cli.js +34 -0
- package/verify.d.ts +6 -0
- package/verify.js +145 -0
package/key.d.ts
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The installation key.
|
|
3
|
+
*
|
|
4
|
+
* Every signed record Imbas produces is signed by one key pair that belongs to this
|
|
5
|
+
* installation and nowhere else. That is a deliberate limit, not a shortcoming:
|
|
6
|
+
* Sigstore's keyless flow publishes the signer's identity to a public transparency
|
|
7
|
+
* log, and an operator running Imbas on their own machine should not have to announce
|
|
8
|
+
* to the world that they approved a change. So the key is local, the public half is
|
|
9
|
+
* the only thing that ever leaves, and what a signature proves is bounded by that —
|
|
10
|
+
* see `proves` / `does_not_prove` in the predicate.
|
|
11
|
+
*
|
|
12
|
+
* The private key is written once, with the tightest permission the OS offers, and is
|
|
13
|
+
* never read into a response, a log line, a provenance record or an export. Nothing
|
|
14
|
+
* in this module hands it out: the loader in `signing.ts` returns a handle whose only
|
|
15
|
+
* outward key material is `publicPem()`. The private PEM lives in a closure, so a
|
|
16
|
+
* caller cannot find it by spreading the object or serialising it.
|
|
17
|
+
*
|
|
18
|
+
* This file is compiled into the published verifier, so it holds only what *checking* a
|
|
19
|
+
* signature needs. Key creation and signing live in `signing.ts`, which the verifier does
|
|
20
|
+
* not compile — the tool that checks the evidence must not be able to manufacture it.
|
|
21
|
+
*/
|
|
22
|
+
import { type KeyObject } from "node:crypto";
|
|
23
|
+
/** Where the installation key lives inside a data dir. */
|
|
24
|
+
export declare function keyDir(dataDir: string): string;
|
|
25
|
+
export declare function privateKeyPath(dataDir: string): string;
|
|
26
|
+
export declare function publicKeyPath(dataDir: string): string;
|
|
27
|
+
/** sha256 of arbitrary bytes, hex. The DER export is bytes, so this is not `ids.sha256`. */
|
|
28
|
+
export declare function sha256Bytes(bytes: Buffer): string;
|
|
29
|
+
/**
|
|
30
|
+
* The keyid: sha256 of the DER SubjectPublicKeyInfo, hex.
|
|
31
|
+
*
|
|
32
|
+
* Over the DER rather than the PEM on purpose. PEM is base64 of the DER with line
|
|
33
|
+
* breaks and a header, so two encoders can produce different PEM text for the same
|
|
34
|
+
* key — and a keyid that changed with the formatting would be useless as an
|
|
35
|
+
* identifier of the key. The DER is the key; the PEM is a way of writing it down.
|
|
36
|
+
*/
|
|
37
|
+
export declare function keyIdOf(publicKey: KeyObject): string;
|
|
38
|
+
/** Reads a keyid from a PEM public key without creating anything. */
|
|
39
|
+
export declare function keyIdOfPem(publicPem: string): string;
|
|
40
|
+
/**
|
|
41
|
+
* Reads a keyid, or null when the text is not a public key at all.
|
|
42
|
+
*
|
|
43
|
+
* Verifying is often the first thing anyone runs on a fresh machine, with a key file
|
|
44
|
+
* they were handed and did not inspect. A DER file, a private key, an HTML error page
|
|
45
|
+
* saved by mistake — all of these should produce "that is not a public key" and not a
|
|
46
|
+
* stack trace from node:crypto. The distinction matters: an unhandled exception tells
|
|
47
|
+
* the operator the tool is broken, when the truth is that their input is wrong.
|
|
48
|
+
*/
|
|
49
|
+
export declare function tryKeyIdOfPem(text: string): string | null;
|
|
50
|
+
export declare class KeyError extends Error {
|
|
51
|
+
readonly code: string;
|
|
52
|
+
constructor(code: string, message: string);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The installation key, as the rest of the engine sees it.
|
|
56
|
+
*
|
|
57
|
+
* There is no `privatePem` field, and that is the point: the private key exists only
|
|
58
|
+
* inside the closure these methods share, so no caller can log it, serialise the
|
|
59
|
+
* handle and find it, or return it from an endpoint by spreading the object.
|
|
60
|
+
*/
|
|
61
|
+
export interface InstallationKey {
|
|
62
|
+
/** sha256 of the DER SPKI, hex. The `keyid` in every envelope. */
|
|
63
|
+
readonly keyid: string;
|
|
64
|
+
/** The public key as PEM. The only key material that may leave the process. */
|
|
65
|
+
publicPem(): string;
|
|
66
|
+
/** ECDSA P-256 / SHA-256 over these bytes. DER-encoded, as `sign` returns. */
|
|
67
|
+
signBytes(bytes: Buffer): Buffer;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Verifies a signature against a PEM public key.
|
|
71
|
+
*
|
|
72
|
+
* `dsaEncoding: "der"` is the default and is stated because it is load-bearing:
|
|
73
|
+
* ECDSA signatures have two encodings, DER and IEEE-P1363 ("raw"), and a verifier that
|
|
74
|
+
* guesses wrong reports a valid signature as forged. DER is what `openssl dgst
|
|
75
|
+
* -verify` expects, which is what makes an independent check possible at all.
|
|
76
|
+
*/
|
|
77
|
+
export declare function verifyBytes(publicPem: string, bytes: Buffer, signature: Buffer): boolean;
|
|
78
|
+
/** Builds a handle around a private key already in memory. */
|
|
79
|
+
/**
|
|
80
|
+
* Loads the installation key, generating it on first use.
|
|
81
|
+
*
|
|
82
|
+
* Creating it lazily rather than at install time keeps the promise simple: the key
|
|
83
|
+
* exists when it is first needed, in the data dir, with no separate setup step that
|
|
84
|
+
* can be skipped and leave signing broken later.
|
|
85
|
+
*/
|
|
86
|
+
/**
|
|
87
|
+
* Loads or creates a key pair at explicit paths.
|
|
88
|
+
*
|
|
89
|
+
* Extracted so the policy key — which signs retirement lists and must be a *different*
|
|
90
|
+
* key from the one that signs records — shares one implementation with the installation
|
|
91
|
+
* key rather than a copy. A second copy of the permission dance is a second place for it to
|
|
92
|
+
* drift, and the drift would be invisible on Windows where the mode bits are advisory.
|
|
93
|
+
*/
|
|
94
|
+
/**
|
|
95
|
+
* The policy key: what signs a retirement list, and nothing else.
|
|
96
|
+
*
|
|
97
|
+
* A second key for one reason, and it is the whole reason this exists. The attack key
|
|
98
|
+
* retirement defends against is a **stolen installation key**. If the retirement list were
|
|
99
|
+
* signed by that same key, the thief could hand out a confident "no keys were ever
|
|
100
|
+
* retired" and clear themselves — the list would prove nothing against exactly the threat
|
|
101
|
+
* it was built for. A policy key that can be kept somewhere else is what makes the list
|
|
102
|
+
* worth signing at all.
|
|
103
|
+
*
|
|
104
|
+
* It is created on first use like the installation key, so an operator who never retires
|
|
105
|
+
* anything never gets one. `docs/EVIDENCE.md` is explicit about the limit this does *not*
|
|
106
|
+
* remove: on a single machine both private keys sit in the same directory, and whoever
|
|
107
|
+
* holds the policy key can sign a false list.
|
|
108
|
+
*/
|
|
109
|
+
export declare function policyKeyDir(dataDir: string): string;
|
|
110
|
+
export declare function policyPrivateKeyPath(dataDir: string): string;
|
|
111
|
+
export declare function policyPublicKeyPath(dataDir: string): string;
|
|
112
|
+
/** An in-memory key pair, for tests and the demo. Never written to disk. */
|
|
113
|
+
/**
|
|
114
|
+
* Builds a signing handle from a private key PEM already in memory.
|
|
115
|
+
*
|
|
116
|
+
* The counter-signing path needs this: a second party signs a record with a key that is
|
|
117
|
+
* not this installation's, and `loadOrCreateKey` would be wrong in two ways — it reads the
|
|
118
|
+
* data dir, and it *creates* a key when there is none. A counter-signer who silently
|
|
119
|
+
* generated a fresh key would sign with something nobody has the public half of, and the
|
|
120
|
+
* record would carry a signature no verifier could ever check.
|
|
121
|
+
*
|
|
122
|
+
* Refuses anything that is not a private key, by name, rather than throwing an OpenSSL
|
|
123
|
+
* error. The keyid is derived from the public half of the same key, so a handle can never
|
|
124
|
+
* disagree with itself about which key it is.
|
|
125
|
+
*/
|
|
126
|
+
export type PrivateKeyRead = {
|
|
127
|
+
ok: true;
|
|
128
|
+
key: InstallationKey;
|
|
129
|
+
} | {
|
|
130
|
+
ok: false;
|
|
131
|
+
message: string;
|
|
132
|
+
};
|
package/key.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The installation key.
|
|
3
|
+
*
|
|
4
|
+
* Every signed record Imbas produces is signed by one key pair that belongs to this
|
|
5
|
+
* installation and nowhere else. That is a deliberate limit, not a shortcoming:
|
|
6
|
+
* Sigstore's keyless flow publishes the signer's identity to a public transparency
|
|
7
|
+
* log, and an operator running Imbas on their own machine should not have to announce
|
|
8
|
+
* to the world that they approved a change. So the key is local, the public half is
|
|
9
|
+
* the only thing that ever leaves, and what a signature proves is bounded by that —
|
|
10
|
+
* see `proves` / `does_not_prove` in the predicate.
|
|
11
|
+
*
|
|
12
|
+
* The private key is written once, with the tightest permission the OS offers, and is
|
|
13
|
+
* never read into a response, a log line, a provenance record or an export. Nothing
|
|
14
|
+
* in this module hands it out: the loader in `signing.ts` returns a handle whose only
|
|
15
|
+
* outward key material is `publicPem()`. The private PEM lives in a closure, so a
|
|
16
|
+
* caller cannot find it by spreading the object or serialising it.
|
|
17
|
+
*
|
|
18
|
+
* This file is compiled into the published verifier, so it holds only what *checking* a
|
|
19
|
+
* signature needs. Key creation and signing live in `signing.ts`, which the verifier does
|
|
20
|
+
* not compile — the tool that checks the evidence must not be able to manufacture it.
|
|
21
|
+
*/
|
|
22
|
+
import { createHash, createPublicKey, verify } from "node:crypto";
|
|
23
|
+
import { join, resolve } from "node:path";
|
|
24
|
+
/** Where the installation key lives inside a data dir. */
|
|
25
|
+
export function keyDir(dataDir) {
|
|
26
|
+
return join(resolve(dataDir), "keys");
|
|
27
|
+
}
|
|
28
|
+
export function privateKeyPath(dataDir) {
|
|
29
|
+
return join(keyDir(dataDir), "installation-key.pem");
|
|
30
|
+
}
|
|
31
|
+
export function publicKeyPath(dataDir) {
|
|
32
|
+
return join(keyDir(dataDir), "installation-key.pub.pem");
|
|
33
|
+
}
|
|
34
|
+
/** sha256 of arbitrary bytes, hex. The DER export is bytes, so this is not `ids.sha256`. */
|
|
35
|
+
export function sha256Bytes(bytes) {
|
|
36
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The keyid: sha256 of the DER SubjectPublicKeyInfo, hex.
|
|
40
|
+
*
|
|
41
|
+
* Over the DER rather than the PEM on purpose. PEM is base64 of the DER with line
|
|
42
|
+
* breaks and a header, so two encoders can produce different PEM text for the same
|
|
43
|
+
* key — and a keyid that changed with the formatting would be useless as an
|
|
44
|
+
* identifier of the key. The DER is the key; the PEM is a way of writing it down.
|
|
45
|
+
*/
|
|
46
|
+
export function keyIdOf(publicKey) {
|
|
47
|
+
return sha256Bytes(publicKey.export({ type: "spki", format: "der" }));
|
|
48
|
+
}
|
|
49
|
+
/** Reads a keyid from a PEM public key without creating anything. */
|
|
50
|
+
export function keyIdOfPem(publicPem) {
|
|
51
|
+
return keyIdOf(createPublicKey(publicPem));
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Reads a keyid, or null when the text is not a public key at all.
|
|
55
|
+
*
|
|
56
|
+
* Verifying is often the first thing anyone runs on a fresh machine, with a key file
|
|
57
|
+
* they were handed and did not inspect. A DER file, a private key, an HTML error page
|
|
58
|
+
* saved by mistake — all of these should produce "that is not a public key" and not a
|
|
59
|
+
* stack trace from node:crypto. The distinction matters: an unhandled exception tells
|
|
60
|
+
* the operator the tool is broken, when the truth is that their input is wrong.
|
|
61
|
+
*/
|
|
62
|
+
export function tryKeyIdOfPem(text) {
|
|
63
|
+
try {
|
|
64
|
+
return keyIdOfPem(text);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export class KeyError extends Error {
|
|
71
|
+
code;
|
|
72
|
+
constructor(code, message) {
|
|
73
|
+
super(message);
|
|
74
|
+
this.code = code;
|
|
75
|
+
this.name = "KeyError";
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Verifies a signature against a PEM public key.
|
|
80
|
+
*
|
|
81
|
+
* `dsaEncoding: "der"` is the default and is stated because it is load-bearing:
|
|
82
|
+
* ECDSA signatures have two encodings, DER and IEEE-P1363 ("raw"), and a verifier that
|
|
83
|
+
* guesses wrong reports a valid signature as forged. DER is what `openssl dgst
|
|
84
|
+
* -verify` expects, which is what makes an independent check possible at all.
|
|
85
|
+
*/
|
|
86
|
+
export function verifyBytes(publicPem, bytes, signature) {
|
|
87
|
+
let key;
|
|
88
|
+
try {
|
|
89
|
+
key = createPublicKey(publicPem);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
return verify("sha256", bytes, { key, dsaEncoding: "der" }, signature);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// A malformed signature (wrong length, garbage) makes `verify` throw rather than
|
|
99
|
+
// return false. Both mean "not verified", and only one of them is an exception.
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/** Builds a handle around a private key already in memory. */
|
|
104
|
+
/**
|
|
105
|
+
* Loads the installation key, generating it on first use.
|
|
106
|
+
*
|
|
107
|
+
* Creating it lazily rather than at install time keeps the promise simple: the key
|
|
108
|
+
* exists when it is first needed, in the data dir, with no separate setup step that
|
|
109
|
+
* can be skipped and leave signing broken later.
|
|
110
|
+
*/
|
|
111
|
+
/**
|
|
112
|
+
* Loads or creates a key pair at explicit paths.
|
|
113
|
+
*
|
|
114
|
+
* Extracted so the policy key — which signs retirement lists and must be a *different*
|
|
115
|
+
* key from the one that signs records — shares one implementation with the installation
|
|
116
|
+
* key rather than a copy. A second copy of the permission dance is a second place for it to
|
|
117
|
+
* drift, and the drift would be invisible on Windows where the mode bits are advisory.
|
|
118
|
+
*/
|
|
119
|
+
/**
|
|
120
|
+
* The policy key: what signs a retirement list, and nothing else.
|
|
121
|
+
*
|
|
122
|
+
* A second key for one reason, and it is the whole reason this exists. The attack key
|
|
123
|
+
* retirement defends against is a **stolen installation key**. If the retirement list were
|
|
124
|
+
* signed by that same key, the thief could hand out a confident "no keys were ever
|
|
125
|
+
* retired" and clear themselves — the list would prove nothing against exactly the threat
|
|
126
|
+
* it was built for. A policy key that can be kept somewhere else is what makes the list
|
|
127
|
+
* worth signing at all.
|
|
128
|
+
*
|
|
129
|
+
* It is created on first use like the installation key, so an operator who never retires
|
|
130
|
+
* anything never gets one. `docs/EVIDENCE.md` is explicit about the limit this does *not*
|
|
131
|
+
* remove: on a single machine both private keys sit in the same directory, and whoever
|
|
132
|
+
* holds the policy key can sign a false list.
|
|
133
|
+
*/
|
|
134
|
+
export function policyKeyDir(dataDir) {
|
|
135
|
+
return join(resolve(dataDir), "keys");
|
|
136
|
+
}
|
|
137
|
+
export function policyPrivateKeyPath(dataDir) {
|
|
138
|
+
return join(policyKeyDir(dataDir), "policy-key.pem");
|
|
139
|
+
}
|
|
140
|
+
export function policyPublicKeyPath(dataDir) {
|
|
141
|
+
return join(policyKeyDir(dataDir), "policy-key.pub.pem");
|
|
142
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,45 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "imbas-verify",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Check a signed Imbas approval record without the engine. Verifies signatures, the patch digest and the provenance chain head; reads only the files you name.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"imbas-verify": "verify-cli.js"
|
|
9
|
+
},
|
|
10
|
+
"main": "./verify.js",
|
|
11
|
+
"types": "./verify.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./verify.d.ts",
|
|
15
|
+
"default": "./verify.js"
|
|
16
|
+
},
|
|
17
|
+
"./args": {
|
|
18
|
+
"types": "./args.d.ts",
|
|
19
|
+
"default": "./args.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"*.js",
|
|
24
|
+
"*.d.ts",
|
|
25
|
+
"README.md",
|
|
26
|
+
"FORMAT.md",
|
|
27
|
+
"CHANGELOG.md",
|
|
28
|
+
"LICENSE",
|
|
29
|
+
"NOTICE"
|
|
30
|
+
],
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=22"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"attestation",
|
|
36
|
+
"provenance",
|
|
37
|
+
"supply-chain",
|
|
38
|
+
"dsse",
|
|
39
|
+
"in-toto",
|
|
40
|
+
"verification"
|
|
41
|
+
],
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
}
|
|
45
|
+
}
|
package/read-file.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads a text file, returning a named failure instead of throwing.
|
|
3
|
+
*
|
|
4
|
+
* The same reasoning as `readAttestationFile`: a path the operator typed wrong must
|
|
5
|
+
* produce "cannot read that file", not a stack trace from `node:fs`. Verify is usually
|
|
6
|
+
* the first thing anyone runs on a fresh machine, with paths they were given.
|
|
7
|
+
*/
|
|
8
|
+
export type TextRead = {
|
|
9
|
+
ok: true;
|
|
10
|
+
text: string;
|
|
11
|
+
} | {
|
|
12
|
+
ok: false;
|
|
13
|
+
check: "key_unreadable";
|
|
14
|
+
message: string;
|
|
15
|
+
};
|
|
16
|
+
export declare function readTextFile(path: string): TextRead;
|
|
17
|
+
/**
|
|
18
|
+
* Reads a file as raw bytes, for the patch.
|
|
19
|
+
*
|
|
20
|
+
* Separate from `readTextFile` because the patch digest is over bytes. Decoding to a
|
|
21
|
+
* string and re-encoding would go through UTF-8 a second time, and a patch is not
|
|
22
|
+
* guaranteed to be valid UTF-8 - the digest would then be of different bytes than the
|
|
23
|
+
* ones that were approved, and an honest file would report a mismatch.
|
|
24
|
+
*/
|
|
25
|
+
export type BinaryRead = {
|
|
26
|
+
ok: true;
|
|
27
|
+
bytes: Buffer;
|
|
28
|
+
} | {
|
|
29
|
+
ok: false;
|
|
30
|
+
check: "patch_unreadable";
|
|
31
|
+
message: string;
|
|
32
|
+
};
|
|
33
|
+
export declare function readBinaryFile(path: string): BinaryRead;
|
package/read-file.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File reads that return a named failure instead of throwing.
|
|
3
|
+
*
|
|
4
|
+
* Split from `attest.ts` for the same reason as `verify.ts`: the verifier needs these and
|
|
5
|
+
* nothing else in that module needs the engine.
|
|
6
|
+
*/
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
export function readTextFile(path) {
|
|
9
|
+
try {
|
|
10
|
+
return { ok: true, text: readFileSync(path, "utf8") };
|
|
11
|
+
}
|
|
12
|
+
catch (cause) {
|
|
13
|
+
const code = cause.code ?? "unknown";
|
|
14
|
+
return { ok: false, check: "key_unreadable", message: `Cannot read ${path} (${code}).` };
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function readBinaryFile(path) {
|
|
18
|
+
try {
|
|
19
|
+
return { ok: true, bytes: readFileSync(path) };
|
|
20
|
+
}
|
|
21
|
+
catch (cause) {
|
|
22
|
+
const code = cause.code ?? "unknown";
|
|
23
|
+
return { ok: false, check: "patch_unreadable", message: `Cannot read ${path} (${code}).` };
|
|
24
|
+
}
|
|
25
|
+
}
|
package/record.d.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shape of the signed approval predicate, declared once.
|
|
3
|
+
*
|
|
4
|
+
* These interfaces lived in `predicate.ts`, beside the code that *builds* them. That was
|
|
5
|
+
* wrong for one reason: `attestation.ts` (the verifier) needs to name the type, and a type
|
|
6
|
+
* import still makes the compiler load the module it names. `predicate.ts` imports the
|
|
7
|
+
* gates, the pipeline state and the provenance store, so verifying a record dragged the
|
|
8
|
+
* whole engine into the program — and, in the published verifier, into the closure.
|
|
9
|
+
*
|
|
10
|
+
* Declaring them here means the verifier names nothing outside the package at all — not
|
|
11
|
+
* even in a type import. That matters twice over: a type import still makes the compiler
|
|
12
|
+
* load the named module while building, and it writes the name into the emitted `.d.ts`,
|
|
13
|
+
* where a consumer who installed only this package would see `any` or a typecheck error.
|
|
14
|
+
*
|
|
15
|
+
* `StageId` and `GateKind` therefore carry local declarations rather than imports, and
|
|
16
|
+
* `engine/test/verify-standalone.test.ts` asserts they still agree with the contract.
|
|
17
|
+
*
|
|
18
|
+
* `predicate.ts` re-exports everything in this file, so existing imports are unchanged.
|
|
19
|
+
* There is still exactly one declaration of the predicate: this one.
|
|
20
|
+
*/
|
|
21
|
+
export type StageId = "intake" | "research" | "plan" | "build" | "review" | "demo" | "notify" | "handoff";
|
|
22
|
+
export type GateKind = "operator" | "engineer";
|
|
23
|
+
/** What the record says about one gate. */
|
|
24
|
+
export interface PredicateGate {
|
|
25
|
+
stage: StageId;
|
|
26
|
+
kind: GateKind;
|
|
27
|
+
/** The decision, or null if the gate was never decided. */
|
|
28
|
+
decision: "approve" | "reject" | null;
|
|
29
|
+
gate_id: string;
|
|
30
|
+
/**
|
|
31
|
+
* The artifact this gate decided, or null at a decision-only stage.
|
|
32
|
+
*
|
|
33
|
+
* HANDOFF produces nothing, so there is nothing to point at. Null says that
|
|
34
|
+
* plainly; a placeholder id would claim an artifact that never existed.
|
|
35
|
+
*/
|
|
36
|
+
artifact_id: string | null;
|
|
37
|
+
requested_at: string;
|
|
38
|
+
decided_at: string | null;
|
|
39
|
+
principal_id: string | null;
|
|
40
|
+
principal_role: string | null;
|
|
41
|
+
authenticated_via: string | null;
|
|
42
|
+
/** Which explainer the approver acknowledged. Invariant 5, recorded. */
|
|
43
|
+
acknowledged_explainer_id: string | null;
|
|
44
|
+
explainer_view: string | null;
|
|
45
|
+
}
|
|
46
|
+
/** One stage in the pipeline that did not run. */
|
|
47
|
+
export interface PredicateNotRun {
|
|
48
|
+
stage: StageId;
|
|
49
|
+
/**
|
|
50
|
+
* Why, in the words that were shown when the stage was stepped over. Copied from
|
|
51
|
+
* the `stage_unavailable` record's explainer, never rewritten for the signature: a
|
|
52
|
+
* reason that reads differently in the record than in the log is a reason a reader
|
|
53
|
+
* cannot check.
|
|
54
|
+
*/
|
|
55
|
+
reason: string;
|
|
56
|
+
}
|
|
57
|
+
/** One override, with the per-finding reasons exactly as they were recorded. */
|
|
58
|
+
export interface PredicateOverride {
|
|
59
|
+
override_id: string;
|
|
60
|
+
stage: StageId;
|
|
61
|
+
overridden_verdict: "changes_requested";
|
|
62
|
+
reviewer_artifact_id: string;
|
|
63
|
+
reviewer_principal_id: string;
|
|
64
|
+
reason: string;
|
|
65
|
+
finding_reasons: Array<{
|
|
66
|
+
finding_id: string;
|
|
67
|
+
reason: string;
|
|
68
|
+
}>;
|
|
69
|
+
/** Who set the verdict aside, and when. */
|
|
70
|
+
principal_id: string | null;
|
|
71
|
+
decided_at: string | null;
|
|
72
|
+
/** The provenance record carrying it, so a reader can find it in the chain. */
|
|
73
|
+
record_id: string;
|
|
74
|
+
}
|
|
75
|
+
/** A model that took part, and what it was, exactly as provenance recorded it. */
|
|
76
|
+
export interface PredicateModel {
|
|
77
|
+
stage: StageId;
|
|
78
|
+
adapter: string;
|
|
79
|
+
provider: string;
|
|
80
|
+
model: string;
|
|
81
|
+
mocked: boolean;
|
|
82
|
+
prompt_version: string | null;
|
|
83
|
+
}
|
|
84
|
+
export interface ApprovalPredicate {
|
|
85
|
+
/** The name and version of this predicate, so a reader can find the definition. */
|
|
86
|
+
predicate_name: "imbas.approval";
|
|
87
|
+
predicate_version: "1";
|
|
88
|
+
schema_version: string;
|
|
89
|
+
job_id: string;
|
|
90
|
+
job_title: string;
|
|
91
|
+
sample: boolean;
|
|
92
|
+
created_at: string;
|
|
93
|
+
completed_at: string;
|
|
94
|
+
/** Every gate in the job, in pipeline order. */
|
|
95
|
+
gates: PredicateGate[];
|
|
96
|
+
/** Every override, in the order they were recorded. */
|
|
97
|
+
overrides: PredicateOverride[];
|
|
98
|
+
/**
|
|
99
|
+
* Every stage in the pipeline that did not run, and why, copied from its
|
|
100
|
+
* `stage_unavailable` record.
|
|
101
|
+
*
|
|
102
|
+
* Signed rather than only logged, because the claim this record makes is "production
|
|
103
|
+
* was authorised for what was actually checked". A job that reached the engineer gate
|
|
104
|
+
* with DEMO and NOTIFY unexecuted is still a legitimate job — but a reader of the
|
|
105
|
+
* record must be able to see the gap without also being trusted to have read the
|
|
106
|
+
* chain. Empty means every stage ran; it is never derived by subtracting from the
|
|
107
|
+
* list of stages that happened to produce an artifact.
|
|
108
|
+
*/
|
|
109
|
+
stages_not_run: PredicateNotRun[];
|
|
110
|
+
/** Every adapter that produced an artifact, in the order they ran. */
|
|
111
|
+
models: PredicateModel[];
|
|
112
|
+
/** The head of the provenance chain. Verifying the chain verifies this many records. */
|
|
113
|
+
provenance: {
|
|
114
|
+
head_hash: string;
|
|
115
|
+
record_count: number;
|
|
116
|
+
chain_valid: boolean;
|
|
117
|
+
};
|
|
118
|
+
/** The versions the record was produced under, so a reader can place it in time. */
|
|
119
|
+
versions: {
|
|
120
|
+
contracts: string;
|
|
121
|
+
glossary: string;
|
|
122
|
+
};
|
|
123
|
+
/** The installation whose key signed this. */
|
|
124
|
+
installation_keyid: string;
|
|
125
|
+
proves: string[];
|
|
126
|
+
does_not_prove: string[];
|
|
127
|
+
}
|
package/record.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|