@pipeline-builder/api-core 4.1.20 → 4.1.22
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/lib/helpers/access-helpers.d.ts +50 -2
- package/lib/helpers/access-helpers.js +44 -3
- package/lib/helpers/index.d.ts +1 -1
- package/lib/helpers/index.js +2 -2
- package/lib/middleware/ask-provenance.d.ts +53 -0
- package/lib/middleware/ask-provenance.js +26 -0
- package/lib/middleware/index.d.ts +1 -0
- package/lib/middleware/index.js +2 -1
- package/lib/services/http-client.d.ts +0 -1
- package/lib/services/http-client.js +5 -7
- package/lib/services/index.d.ts +2 -2
- package/lib/services/index.js +3 -3
- package/lib/services/remote-audit-client.d.ts +1 -15
- package/lib/services/remote-audit-client.js +2 -295
- package/lib/services/s3-client.d.ts +54 -0
- package/lib/services/s3-client.js +106 -0
- package/lib/types/ask-proposals.d.ts +269 -0
- package/lib/types/ask-proposals.js +308 -0
- package/lib/types/compliance-exemption.d.ts +24 -0
- package/lib/types/compliance-exemption.js +4 -0
- package/lib/types/index.d.ts +5 -2
- package/lib/types/index.js +5 -2
- package/lib/types/remote-audit-actions.d.ts +23 -0
- package/lib/types/remote-audit-actions.js +305 -0
- package/lib/utils/date.d.ts +19 -0
- package/lib/utils/date.js +22 -0
- package/lib/utils/dsse.d.ts +32 -0
- package/lib/utils/dsse.js +55 -0
- package/lib/utils/index.d.ts +3 -1
- package/lib/utils/index.js +4 -2
- package/lib/utils/response.d.ts +34 -4
- package/lib/utils/response.js +30 -5
- package/lib/utils/sensitive-patterns.js +17 -1
- package/lib/validation/index.d.ts +2 -2
- package/lib/validation/index.js +3 -3
- package/package.json +14 -1
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared S3-compatible client, and the bucket-ensure backstop that goes
|
|
3
|
+
* with it.
|
|
4
|
+
*
|
|
5
|
+
* Backed by MinIO in every environment (self-hosted, no AWS-account coupling);
|
|
6
|
+
* because MinIO speaks the S3 API this uses the AWS S3 SDK, so a real-S3 swap is
|
|
7
|
+
* a pure config change (drop the custom endpoint + path-style, supply IAM creds).
|
|
8
|
+
*
|
|
9
|
+
* Config (env) — one set of names, read identically by every service; each
|
|
10
|
+
* container is pointed at its OWN bucket with bucket-scoped credentials:
|
|
11
|
+
* S3_ENDPOINT MinIO/S3 endpoint URL (e.g. http://minio:9000). Empty
|
|
12
|
+
* ⇒ default AWS S3 (no custom endpoint).
|
|
13
|
+
* S3_REGION region (default 'us-east-1').
|
|
14
|
+
* S3_ACCESS_KEY_ID access key.
|
|
15
|
+
* S3_SECRET_ACCESS_KEY secret key.
|
|
16
|
+
* S3_FORCE_PATH_STYLE 'true' for MinIO (path-style addressing); default true.
|
|
17
|
+
*
|
|
18
|
+
* `S3_BUCKET` is deliberately NOT read here: the bucket is the caller's, and
|
|
19
|
+
* each service keeps its own default and its own key convention.
|
|
20
|
+
*
|
|
21
|
+
* **Reached through the `@pipeline-builder/api-core/s3` subpath, never the root
|
|
22
|
+
* barrel** — only two services do blob storage, and the S3 SDK is a large
|
|
23
|
+
* eager dependency that the other eight must not pay for at boot. Same reason
|
|
24
|
+
* `@aws-sdk/client-kms` is dynamically imported inside secret-encryption.
|
|
25
|
+
*/
|
|
26
|
+
import { S3Client } from '@aws-sdk/client-s3';
|
|
27
|
+
/**
|
|
28
|
+
* The process-wide S3 client, built from env on first use.
|
|
29
|
+
*
|
|
30
|
+
* Lazy so a service that never touches blob storage — and a test that never
|
|
31
|
+
* configures S3 — never constructs one.
|
|
32
|
+
*/
|
|
33
|
+
export declare function s3Client(): S3Client;
|
|
34
|
+
/**
|
|
35
|
+
* Ensure `bucket` exists, once per process per bucket.
|
|
36
|
+
*
|
|
37
|
+
* MinIO — unlike an auto-provisioned S3 bucket — won't create a bucket on first
|
|
38
|
+
* write, so HEAD it and CREATE on 404/NoSuchBucket. Idempotent and
|
|
39
|
+
* concurrency-safe: concurrent callers share the single in-flight promise.
|
|
40
|
+
*
|
|
41
|
+
* The important part is the failure branch. A CreateBucket failure is EITHER a
|
|
42
|
+
* benign race (another replica created it between our HEAD and our CREATE) or a
|
|
43
|
+
* real outage (MinIO not up yet, credentials wrong, network down). Re-HEADing
|
|
44
|
+
* tells them apart:
|
|
45
|
+
*
|
|
46
|
+
* - it now exists → benign race, continue;
|
|
47
|
+
* - it still does not → DROP the memo entry and fail this attempt.
|
|
48
|
+
*
|
|
49
|
+
* Dropping the entry is load-bearing. Memoizing a resolved "ready" after a
|
|
50
|
+
* failed create caches a permanently-broken success for the process lifetime:
|
|
51
|
+
* every later upload then fails `NoSuchBucket` against a MinIO that came up
|
|
52
|
+
* seconds after boot, until the pod is restarted.
|
|
53
|
+
*/
|
|
54
|
+
export declare function ensureBucket(bucket: string): Promise<void>;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Copyright 2026 Pipeline Builder Contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* The shared S3-compatible client, and the bucket-ensure backstop that goes
|
|
5
|
+
* with it.
|
|
6
|
+
*
|
|
7
|
+
* Backed by MinIO in every environment (self-hosted, no AWS-account coupling);
|
|
8
|
+
* because MinIO speaks the S3 API this uses the AWS S3 SDK, so a real-S3 swap is
|
|
9
|
+
* a pure config change (drop the custom endpoint + path-style, supply IAM creds).
|
|
10
|
+
*
|
|
11
|
+
* Config (env) — one set of names, read identically by every service; each
|
|
12
|
+
* container is pointed at its OWN bucket with bucket-scoped credentials:
|
|
13
|
+
* S3_ENDPOINT MinIO/S3 endpoint URL (e.g. http://minio:9000). Empty
|
|
14
|
+
* ⇒ default AWS S3 (no custom endpoint).
|
|
15
|
+
* S3_REGION region (default 'us-east-1').
|
|
16
|
+
* S3_ACCESS_KEY_ID access key.
|
|
17
|
+
* S3_SECRET_ACCESS_KEY secret key.
|
|
18
|
+
* S3_FORCE_PATH_STYLE 'true' for MinIO (path-style addressing); default true.
|
|
19
|
+
*
|
|
20
|
+
* `S3_BUCKET` is deliberately NOT read here: the bucket is the caller's, and
|
|
21
|
+
* each service keeps its own default and its own key convention.
|
|
22
|
+
*
|
|
23
|
+
* **Reached through the `@pipeline-builder/api-core/s3` subpath, never the root
|
|
24
|
+
* barrel** — only two services do blob storage, and the S3 SDK is a large
|
|
25
|
+
* eager dependency that the other eight must not pay for at boot. Same reason
|
|
26
|
+
* `@aws-sdk/client-kms` is dynamically imported inside secret-encryption.
|
|
27
|
+
*/
|
|
28
|
+
import { S3Client, HeadBucketCommand, CreateBucketCommand } from '@aws-sdk/client-s3';
|
|
29
|
+
import { envBool, envStr } from '../utils/env.js';
|
|
30
|
+
import { createLogger } from '../utils/logger.js';
|
|
31
|
+
const logger = createLogger('s3-client');
|
|
32
|
+
let client = null;
|
|
33
|
+
/**
|
|
34
|
+
* The process-wide S3 client, built from env on first use.
|
|
35
|
+
*
|
|
36
|
+
* Lazy so a service that never touches blob storage — and a test that never
|
|
37
|
+
* configures S3 — never constructs one.
|
|
38
|
+
*/
|
|
39
|
+
export function s3Client() {
|
|
40
|
+
if (client)
|
|
41
|
+
return client;
|
|
42
|
+
const endpoint = envStr('S3_ENDPOINT', '');
|
|
43
|
+
client = new S3Client({
|
|
44
|
+
region: envStr('S3_REGION', 'us-east-1'),
|
|
45
|
+
...(endpoint ? { endpoint } : {}),
|
|
46
|
+
// Path-style is required by MinIO (no virtual-host bucket DNS). Harmless
|
|
47
|
+
// against real S3, but default-on here because MinIO is the default backend.
|
|
48
|
+
forcePathStyle: envBool('S3_FORCE_PATH_STYLE', true),
|
|
49
|
+
credentials: {
|
|
50
|
+
accessKeyId: envStr('S3_ACCESS_KEY_ID', 'minioadmin'),
|
|
51
|
+
secretAccessKey: envStr('S3_SECRET_ACCESS_KEY', 'minioadmin'),
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
return client;
|
|
55
|
+
}
|
|
56
|
+
/** In-flight / settled ensure per bucket. A rejected attempt is never left here. */
|
|
57
|
+
const bucketsReady = new Map();
|
|
58
|
+
/**
|
|
59
|
+
* Ensure `bucket` exists, once per process per bucket.
|
|
60
|
+
*
|
|
61
|
+
* MinIO — unlike an auto-provisioned S3 bucket — won't create a bucket on first
|
|
62
|
+
* write, so HEAD it and CREATE on 404/NoSuchBucket. Idempotent and
|
|
63
|
+
* concurrency-safe: concurrent callers share the single in-flight promise.
|
|
64
|
+
*
|
|
65
|
+
* The important part is the failure branch. A CreateBucket failure is EITHER a
|
|
66
|
+
* benign race (another replica created it between our HEAD and our CREATE) or a
|
|
67
|
+
* real outage (MinIO not up yet, credentials wrong, network down). Re-HEADing
|
|
68
|
+
* tells them apart:
|
|
69
|
+
*
|
|
70
|
+
* - it now exists → benign race, continue;
|
|
71
|
+
* - it still does not → DROP the memo entry and fail this attempt.
|
|
72
|
+
*
|
|
73
|
+
* Dropping the entry is load-bearing. Memoizing a resolved "ready" after a
|
|
74
|
+
* failed create caches a permanently-broken success for the process lifetime:
|
|
75
|
+
* every later upload then fails `NoSuchBucket` against a MinIO that came up
|
|
76
|
+
* seconds after boot, until the pod is restarted.
|
|
77
|
+
*/
|
|
78
|
+
export async function ensureBucket(bucket) {
|
|
79
|
+
let ready = bucketsReady.get(bucket);
|
|
80
|
+
if (!ready) {
|
|
81
|
+
ready = (async () => {
|
|
82
|
+
try {
|
|
83
|
+
await s3Client().send(new HeadBucketCommand({ Bucket: bucket }));
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
try {
|
|
87
|
+
await s3Client().send(new CreateBucketCommand({ Bucket: bucket }));
|
|
88
|
+
logger.info('Created S3 bucket', { bucket });
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
try {
|
|
92
|
+
await s3Client().send(new HeadBucketCommand({ Bucket: bucket }));
|
|
93
|
+
logger.warn('S3 bucket ensure race (now exists, continuing)', { bucket, error: String(err) });
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
bucketsReady.delete(bucket);
|
|
97
|
+
throw new Error(`S3 bucket "${bucket}" unavailable: ${String(err)}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
})();
|
|
102
|
+
bucketsReady.set(bucket, ready);
|
|
103
|
+
}
|
|
104
|
+
return ready;
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiczMtY2xpZW50LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL3NlcnZpY2VzL3MzLWNsaWVudC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSwrQ0FBK0M7QUFDL0Msc0NBQXNDO0FBRXRDOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7R0F3Qkc7QUFFSCxPQUFPLEVBQUUsUUFBUSxFQUFFLGlCQUFpQixFQUFFLG1CQUFtQixFQUFFLE1BQU0sb0JBQW9CLENBQUM7QUFFdEYsT0FBTyxFQUFFLE9BQU8sRUFBRSxNQUFNLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUNsRCxPQUFPLEVBQUUsWUFBWSxFQUFFLE1BQU0sb0JBQW9CLENBQUM7QUFFbEQsTUFBTSxNQUFNLEdBQUcsWUFBWSxDQUFDLFdBQVcsQ0FBQyxDQUFDO0FBRXpDLElBQUksTUFBTSxHQUFvQixJQUFJLENBQUM7QUFFbkM7Ozs7O0dBS0c7QUFDSCxNQUFNLFVBQVUsUUFBUTtJQUN0QixJQUFJLE1BQU07UUFBRSxPQUFPLE1BQU0sQ0FBQztJQUMxQixNQUFNLFFBQVEsR0FBRyxNQUFNLENBQUMsYUFBYSxFQUFFLEVBQUUsQ0FBQyxDQUFDO0lBQzNDLE1BQU0sR0FBRyxJQUFJLFFBQVEsQ0FBQztRQUNwQixNQUFNLEVBQUUsTUFBTSxDQUFDLFdBQVcsRUFBRSxXQUFXLENBQUM7UUFDeEMsR0FBRyxDQUFDLFFBQVEsQ0FBQyxDQUFDLENBQUMsRUFBRSxRQUFRLEVBQUUsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDO1FBQ2pDLHlFQUF5RTtRQUN6RSw2RUFBNkU7UUFDN0UsY0FBYyxFQUFFLE9BQU8sQ0FBQyxxQkFBcUIsRUFBRSxJQUFJLENBQUM7UUFDcEQsV0FBVyxFQUFFO1lBQ1gsV0FBVyxFQUFFLE1BQU0sQ0FBQyxrQkFBa0IsRUFBRSxZQUFZLENBQUM7WUFDckQsZUFBZSxFQUFFLE1BQU0sQ0FBQyxzQkFBc0IsRUFBRSxZQUFZLENBQUM7U0FDOUQ7S0FDRixDQUFDLENBQUM7SUFDSCxPQUFPLE1BQU0sQ0FBQztBQUNoQixDQUFDO0FBRUQsb0ZBQW9GO0FBQ3BGLE1BQU0sWUFBWSxHQUFHLElBQUksR0FBRyxFQUF5QixDQUFDO0FBRXREOzs7Ozs7Ozs7Ozs7Ozs7Ozs7O0dBbUJHO0FBQ0gsTUFBTSxDQUFDLEtBQUssVUFBVSxZQUFZLENBQUMsTUFBYztJQUMvQyxJQUFJLEtBQUssR0FBRyxZQUFZLENBQUMsR0FBRyxDQUFDLE1BQU0sQ0FBQyxDQUFDO0lBQ3JDLElBQUksQ0FBQyxLQUFLLEVBQUUsQ0FBQztRQUNYLEtBQUssR0FBRyxDQUFDLEtBQUssSUFBSSxFQUFFO1lBQ2xCLElBQUksQ0FBQztnQkFDSCxNQUFNLFFBQVEsRUFBRSxDQUFDLElBQUksQ0FBQyxJQUFJLGlCQUFpQixDQUFDLEVBQUUsTUFBTSxFQUFFLE1BQU0sRUFBRSxDQUFDLENBQUMsQ0FBQztZQUNuRSxDQUFDO1lBQUMsTUFBTSxDQUFDO2dCQUNQLElBQUksQ0FBQztvQkFDSCxNQUFNLFFBQVEsRUFBRSxDQUFDLElBQUksQ0FBQyxJQUFJLG1CQUFtQixDQUFDLEVBQUUsTUFBTSxFQUFFLE1BQU0sRUFBRSxDQUFDLENBQUMsQ0FBQztvQkFDbkUsTUFBTSxDQUFDLElBQUksQ0FBQyxtQkFBbUIsRUFBRSxFQUFFLE1BQU0sRUFBRSxDQUFDLENBQUM7Z0JBQy9DLENBQUM7Z0JBQUMsT0FBTyxHQUFHLEVBQUUsQ0FBQztvQkFDYixJQUFJLENBQUM7d0JBQ0gsTUFBTSxRQUFRLEVBQUUsQ0FBQyxJQUFJLENBQUMsSUFBSSxpQkFBaUIsQ0FBQyxFQUFFLE1BQU0sRUFBRSxNQUFNLEVBQUUsQ0FBQyxDQUFDLENBQUM7d0JBQ2pFLE1BQU0sQ0FBQyxJQUFJLENBQUMsZ0RBQWdELEVBQUUsRUFBRSxNQUFNLEVBQUUsS0FBSyxFQUFFLE1BQU0sQ0FBQyxHQUFHLENBQUMsRUFBRSxDQUFDLENBQUM7b0JBQ2hHLENBQUM7b0JBQUMsTUFBTSxDQUFDO3dCQUNQLFlBQVksQ0FBQyxNQUFNLENBQUMsTUFBTSxDQUFDLENBQUM7d0JBQzVCLE1BQU0sSUFBSSxLQUFLLENBQUMsY0FBYyxNQUFNLGtCQUFrQixNQUFNLENBQUMsR0FBRyxDQUFDLEVBQUUsQ0FBQyxDQUFDO29CQUN2RSxDQUFDO2dCQUNILENBQUM7WUFDSCxDQUFDO1FBQ0gsQ0FBQyxDQUFDLEVBQUUsQ0FBQztRQUNMLFlBQVksQ0FBQyxHQUFHLENBQUMsTUFBTSxFQUFFLEtBQUssQ0FBQyxDQUFDO0lBQ2xDLENBQUM7SUFDRCxPQUFPLEtBQUssQ0FBQztBQUNmLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyIvLyBDb3B5cmlnaHQgMjAyNiBQaXBlbGluZSBCdWlsZGVyIENvbnRyaWJ1dG9yc1xuLy8gU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IEFwYWNoZS0yLjBcblxuLyoqXG4gKiBUaGUgc2hhcmVkIFMzLWNvbXBhdGlibGUgY2xpZW50LCBhbmQgdGhlIGJ1Y2tldC1lbnN1cmUgYmFja3N0b3AgdGhhdCBnb2VzXG4gKiB3aXRoIGl0LlxuICpcbiAqIEJhY2tlZCBieSBNaW5JTyBpbiBldmVyeSBlbnZpcm9ubWVudCAoc2VsZi1ob3N0ZWQsIG5vIEFXUy1hY2NvdW50IGNvdXBsaW5nKTtcbiAqIGJlY2F1c2UgTWluSU8gc3BlYWtzIHRoZSBTMyBBUEkgdGhpcyB1c2VzIHRoZSBBV1MgUzMgU0RLLCBzbyBhIHJlYWwtUzMgc3dhcCBpc1xuICogYSBwdXJlIGNvbmZpZyBjaGFuZ2UgKGRyb3AgdGhlIGN1c3RvbSBlbmRwb2ludCArIHBhdGgtc3R5bGUsIHN1cHBseSBJQU0gY3JlZHMpLlxuICpcbiAqIENvbmZpZyAoZW52KSDigJQgb25lIHNldCBvZiBuYW1lcywgcmVhZCBpZGVudGljYWxseSBieSBldmVyeSBzZXJ2aWNlOyBlYWNoXG4gKiBjb250YWluZXIgaXMgcG9pbnRlZCBhdCBpdHMgT1dOIGJ1Y2tldCB3aXRoIGJ1Y2tldC1zY29wZWQgY3JlZGVudGlhbHM6XG4gKiAgIFMzX0VORFBPSU5UICAgICAgICAgICBNaW5JTy9TMyBlbmRwb2ludCBVUkwgKGUuZy4gaHR0cDovL21pbmlvOjkwMDApLiBFbXB0eVxuICogICAgICAgICAgICAgICAgICAgICAgICAg4oeSIGRlZmF1bHQgQVdTIFMzIChubyBjdXN0b20gZW5kcG9pbnQpLlxuICogICBTM19SRUdJT04gICAgICAgICAgICAgcmVnaW9uIChkZWZhdWx0ICd1cy1lYXN0LTEnKS5cbiAqICAgUzNfQUNDRVNTX0tFWV9JRCAgICAgIGFjY2VzcyBrZXkuXG4gKiAgIFMzX1NFQ1JFVF9BQ0NFU1NfS0VZICBzZWNyZXQga2V5LlxuICogICBTM19GT1JDRV9QQVRIX1NUWUxFICAgJ3RydWUnIGZvciBNaW5JTyAocGF0aC1zdHlsZSBhZGRyZXNzaW5nKTsgZGVmYXVsdCB0cnVlLlxuICpcbiAqIGBTM19CVUNLRVRgIGlzIGRlbGliZXJhdGVseSBOT1QgcmVhZCBoZXJlOiB0aGUgYnVja2V0IGlzIHRoZSBjYWxsZXIncywgYW5kXG4gKiBlYWNoIHNlcnZpY2Uga2VlcHMgaXRzIG93biBkZWZhdWx0IGFuZCBpdHMgb3duIGtleSBjb252ZW50aW9uLlxuICpcbiAqICoqUmVhY2hlZCB0aHJvdWdoIHRoZSBgQHBpcGVsaW5lLWJ1aWxkZXIvYXBpLWNvcmUvczNgIHN1YnBhdGgsIG5ldmVyIHRoZSByb290XG4gKiBiYXJyZWwqKiDigJQgb25seSB0d28gc2VydmljZXMgZG8gYmxvYiBzdG9yYWdlLCBhbmQgdGhlIFMzIFNESyBpcyBhIGxhcmdlXG4gKiBlYWdlciBkZXBlbmRlbmN5IHRoYXQgdGhlIG90aGVyIGVpZ2h0IG11c3Qgbm90IHBheSBmb3IgYXQgYm9vdC4gU2FtZSByZWFzb25cbiAqIGBAYXdzLXNkay9jbGllbnQta21zYCBpcyBkeW5hbWljYWxseSBpbXBvcnRlZCBpbnNpZGUgc2VjcmV0LWVuY3J5cHRpb24uXG4gKi9cblxuaW1wb3J0IHsgUzNDbGllbnQsIEhlYWRCdWNrZXRDb21tYW5kLCBDcmVhdGVCdWNrZXRDb21tYW5kIH0gZnJvbSAnQGF3cy1zZGsvY2xpZW50LXMzJztcblxuaW1wb3J0IHsgZW52Qm9vbCwgZW52U3RyIH0gZnJvbSAnLi4vdXRpbHMvZW52LmpzJztcbmltcG9ydCB7IGNyZWF0ZUxvZ2dlciB9IGZyb20gJy4uL3V0aWxzL2xvZ2dlci5qcyc7XG5cbmNvbnN0IGxvZ2dlciA9IGNyZWF0ZUxvZ2dlcignczMtY2xpZW50Jyk7XG5cbmxldCBjbGllbnQ6IFMzQ2xpZW50IHwgbnVsbCA9IG51bGw7XG5cbi8qKlxuICogVGhlIHByb2Nlc3Mtd2lkZSBTMyBjbGllbnQsIGJ1aWx0IGZyb20gZW52IG9uIGZpcnN0IHVzZS5cbiAqXG4gKiBMYXp5IHNvIGEgc2VydmljZSB0aGF0IG5ldmVyIHRvdWNoZXMgYmxvYiBzdG9yYWdlIOKAlCBhbmQgYSB0ZXN0IHRoYXQgbmV2ZXJcbiAqIGNvbmZpZ3VyZXMgUzMg4oCUIG5ldmVyIGNvbnN0cnVjdHMgb25lLlxuICovXG5leHBvcnQgZnVuY3Rpb24gczNDbGllbnQoKTogUzNDbGllbnQge1xuICBpZiAoY2xpZW50KSByZXR1cm4gY2xpZW50O1xuICBjb25zdCBlbmRwb2ludCA9IGVudlN0cignUzNfRU5EUE9JTlQnLCAnJyk7XG4gIGNsaWVudCA9IG5ldyBTM0NsaWVudCh7XG4gICAgcmVnaW9uOiBlbnZTdHIoJ1MzX1JFR0lPTicsICd1cy1lYXN0LTEnKSxcbiAgICAuLi4oZW5kcG9pbnQgPyB7IGVuZHBvaW50IH0gOiB7fSksXG4gICAgLy8gUGF0aC1zdHlsZSBpcyByZXF1aXJlZCBieSBNaW5JTyAobm8gdmlydHVhbC1ob3N0IGJ1Y2tldCBETlMpLiBIYXJtbGVzc1xuICAgIC8vIGFnYWluc3QgcmVhbCBTMywgYnV0IGRlZmF1bHQtb24gaGVyZSBiZWNhdXNlIE1pbklPIGlzIHRoZSBkZWZhdWx0IGJhY2tlbmQuXG4gICAgZm9yY2VQYXRoU3R5bGU6IGVudkJvb2woJ1MzX0ZPUkNFX1BBVEhfU1RZTEUnLCB0cnVlKSxcbiAgICBjcmVkZW50aWFsczoge1xuICAgICAgYWNjZXNzS2V5SWQ6IGVudlN0cignUzNfQUNDRVNTX0tFWV9JRCcsICdtaW5pb2FkbWluJyksXG4gICAgICBzZWNyZXRBY2Nlc3NLZXk6IGVudlN0cignUzNfU0VDUkVUX0FDQ0VTU19LRVknLCAnbWluaW9hZG1pbicpLFxuICAgIH0sXG4gIH0pO1xuICByZXR1cm4gY2xpZW50O1xufVxuXG4vKiogSW4tZmxpZ2h0IC8gc2V0dGxlZCBlbnN1cmUgcGVyIGJ1Y2tldC4gQSByZWplY3RlZCBhdHRlbXB0IGlzIG5ldmVyIGxlZnQgaGVyZS4gKi9cbmNvbnN0IGJ1Y2tldHNSZWFkeSA9IG5ldyBNYXA8c3RyaW5nLCBQcm9taXNlPHZvaWQ+PigpO1xuXG4vKipcbiAqIEVuc3VyZSBgYnVja2V0YCBleGlzdHMsIG9uY2UgcGVyIHByb2Nlc3MgcGVyIGJ1Y2tldC5cbiAqXG4gKiBNaW5JTyDigJQgdW5saWtlIGFuIGF1dG8tcHJvdmlzaW9uZWQgUzMgYnVja2V0IOKAlCB3b24ndCBjcmVhdGUgYSBidWNrZXQgb24gZmlyc3RcbiAqIHdyaXRlLCBzbyBIRUFEIGl0IGFuZCBDUkVBVEUgb24gNDA0L05vU3VjaEJ1Y2tldC4gSWRlbXBvdGVudCBhbmRcbiAqIGNvbmN1cnJlbmN5LXNhZmU6IGNvbmN1cnJlbnQgY2FsbGVycyBzaGFyZSB0aGUgc2luZ2xlIGluLWZsaWdodCBwcm9taXNlLlxuICpcbiAqIFRoZSBpbXBvcnRhbnQgcGFydCBpcyB0aGUgZmFpbHVyZSBicmFuY2guIEEgQ3JlYXRlQnVja2V0IGZhaWx1cmUgaXMgRUlUSEVSIGFcbiAqIGJlbmlnbiByYWNlIChhbm90aGVyIHJlcGxpY2EgY3JlYXRlZCBpdCBiZXR3ZWVuIG91ciBIRUFEIGFuZCBvdXIgQ1JFQVRFKSBvciBhXG4gKiByZWFsIG91dGFnZSAoTWluSU8gbm90IHVwIHlldCwgY3JlZGVudGlhbHMgd3JvbmcsIG5ldHdvcmsgZG93bikuIFJlLUhFQURpbmdcbiAqIHRlbGxzIHRoZW0gYXBhcnQ6XG4gKlxuICogIC0gaXQgbm93IGV4aXN0cyAg4oaSIGJlbmlnbiByYWNlLCBjb250aW51ZTtcbiAqICAtIGl0IHN0aWxsIGRvZXMgbm90IOKGkiBEUk9QIHRoZSBtZW1vIGVudHJ5IGFuZCBmYWlsIHRoaXMgYXR0ZW1wdC5cbiAqXG4gKiBEcm9wcGluZyB0aGUgZW50cnkgaXMgbG9hZC1iZWFyaW5nLiBNZW1vaXppbmcgYSByZXNvbHZlZCBcInJlYWR5XCIgYWZ0ZXIgYVxuICogZmFpbGVkIGNyZWF0ZSBjYWNoZXMgYSBwZXJtYW5lbnRseS1icm9rZW4gc3VjY2VzcyBmb3IgdGhlIHByb2Nlc3MgbGlmZXRpbWU6XG4gKiBldmVyeSBsYXRlciB1cGxvYWQgdGhlbiBmYWlscyBgTm9TdWNoQnVja2V0YCBhZ2FpbnN0IGEgTWluSU8gdGhhdCBjYW1lIHVwXG4gKiBzZWNvbmRzIGFmdGVyIGJvb3QsIHVudGlsIHRoZSBwb2QgaXMgcmVzdGFydGVkLlxuICovXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gZW5zdXJlQnVja2V0KGJ1Y2tldDogc3RyaW5nKTogUHJvbWlzZTx2b2lkPiB7XG4gIGxldCByZWFkeSA9IGJ1Y2tldHNSZWFkeS5nZXQoYnVja2V0KTtcbiAgaWYgKCFyZWFkeSkge1xuICAgIHJlYWR5ID0gKGFzeW5jICgpID0+IHtcbiAgICAgIHRyeSB7XG4gICAgICAgIGF3YWl0IHMzQ2xpZW50KCkuc2VuZChuZXcgSGVhZEJ1Y2tldENvbW1hbmQoeyBCdWNrZXQ6IGJ1Y2tldCB9KSk7XG4gICAgICB9IGNhdGNoIHtcbiAgICAgICAgdHJ5IHtcbiAgICAgICAgICBhd2FpdCBzM0NsaWVudCgpLnNlbmQobmV3IENyZWF0ZUJ1Y2tldENvbW1hbmQoeyBCdWNrZXQ6IGJ1Y2tldCB9KSk7XG4gICAgICAgICAgbG9nZ2VyLmluZm8oJ0NyZWF0ZWQgUzMgYnVja2V0JywgeyBidWNrZXQgfSk7XG4gICAgICAgIH0gY2F0Y2ggKGVycikge1xuICAgICAgICAgIHRyeSB7XG4gICAgICAgICAgICBhd2FpdCBzM0NsaWVudCgpLnNlbmQobmV3IEhlYWRCdWNrZXRDb21tYW5kKHsgQnVja2V0OiBidWNrZXQgfSkpO1xuICAgICAgICAgICAgbG9nZ2VyLndhcm4oJ1MzIGJ1Y2tldCBlbnN1cmUgcmFjZSAobm93IGV4aXN0cywgY29udGludWluZyknLCB7IGJ1Y2tldCwgZXJyb3I6IFN0cmluZyhlcnIpIH0pO1xuICAgICAgICAgIH0gY2F0Y2gge1xuICAgICAgICAgICAgYnVja2V0c1JlYWR5LmRlbGV0ZShidWNrZXQpO1xuICAgICAgICAgICAgdGhyb3cgbmV3IEVycm9yKGBTMyBidWNrZXQgXCIke2J1Y2tldH1cIiB1bmF2YWlsYWJsZTogJHtTdHJpbmcoZXJyKX1gKTtcbiAgICAgICAgICB9XG4gICAgICAgIH1cbiAgICAgIH1cbiAgICB9KSgpO1xuICAgIGJ1Y2tldHNSZWFkeS5zZXQoYnVja2V0LCByZWFkeSk7XG4gIH1cbiAgcmV0dXJuIHJlYWR5O1xufVxuIl19
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared contract between the ask agent's PROPOSE tools and the browser's
|
|
3
|
+
* CONFIRM handlers. Two things live here, and they live here because BOTH sides
|
|
4
|
+
* need them: `api/ask` builds its tool schema from this module, the frontend's
|
|
5
|
+
* confirm handler builds its request from it, and the tests of both assert
|
|
6
|
+
* against it. A second copy would be a second policy.
|
|
7
|
+
*
|
|
8
|
+
* Dependency-free on purpose — no runtime imports at all — so the browser can
|
|
9
|
+
* import it through api-core's `./ask-proposals` entry without dragging in the
|
|
10
|
+
* server graph (express / jwt / ioredis). The only imports are `import type`,
|
|
11
|
+
* which the compiler erases.
|
|
12
|
+
*
|
|
13
|
+
* -----------------------------------------------------------------------------
|
|
14
|
+
* 1. PROVENANCE
|
|
15
|
+
* -----------------------------------------------------------------------------
|
|
16
|
+
* Every commit that started life as an agent draft stamps
|
|
17
|
+
* `details: { proposedBy: 'ask-agent' }` onto its audit event. The audit trail
|
|
18
|
+
* is hash-chained and tamper-evident, but without this field it cannot
|
|
19
|
+
* distinguish "the admin changed this" from "an AI drafted it and the admin
|
|
20
|
+
* clicked Apply" — the two mean very different things in an incident review.
|
|
21
|
+
* The constant exists so the literal is not retyped in two codebases, where a
|
|
22
|
+
* typo would silently produce an unqueryable trail.
|
|
23
|
+
*
|
|
24
|
+
* -----------------------------------------------------------------------------
|
|
25
|
+
* 2. THE ORG-SETTINGS ALLOWLIST
|
|
26
|
+
* -----------------------------------------------------------------------------
|
|
27
|
+
* This is an ALLOWLIST, not a denylist, and the distinction is the whole point:
|
|
28
|
+
* a setting added to the platform tomorrow is UNAVAILABLE to the agent until
|
|
29
|
+
* someone deliberately adds it here, rather than being exposed by omission.
|
|
30
|
+
* If you are reading this because a proposal was refused, the fix is to add the
|
|
31
|
+
* field below WITH its `why` — not to loosen {@link pickAllowed}.
|
|
32
|
+
*
|
|
33
|
+
* A field earns a place here only if all of the following hold. They were
|
|
34
|
+
* checked against the routes, not assumed:
|
|
35
|
+
*
|
|
36
|
+
* - **No step-up.** A route carrying `requireStepUp` is out, no exceptions.
|
|
37
|
+
* Step-up means the platform already decided a live session is not enough
|
|
38
|
+
* authority; a chat proposal is strictly less. This is what keeps
|
|
39
|
+
* `PUT /organization/ai-config` out (it persists per-org provider SECRETS),
|
|
40
|
+
* along with the MFA / password / authenticator policies,
|
|
41
|
+
* `PUT /plugins/install-policy`, org restore and transfer-owner.
|
|
42
|
+
* - **Not a security control.** MFA / password / authenticator / impersonation
|
|
43
|
+
* policy, SSO+SAML and its group mappings, verified domains and domain-based
|
|
44
|
+
* join, join-request decisions, transfer-owner and team lifecycle are ABSENT
|
|
45
|
+
* here rather than gated within — "relax the MFA policy" is precisely what an
|
|
46
|
+
* injection payload asks for, and it arrives through text the agent already
|
|
47
|
+
* reads. Absent cannot be argued with; gated can.
|
|
48
|
+
* - **Carries no secret and no callback target.** An API that returns
|
|
49
|
+
* `hasWebhookSecret` instead of the secret is telling you the field cannot be
|
|
50
|
+
* diffed, so it cannot be reviewed, so it cannot be proposed. Webhook URLs
|
|
51
|
+
* and email addresses are out for the same reason plus one more: they are
|
|
52
|
+
* exfiltration targets, and a confirmation mail to an attacker-chosen address
|
|
53
|
+
* is a send the platform performs on the model's say-so.
|
|
54
|
+
* - **Reviewable as a diff.** A scalar with a closed shape (boolean, bounded
|
|
55
|
+
* integer, small enum). A list of opaque user ids renders as ids in a card
|
|
56
|
+
* nobody can check, so recipient LISTS are out even though cadence is in.
|
|
57
|
+
*
|
|
58
|
+
* Each entry names the API that applies it. The browser applies it through the
|
|
59
|
+
* USER's own session and the user's own permissions — the agent never writes —
|
|
60
|
+
* so a proposal a member may not commit simply 403s at the route, exactly as a
|
|
61
|
+
* hand-made request would.
|
|
62
|
+
*
|
|
63
|
+
* Rule 10 lives in {@link pickAllowed}: a candidate carrying a field outside
|
|
64
|
+
* this list is the injection signal, so the refused names come back to the
|
|
65
|
+
* caller to be counted and logged rather than being quietly dropped.
|
|
66
|
+
*/
|
|
67
|
+
import type { Permission } from './permissions.js';
|
|
68
|
+
/** The `proposedBy` value every agent-drafted commit records. */
|
|
69
|
+
export declare const ASK_AGENT_PROPOSER = "ask-agent";
|
|
70
|
+
/** The audit `details` key that carries provenance. */
|
|
71
|
+
export declare const PROPOSED_BY_DETAIL_KEY = "proposedBy";
|
|
72
|
+
/** The provenance fragment merged into a `RemoteAuditEvent.details`. */
|
|
73
|
+
export interface AskProposalProvenance {
|
|
74
|
+
readonly proposedBy: typeof ASK_AGENT_PROPOSER;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Merge agent provenance into an audit event's free-form `details`.
|
|
78
|
+
* Provenance is applied LAST so a caller-supplied `proposedBy` cannot overwrite
|
|
79
|
+
* it — the point of the field is that it is not negotiable.
|
|
80
|
+
*/
|
|
81
|
+
export declare function askProposalAuditDetails(details?: Readonly<Record<string, unknown>>): Record<string, unknown> & AskProposalProvenance;
|
|
82
|
+
/**
|
|
83
|
+
* The request HEADER a proposal commit stamps, and the only channel provenance
|
|
84
|
+
* travels on.
|
|
85
|
+
*
|
|
86
|
+
* A HEADER rather than a body field, for three reasons:
|
|
87
|
+
*
|
|
88
|
+
* - **It cannot collide.** `proposedBy` in a body would be a new key in six
|
|
89
|
+
* different domain schemas — `PipelineUpdateSchema`, the plugin/template
|
|
90
|
+
* update schemas, three notification/settings schemas — two of which are
|
|
91
|
+
* `.strict()` and the rest of which feed `Object.keys(body)` straight into
|
|
92
|
+
* the update's column list and the audit's `fields`. A field named after an
|
|
93
|
+
* audit key would then be indistinguishable from a real one, and the day a
|
|
94
|
+
* domain grows its own `proposedBy` the two meanings merge silently.
|
|
95
|
+
* - **It is transport metadata, and stays out of the payload.** Nothing
|
|
96
|
+
* downstream of `validateBody` ever sees it, so it cannot be persisted, cannot
|
|
97
|
+
* widen a diff, and cannot reach a service that forwards the body onward.
|
|
98
|
+
* - **The platform already does this.** `X-Step-Up-Token` is the same shape of
|
|
99
|
+
* thing — a per-request assertion about HOW the call was made rather than
|
|
100
|
+
* WHAT it changes — and rides a header for the same reasons.
|
|
101
|
+
*
|
|
102
|
+
* Lower-case because that is how Node normalises incoming header names; the
|
|
103
|
+
* browser sends `X-PB-Proposed-By`, which arrives here.
|
|
104
|
+
*/
|
|
105
|
+
export declare const ASK_PROPOSED_BY_HEADER = "x-pb-proposed-by";
|
|
106
|
+
/**
|
|
107
|
+
* What a request's provenance header claims.
|
|
108
|
+
*
|
|
109
|
+
* `foreign` is any value that is not EXACTLY {@link ASK_AGENT_PROPOSER} — there
|
|
110
|
+
* is one legitimate proposer and no free-form text, so anything else is a bug
|
|
111
|
+
* or a forgery and is named as such rather than folded into `none`.
|
|
112
|
+
*/
|
|
113
|
+
export type ProposerClaim = 'none' | 'ask-agent' | 'foreign';
|
|
114
|
+
/**
|
|
115
|
+
* Read the provenance claim off a request's header bag.
|
|
116
|
+
*
|
|
117
|
+
* Deliberately exact: no trimming, no case folding, no prefix matching. The
|
|
118
|
+
* only sender is the browser's confirm path, which stamps the shared constant,
|
|
119
|
+
* so every other value is refused rather than repaired. This is what keeps a
|
|
120
|
+
* client from writing arbitrary audit `details` — the header's whole vocabulary
|
|
121
|
+
* is one word, and the word is not the client's to choose.
|
|
122
|
+
*
|
|
123
|
+
* Takes the header BAG (`req.headers`) rather than a request, so this module
|
|
124
|
+
* keeps its zero runtime imports and the browser can still import the constant.
|
|
125
|
+
*/
|
|
126
|
+
export declare function readProposerClaim(headers: unknown): ProposerClaim;
|
|
127
|
+
/**
|
|
128
|
+
* The audit `details` for a write, with provenance folded in when — and ONLY
|
|
129
|
+
* when — the request carries the agent marker.
|
|
130
|
+
*
|
|
131
|
+
* `details` is what the HANDLER built and stays authoritative. Provenance is
|
|
132
|
+
* additive: the ONLY key it can ever write is `proposedBy`, so every field the
|
|
133
|
+
* handler set survives untouched, and {@link askProposalAuditDetails} applies
|
|
134
|
+
* that one key LAST so a `proposedBy` arriving from anywhere else cannot speak
|
|
135
|
+
* in its place. A request carrying no marker, or a `foreign` one, gets the
|
|
136
|
+
* handler's details unchanged — an ordinary edit records no `proposedBy` at
|
|
137
|
+
* all, which is the distinction the field exists to make.
|
|
138
|
+
*/
|
|
139
|
+
export declare function withProposalProvenance(headers: unknown, details?: Readonly<Record<string, unknown>>): Record<string, unknown>;
|
|
140
|
+
/** A value an allowlisted org setting can hold. */
|
|
141
|
+
export type OrgSettingValue = string | number | boolean;
|
|
142
|
+
/** Which per-org record (and therefore which API) a field belongs to. */
|
|
143
|
+
export type OrgSettingSurface = 'reporting' | 'complianceNotifications' | 'pluginSecurityNotifications';
|
|
144
|
+
/**
|
|
145
|
+
* Every proposable setting, as `<surface>.<field>`. Writing the union out by
|
|
146
|
+
* hand (rather than inferring it from the array) is deliberate: the key space
|
|
147
|
+
* is the contract the tool schema and the confirm handler share, so it is
|
|
148
|
+
* stated once, explicitly, and a typo in an entry below is a compile error.
|
|
149
|
+
*/
|
|
150
|
+
export type OrgSettingKey = 'reporting.incidentWindowHours' | 'complianceNotifications.notifyOnBlock' | 'complianceNotifications.notifyOnWarning' | 'complianceNotifications.emailEnabled' | 'complianceNotifications.digestMode' | 'pluginSecurityNotifications.notifyRescan' | 'pluginSecurityNotifications.digestMode';
|
|
151
|
+
/** The closed shape a proposed value must match before it is accepted. */
|
|
152
|
+
export type OrgSettingShape = {
|
|
153
|
+
readonly kind: 'boolean';
|
|
154
|
+
} | {
|
|
155
|
+
readonly kind: 'integer';
|
|
156
|
+
readonly min: number;
|
|
157
|
+
readonly max: number;
|
|
158
|
+
} | {
|
|
159
|
+
readonly kind: 'enum';
|
|
160
|
+
readonly values: readonly string[];
|
|
161
|
+
};
|
|
162
|
+
/** One proposable org setting. */
|
|
163
|
+
export interface OrgSettingSpec {
|
|
164
|
+
/** Stable proposal key — `<surface>.<field>`. This is what the model names. */
|
|
165
|
+
readonly key: OrgSettingKey;
|
|
166
|
+
readonly surface: OrgSettingSurface;
|
|
167
|
+
/** The request-body field name the API expects. */
|
|
168
|
+
readonly field: string;
|
|
169
|
+
readonly shape: OrgSettingShape;
|
|
170
|
+
/** The route that applies it, called by the BROWSER with the user's session. */
|
|
171
|
+
readonly api: {
|
|
172
|
+
readonly method: 'PUT';
|
|
173
|
+
readonly path: string;
|
|
174
|
+
};
|
|
175
|
+
/** The permission that route requires of the caller. */
|
|
176
|
+
readonly permission: Permission;
|
|
177
|
+
/** Short human label for the confirm card. */
|
|
178
|
+
readonly label: string;
|
|
179
|
+
/** WHY this field is safe to propose. Every entry states its own case. */
|
|
180
|
+
readonly why: string;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* The allowlist. Adding an entry is a deliberate act: name the route you
|
|
184
|
+
* checked, confirm it carries no `requireStepUp`, and write the `why`.
|
|
185
|
+
*/
|
|
186
|
+
export declare const ORG_SETTING_PROPOSAL_ALLOWLIST: readonly OrgSettingSpec[];
|
|
187
|
+
/**
|
|
188
|
+
* Fields DELIBERATELY excluded, with the reason, so a future reader does not
|
|
189
|
+
* re-litigate them and a reviewer can see they were considered rather than
|
|
190
|
+
* missed. This is documentation only — {@link pickAllowed} consults the
|
|
191
|
+
* allowlist alone, and a field absent from BOTH lists is refused identically.
|
|
192
|
+
*/
|
|
193
|
+
export declare const ORG_SETTING_PROPOSAL_EXCLUSIONS: ReadonlyArray<{
|
|
194
|
+
readonly field: string;
|
|
195
|
+
readonly why: string;
|
|
196
|
+
}>;
|
|
197
|
+
/** Every proposable key — the enum a propose tool's input schema is built from. */
|
|
198
|
+
export declare const ORG_SETTING_PROPOSAL_KEYS: readonly OrgSettingKey[];
|
|
199
|
+
/** Is `key` on the allowlist? */
|
|
200
|
+
export declare function isProposableOrgSetting(key: string): key is OrgSettingKey;
|
|
201
|
+
/** The spec for `key`, or undefined when it is not proposable. */
|
|
202
|
+
export declare function orgSettingSpec(key: string): OrgSettingSpec | undefined;
|
|
203
|
+
/** Does `value` match the shape the field declares? */
|
|
204
|
+
export declare function isValidOrgSettingValue(spec: OrgSettingSpec, value: unknown): value is OrgSettingValue;
|
|
205
|
+
/** A proposal's settings, keyed by allowlist key. */
|
|
206
|
+
export type OrgSettingPatch = Partial<Record<OrgSettingKey, OrgSettingValue>>;
|
|
207
|
+
/** What {@link pickAllowed} kept, and what it threw away. */
|
|
208
|
+
export interface PickAllowedResult {
|
|
209
|
+
/** The allowlisted, shape-valid settings. */
|
|
210
|
+
readonly allowed: OrgSettingPatch;
|
|
211
|
+
/**
|
|
212
|
+
* Field names the candidate carried that are NOT on the allowlist. THIS is
|
|
213
|
+
* the injection signal (rule 10): a legitimate draft has none, so a non-empty
|
|
214
|
+
* list is a counter to increment and a line to log, not a silent drop.
|
|
215
|
+
*/
|
|
216
|
+
readonly refused: readonly string[];
|
|
217
|
+
/**
|
|
218
|
+
* Allowlisted keys whose value did not match the declared shape. Dropped too,
|
|
219
|
+
* but reported separately — a bad value is a drafting mistake, not an attempt
|
|
220
|
+
* to reach a field the agent was never given.
|
|
221
|
+
*/
|
|
222
|
+
readonly invalid: readonly OrgSettingKey[];
|
|
223
|
+
/** `refused.length`, so a caller can count without re-deriving it. */
|
|
224
|
+
readonly refusedCount: number;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Keep only the allowlisted, shape-valid fields of a candidate proposal, and
|
|
228
|
+
* report everything dropped.
|
|
229
|
+
*
|
|
230
|
+
* The result's `allowed` map IS the commit payload (rule 3): the confirm
|
|
231
|
+
* handler sends these fields and only these, so anything the model slipped in
|
|
232
|
+
* is structurally unappliable rather than merely unrendered.
|
|
233
|
+
*
|
|
234
|
+
* `candidate` is untrusted model output, so it is read defensively: anything
|
|
235
|
+
* that is not a plain object yields an empty result.
|
|
236
|
+
*/
|
|
237
|
+
export declare function pickAllowed(candidate: unknown): PickAllowedResult;
|
|
238
|
+
/** One reviewed change: what the setting is now, and what it would become. */
|
|
239
|
+
export interface OrgSettingChange {
|
|
240
|
+
readonly spec: OrgSettingSpec;
|
|
241
|
+
readonly from: OrgSettingValue | undefined;
|
|
242
|
+
readonly to: OrgSettingValue;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* The changes a proposal actually makes, given the CURRENT values. A field
|
|
246
|
+
* whose proposed value already equals the current one is dropped — it is not a
|
|
247
|
+
* change, and rendering it as one makes a diff the user cannot read.
|
|
248
|
+
*
|
|
249
|
+
* The confirm handler re-runs this against a FRESH read immediately before
|
|
250
|
+
* committing (rule 7): these records carry no version column, so if the list
|
|
251
|
+
* that comes back differs from the one the user approved, state moved under the
|
|
252
|
+
* proposal and the commit must be refused rather than applied blind.
|
|
253
|
+
*/
|
|
254
|
+
export declare function diffOrgSettings(current: Readonly<OrgSettingPatch>, proposed: Readonly<OrgSettingPatch>): readonly OrgSettingChange[];
|
|
255
|
+
/** One API call the confirm handler makes: one surface, one body. */
|
|
256
|
+
export interface OrgSettingRequest {
|
|
257
|
+
readonly surface: OrgSettingSurface;
|
|
258
|
+
readonly method: 'PUT';
|
|
259
|
+
readonly path: string;
|
|
260
|
+
readonly permission: Permission;
|
|
261
|
+
/** Request body: the API's own field names, carrying only reviewed values. */
|
|
262
|
+
readonly body: Record<string, OrgSettingValue>;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Group approved changes into one request per surface, translating proposal
|
|
266
|
+
* keys back to the field names each API expects. Order follows the allowlist,
|
|
267
|
+
* so the same proposal always produces the same call sequence.
|
|
268
|
+
*/
|
|
269
|
+
export declare function orgSettingRequests(changes: readonly OrgSettingChange[]): readonly OrgSettingRequest[];
|