@fourier-labs/harbour-contracts 0.9.0 → 0.10.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.
Files changed (40) hide show
  1. package/README.md +5 -0
  2. package/contract-release.json +30 -18
  3. package/dist/gen-ts/binding-classification-digest-helper.d.ts +11 -0
  4. package/dist/gen-ts/binding-classification-digest-helper.js +47 -0
  5. package/dist/gen-ts/binding-classification-subject.d.ts +56 -0
  6. package/dist/gen-ts/binding-classification-subject.js +3 -0
  7. package/dist/gen-ts/contract-release.d.ts +7 -7
  8. package/dist/gen-ts/contract-release.js +7 -7
  9. package/dist/gen-ts/deploy-request.d.ts +52 -0
  10. package/dist/gen-ts/deploy-response.d.ts +8 -0
  11. package/dist/gen-ts/index.d.ts +4 -0
  12. package/dist/gen-ts/index.js +4 -0
  13. package/dist/gen-ts/promotion-request.d.ts +52 -0
  14. package/dist/gen-ts/promotion-response.d.ts +8 -0
  15. package/dist/gen-ts/sanitized-binding-digest-helper.d.ts +7 -0
  16. package/dist/gen-ts/sanitized-binding-digest-helper.js +58 -0
  17. package/dist/gen-ts/sanitized-binding-evidence.d.ts +47 -0
  18. package/dist/gen-ts/sanitized-binding-evidence.js +3 -0
  19. package/fixtures/ai/adversarial-binding-classification-value.json +28 -0
  20. package/fixtures/ai/adversarial-sanitized-binding-relabel.json +32 -0
  21. package/fixtures/ai/valid-binding-classification-subject.json +46 -0
  22. package/fixtures/ai/valid-sanitized-binding-evidence.json +32 -0
  23. package/fixtures/ai/valid-source-revision-digest.json +6 -0
  24. package/gen-ts/binding-classification-digest-helper.ts +44 -0
  25. package/gen-ts/binding-classification-subject.ts +60 -0
  26. package/gen-ts/contract-release.ts +7 -7
  27. package/gen-ts/deploy-request.ts +51 -0
  28. package/gen-ts/deploy-response.ts +8 -0
  29. package/gen-ts/index.ts +4 -0
  30. package/gen-ts/promotion-request.ts +52 -0
  31. package/gen-ts/promotion-response.ts +8 -0
  32. package/gen-ts/sanitized-binding-digest-helper.ts +33 -0
  33. package/gen-ts/sanitized-binding-evidence.ts +51 -0
  34. package/package.json +1 -1
  35. package/schema/binding-classification-subject.schema.json +119 -0
  36. package/schema/deploy-request.schema.json +37 -2
  37. package/schema/deploy-response.schema.json +11 -1
  38. package/schema/promotion-request.schema.json +5 -1
  39. package/schema/promotion-response.schema.json +11 -1
  40. package/schema/sanitized-binding-evidence.schema.json +102 -0
@@ -0,0 +1,28 @@
1
+ {
2
+ "schema": "harbour.binding-classification-subject/1.0",
3
+ "status": "COMPLETE",
4
+ "tenantId": "fourier",
5
+ "appId": "support-copilot",
6
+ "environment": "preview",
7
+ "sourceRef": "https://github.com/Fourier-Labs-AI/support-copilot",
8
+ "sourceRevision": "git-sha1:0123456789abcdef0123456789abcdef01234567",
9
+ "sourceRevisionDigest": "sha256:79520355eb30e88bb0c6656008dd301d8f3d7cef6c14a96882557f13f7c512d9",
10
+ "classifierRevision": "secret-name-classifier-2.0",
11
+ "classifications": {
12
+ "OPENAI_API_KEY": {
13
+ "kind": "provider_credential",
14
+ "confidence": "high",
15
+ "providerId": "openai",
16
+ "category": "api_key",
17
+ "requestedAiMappingId": "openai-responses-v1",
18
+ "value": "must-never-cross-this-boundary"
19
+ }
20
+ },
21
+ "requestedAiMappings": {
22
+ "openai-responses-v1": {
23
+ "mappingVersion": "1.0.0",
24
+ "providerId": "openai"
25
+ }
26
+ },
27
+ "classificationDigest": "sha256:0000000000000000000000000000000000000000000000000000000000000000"
28
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "schema": "harbour.sanitized-binding-evidence/1.0",
3
+ "tenantId": "fourier",
4
+ "appId": "support-copilot",
5
+ "environment": "preview",
6
+ "versionId": "fourier#support-copilot#preview#654321",
7
+ "sourceRef": "https://github.com/Fourier-Labs-AI/support-copilot",
8
+ "sourceRevision": "git-sha1:0123456789abcdef0123456789abcdef01234567",
9
+ "sourceRevisionDigest": "sha256:79520355eb30e88bb0c6656008dd301d8f3d7cef6c14a96882557f13f7c512d9",
10
+ "classificationDigest": "sha256:43622df1d8bc3660a160a120f9af896cfc9c98009547869895b47d4c61a29688",
11
+ "bindings": {
12
+ "LOG_LEVEL": {
13
+ "kind": "configuration",
14
+ "value": "debug"
15
+ },
16
+ "STRIPE_API_KEY": {
17
+ "kind": "secret",
18
+ "secretRef": "secretref://tenant/fourier/app/support-copilot/secret/STRIPE_API_KEY"
19
+ }
20
+ },
21
+ "requiredAiMappings": {
22
+ "anthropic-messages-v1": {
23
+ "mappingVersion": "1.0.0",
24
+ "providerId": "anthropic"
25
+ },
26
+ "openai-responses-v1": {
27
+ "mappingVersion": "1.0.0",
28
+ "providerId": "openai"
29
+ }
30
+ },
31
+ "sanitizedBindingDigest": "sha256:2cc57565f45a645f96060c046d27e313531f34ff7d8dc24732b5f85572ca12a8"
32
+ }
@@ -0,0 +1,46 @@
1
+ {
2
+ "schema": "harbour.binding-classification-subject/1.0",
3
+ "status": "COMPLETE",
4
+ "tenantId": "fourier",
5
+ "appId": "support-copilot",
6
+ "environment": "preview",
7
+ "sourceRef": "https://github.com/Fourier-Labs-AI/support-copilot",
8
+ "sourceRevision": "git-sha1:0123456789abcdef0123456789abcdef01234567",
9
+ "sourceRevisionDigest": "sha256:79520355eb30e88bb0c6656008dd301d8f3d7cef6c14a96882557f13f7c512d9",
10
+ "classifierRevision": "secret-name-classifier-2.0",
11
+ "classifications": {
12
+ "ANTHROPIC_API_KEY": {
13
+ "kind": "provider_credential",
14
+ "confidence": "high",
15
+ "providerId": "anthropic",
16
+ "category": "api_key",
17
+ "requestedAiMappingId": "anthropic-messages-v1"
18
+ },
19
+ "LOG_LEVEL": {
20
+ "kind": "configuration",
21
+ "confidence": "high"
22
+ },
23
+ "OPENAI_API_KEY": {
24
+ "kind": "provider_credential",
25
+ "confidence": "high",
26
+ "providerId": "openai",
27
+ "category": "api_key",
28
+ "requestedAiMappingId": "openai-responses-v1"
29
+ },
30
+ "STRIPE_API_KEY": {
31
+ "kind": "secret",
32
+ "confidence": "high"
33
+ }
34
+ },
35
+ "requestedAiMappings": {
36
+ "anthropic-messages-v1": {
37
+ "mappingVersion": "1.0.0",
38
+ "providerId": "anthropic"
39
+ },
40
+ "openai-responses-v1": {
41
+ "mappingVersion": "1.0.0",
42
+ "providerId": "openai"
43
+ }
44
+ },
45
+ "classificationDigest": "sha256:43622df1d8bc3660a160a120f9af896cfc9c98009547869895b47d4c61a29688"
46
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "schema": "harbour.sanitized-binding-evidence/1.0",
3
+ "tenantId": "fourier",
4
+ "appId": "support-copilot",
5
+ "environment": "preview",
6
+ "versionId": "fourier#support-copilot#preview#123456",
7
+ "sourceRef": "https://github.com/Fourier-Labs-AI/support-copilot",
8
+ "sourceRevision": "git-sha1:0123456789abcdef0123456789abcdef01234567",
9
+ "sourceRevisionDigest": "sha256:79520355eb30e88bb0c6656008dd301d8f3d7cef6c14a96882557f13f7c512d9",
10
+ "classificationDigest": "sha256:43622df1d8bc3660a160a120f9af896cfc9c98009547869895b47d4c61a29688",
11
+ "bindings": {
12
+ "LOG_LEVEL": {
13
+ "kind": "configuration",
14
+ "value": "debug"
15
+ },
16
+ "STRIPE_API_KEY": {
17
+ "kind": "secret",
18
+ "secretRef": "secretref://tenant/fourier/app/support-copilot/secret/STRIPE_API_KEY"
19
+ }
20
+ },
21
+ "requiredAiMappings": {
22
+ "anthropic-messages-v1": {
23
+ "mappingVersion": "1.0.0",
24
+ "providerId": "anthropic"
25
+ },
26
+ "openai-responses-v1": {
27
+ "mappingVersion": "1.0.0",
28
+ "providerId": "openai"
29
+ }
30
+ },
31
+ "sanitizedBindingDigest": "sha256:2cc57565f45a645f96060c046d27e313531f34ff7d8dc24732b5f85572ca12a8"
32
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "sourceRef": "https://github.com/Fourier-Labs-AI/support-copilot",
3
+ "sourceRevision": "git-sha1:0123456789abcdef0123456789abcdef01234567",
4
+ "preimage": "D27:harbour.source-revision/1.0\nS50:https://github.com/Fourier-Labs-AI/support-copilot49:git-sha1:0123456789abcdef0123456789abcdef01234567",
5
+ "digest": "sha256:79520355eb30e88bb0c6656008dd301d8f3d7cef6c14a96882557f13f7c512d9"
6
+ }
@@ -0,0 +1,44 @@
1
+ /* eslint-disable */
2
+ // Code generated by tools/gen/gen.mjs from schema/*.json. DO NOT EDIT.
3
+
4
+ import { createHash } from 'node:crypto';
5
+ import type { BindingClassificationSubject } from './binding-classification-subject.js';
6
+
7
+ const domain = 'harbour.binding-classification-subject/1.0';
8
+ const token = (value: string): string => Buffer.byteLength(value, 'utf8') + ':' + value;
9
+ const record = (tag: string, values: string[]): string => tag + values.map(token).join('');
10
+
11
+ /** Exact source-revision digest preimage shared by the producer and DCP. */
12
+ export function sourceRevisionPreimage(sourceRef: string, sourceRevision: string): string { return [record('D', ['harbour.source-revision/1.0']), record('S', [sourceRef, sourceRevision])].join('\n'); }
13
+
14
+ export function computeSourceRevisionDigest(sourceRef: string, sourceRevision: string): string { return 'sha256:' + createHash('sha256').update(sourceRevisionPreimage(sourceRef, sourceRevision), 'utf8').digest('hex'); }
15
+
16
+ /** Rejects an unknown or provider-mismatched requested AI mapping. JSON Schema owns field presence and bounds. */
17
+ export function assertBindingClassificationMappings(input: BindingClassificationSubject): void {
18
+ if (input.sourceRevisionDigest !== computeSourceRevisionDigest(input.sourceRef, input.sourceRevision)) throw new Error('sourceRevisionDigest mismatch');
19
+ for (const [name, entry] of Object.entries(input.classifications)) {
20
+ if (entry.kind !== 'provider_credential') continue;
21
+ const mapping = entry.requestedAiMappingId ? input.requestedAiMappings[entry.requestedAiMappingId] : undefined;
22
+ if (!mapping) throw new Error('unknown requestedAiMappingId for ' + name);
23
+ if (mapping.providerId !== entry.providerId) throw new Error('providerId mismatch for ' + name);
24
+ }
25
+ }
26
+
27
+ /** Proves the subject classifies every declared binding exactly once and no undeclared name. */
28
+ export function assertBindingClassificationCoverage(input: BindingClassificationSubject, bindingNames: readonly string[]): void {
29
+ const expected = [...bindingNames].sort();
30
+ if (new Set(expected).size !== expected.length) throw new Error('duplicate declared binding name');
31
+ const actual = Object.keys(input.classifications).sort();
32
+ if (expected.length !== actual.length || expected.some((name, index) => name !== actual[index])) throw new Error('binding classification coverage mismatch');
33
+ }
34
+
35
+ /** Exact UTF-8 length-prefixed preimage. Maps are sorted by key; classificationDigest is excluded. */
36
+ export function bindingClassificationPreimage(input: BindingClassificationSubject): string {
37
+ assertBindingClassificationMappings(input);
38
+ const rows = [record('D', [domain]), record('H', [input.schema, input.status, input.tenantId, input.appId, input.environment, input.sourceRef, input.sourceRevision, input.sourceRevisionDigest, input.classifierRevision])];
39
+ for (const name of Object.keys(input.classifications).sort()) { const entry = input.classifications[name]!; rows.push(record('C', [name, entry.kind, entry.confidence, entry.providerId ?? '', entry.category ?? '', entry.requestedAiMappingId ?? ''])); }
40
+ for (const mappingId of Object.keys(input.requestedAiMappings).sort()) { const mapping = input.requestedAiMappings[mappingId]!; rows.push(record('M', [mappingId, mapping.mappingVersion, mapping.providerId])); }
41
+ return rows.join('\n');
42
+ }
43
+
44
+ export function computeBindingClassificationDigest(input: BindingClassificationSubject): string { return 'sha256:' + createHash('sha256').update(bindingClassificationPreimage(input), 'utf8').digest('hex'); }
@@ -0,0 +1,60 @@
1
+ /* eslint-disable */
2
+ // Code generated by tools/gen/gen.mjs from schema/*.json. DO NOT EDIT.
3
+
4
+ export type BindingClassificationIdentifier = string;
5
+ /**
6
+ * Provider-neutral, normalized and credential-free source repository reference. Current GitHub HTTPS references and future SCM adapters use the same safe hierarchical grammar; userinfo, ports, percent encoding, query strings and fragments are unrepresentable.
7
+ */
8
+ export type BindingClassificationSourceRef = string;
9
+ /**
10
+ * Provider-neutral exact revision identifier expressed as algorithm:value. Current Git adapters use git-sha1:<40 lowercase hex>; another SCM or Git hash algorithm does not require a shape change.
11
+ */
12
+ export type BindingClassificationSourceRevision = string;
13
+ export type BindingClassificationDigest = string;
14
+ export type BindingClassificationEntry = {
15
+ kind: 'configuration' | 'secret' | 'provider_credential';
16
+ confidence: 'high' | 'low';
17
+ providerId?: BindingClassificationProviderId;
18
+ category?: BindingClassificationCredentialCategory;
19
+ requestedAiMappingId?: BindingClassificationIdentifier;
20
+ };
21
+ export type BindingClassificationProviderId = 'openai' | 'anthropic' | 'gemini' | 'openrouter';
22
+ /**
23
+ * Closed current credential category. Add a reviewed contract version before supporting another credential mechanism.
24
+ */
25
+ export type BindingClassificationCredentialCategory = 'api_key';
26
+
27
+ /**
28
+ * Complete, immutable and value-free classification of the exact declared environment-binding names for one provider-neutral normalized source revision before DCP allocates a version. The caller never guesses a versionId. DCP validates this subject, allocates the sole versionId and chains classificationDigest into separately persisted SanitizedBindingEvidence; it must never relabel this source-bound evidence.
29
+ */
30
+ export interface BindingClassificationSubject {
31
+ schema: 'harbour.binding-classification-subject/1.0';
32
+ /**
33
+ * Partial or unavailable classification cannot be represented as an admissible subject.
34
+ */
35
+ status: 'COMPLETE';
36
+ tenantId: BindingClassificationIdentifier;
37
+ appId: BindingClassificationIdentifier;
38
+ environment: 'preview' | 'prod';
39
+ sourceRef: BindingClassificationSourceRef;
40
+ sourceRevision: BindingClassificationSourceRevision;
41
+ sourceRevisionDigest: BindingClassificationDigest;
42
+ classifierRevision: string;
43
+ /**
44
+ * Exact classification set keyed by every declared environment-binding name. Object keys make missing duplication tricks and two classifications for one name unrepresentable. Literal values and secret references are intentionally unrepresentable. DCP additionally requires these keys to equal the names in the request's environmentBindings before allocation.
45
+ */
46
+ classifications: {
47
+ [k: string]: BindingClassificationEntry;
48
+ };
49
+ /**
50
+ * Exact set keyed by requested AI mapping ID of supported mappings intended to own provider-client credentials for this source revision. This is pre-PLAN request evidence, not a claim that enforcement already exists. Provider-credential classifications reference one of these exact mapping IDs.
51
+ */
52
+ requestedAiMappings: {
53
+ [k: string]: BindingClassificationAiMapping;
54
+ };
55
+ classificationDigest: BindingClassificationDigest;
56
+ }
57
+ export interface BindingClassificationAiMapping {
58
+ mappingVersion: string;
59
+ providerId: BindingClassificationProviderId;
60
+ }
@@ -1,10 +1,10 @@
1
1
  /* eslint-disable */
2
2
  // Code generated by tools/gen/gen.mjs from schema/*.json. DO NOT EDIT.
3
3
 
4
- export const ContractRepositoryVersion = "0.9.0.0" as const;
5
- export const ContractPublishVersion = "0.9.0" as const;
6
- export const ContractPublishTag = "v0.9.0" as const;
7
- export const ContractSchemasDigest = "sha256:df73090b9fbbac8a39d0ec92e929f4d687bd7920de583fb2463bf8570dc68ebb" as const;
8
- export const ContractAiFixturesDigest = "sha256:180a5f30cf0b6d93fa4c812a217d2365ffe22ac54d11626e71f8072fcf8f67c6" as const;
9
- export const ContractGeneratedGoDigest = "sha256:be376911aec8ee6359a38fdafebf343ce157255e19c544617b7bcccd4329e5de" as const;
10
- export const ContractGeneratedTypeScriptDigest = "sha256:857288e60b476b42dce0304ec05aff6af1f2719278959ed03740ef88af5e736b" as const;
4
+ export const ContractRepositoryVersion = "0.10.0.0" as const;
5
+ export const ContractPublishVersion = "0.10.0" as const;
6
+ export const ContractPublishTag = "v0.10.0" as const;
7
+ export const ContractSchemasDigest = "sha256:e22fd6329b9064a3d653dbc6bc3ee0c2033ee6d8d279f06532a355c9285aa7fe" as const;
8
+ export const ContractAiFixturesDigest = "sha256:8355039f14457087d36aee52ae1a0a93096bf12f34bd8af0d948c605b6dff226" as const;
9
+ export const ContractGeneratedGoDigest = "sha256:d1a57e9b7b414bc239af4dd897c444bfac08e3bfbb24426d525faad6878d6a26" as const;
10
+ export const ContractGeneratedTypeScriptDigest = "sha256:e455c76468808b1191a3d2d774786bb18dc435785d9fa31a559f6b93f3e160c9" as const;
@@ -21,6 +21,16 @@ export type DeployRequestEnvironmentBinding = {
21
21
  export type DeployRequestEnvironmentBinding1 = {
22
22
  [k: string]: unknown;
23
23
  };
24
+ type BindingClassificationEntry = {
25
+ kind: 'configuration' | 'secret' | 'provider_credential';
26
+ confidence: 'high' | 'low';
27
+ providerId?: 'openai' | 'anthropic' | 'gemini' | 'openrouter';
28
+ /**
29
+ * Closed current credential category. Add a reviewed contract version before supporting another credential mechanism.
30
+ */
31
+ category?: 'api_key';
32
+ requestedAiMappingId?: string;
33
+ };
24
34
 
25
35
  /**
26
36
  * JSON body for POST /v1/tenants/{tenant}/apps/{app}/deployments. tenant and app come from path parameters and are not body fields. The preview MVP requires environment=preview. Every accepted POST is a new deployment intent; idempotencyKey is retained only as request correlation.
@@ -44,6 +54,7 @@ export interface DeployRequest {
44
54
  */
45
55
  idempotencyKey?: string;
46
56
  environmentBindings?: DeployRequestEnvironmentBindings;
57
+ bindingClassification: BindingClassificationSubject;
47
58
  }
48
59
  export interface DeployRequestSource {
49
60
  /**
@@ -64,3 +75,43 @@ export interface DeployRequestEnvironmentBindings {
64
75
  */
65
76
  bindings: DeployRequestEnvironmentBinding[];
66
77
  }
78
+ /**
79
+ * Complete pre-allocation, value-free binding-name classification. DCP verifies its tenant/app/environment/source identity and exact name coverage before minting the versionId.
80
+ */
81
+ interface BindingClassificationSubject {
82
+ schema: 'harbour.binding-classification-subject/1.0';
83
+ /**
84
+ * Partial or unavailable classification cannot be represented as an admissible subject.
85
+ */
86
+ status: 'COMPLETE';
87
+ tenantId: string;
88
+ appId: string;
89
+ environment: 'preview' | 'prod';
90
+ /**
91
+ * Provider-neutral, normalized and credential-free source repository reference. Current GitHub HTTPS references and future SCM adapters use the same safe hierarchical grammar; userinfo, ports, percent encoding, query strings and fragments are unrepresentable.
92
+ */
93
+ sourceRef: string;
94
+ /**
95
+ * Provider-neutral exact revision identifier expressed as algorithm:value. Current Git adapters use git-sha1:<40 lowercase hex>; another SCM or Git hash algorithm does not require a shape change.
96
+ */
97
+ sourceRevision: string;
98
+ sourceRevisionDigest: string;
99
+ classifierRevision: string;
100
+ /**
101
+ * Exact classification set keyed by every declared environment-binding name. Object keys make missing duplication tricks and two classifications for one name unrepresentable. Literal values and secret references are intentionally unrepresentable. DCP additionally requires these keys to equal the names in the request's environmentBindings before allocation.
102
+ */
103
+ classifications: {
104
+ [k: string]: BindingClassificationEntry;
105
+ };
106
+ /**
107
+ * Exact set keyed by requested AI mapping ID of supported mappings intended to own provider-client credentials for this source revision. This is pre-PLAN request evidence, not a claim that enforcement already exists. Provider-credential classifications reference one of these exact mapping IDs.
108
+ */
109
+ requestedAiMappings: {
110
+ [k: string]: BindingClassificationAiMapping;
111
+ };
112
+ classificationDigest: string;
113
+ }
114
+ interface BindingClassificationAiMapping {
115
+ mappingVersion: string;
116
+ providerId: 'openai' | 'anthropic' | 'gemini' | 'openrouter';
117
+ }
@@ -17,6 +17,14 @@ export interface DeployResponse {
17
17
  app: string;
18
18
  environment: 'preview';
19
19
  status: 'QUEUED';
20
+ /**
21
+ * Digest of the validated pre-allocation BindingClassificationSubject.
22
+ */
23
+ classificationDigest: string;
24
+ /**
25
+ * Digest of the exact version-bound SanitizedBindingEvidence persisted atomically before this response.
26
+ */
27
+ sanitizedBindingDigest: string;
20
28
  acceptedAt: string;
21
29
  /**
22
30
  * Relative URL for exact-version Status. The versionId path segment is URL encoded.
package/gen-ts/index.ts CHANGED
@@ -23,6 +23,8 @@ export * from './ai-transform-result.js';
23
23
  export * from './ai-verification-evidence.js';
24
24
  export * from './api-error-response.js';
25
25
  export * from './artifact-contract.js';
26
+ export * from './binding-classification-digest-helper.js';
27
+ export * from './binding-classification-subject.js';
26
28
  export * from './contract-release.js';
27
29
  export * from './data-dependency.js';
28
30
  export * from './data-grant.js';
@@ -41,6 +43,8 @@ export * from './error-envelope.js';
41
43
  export * from './kvs-routing-entry.js';
42
44
  export * from './promotion-request.js';
43
45
  export * from './promotion-response.js';
46
+ export * from './sanitized-binding-digest-helper.js';
47
+ export * from './sanitized-binding-evidence.js';
44
48
  export * from './secret-reference.js';
45
49
  export * from './transform-report.js';
46
50
  export * from './version-state-machine.js';
@@ -1,6 +1,17 @@
1
1
  /* eslint-disable */
2
2
  // Code generated by tools/gen/gen.mjs from schema/*.json. DO NOT EDIT.
3
3
 
4
+ type BindingClassificationEntry = {
5
+ kind: 'configuration' | 'secret' | 'provider_credential';
6
+ confidence: 'high' | 'low';
7
+ providerId?: 'openai' | 'anthropic' | 'gemini' | 'openrouter';
8
+ /**
9
+ * Closed current credential category. Add a reviewed contract version before supporting another credential mechanism.
10
+ */
11
+ category?: 'api_key';
12
+ requestedAiMappingId?: string;
13
+ };
14
+
4
15
  /**
5
16
  * JSON body for POST /v1/tenants/{tenant}/apps/{app}/promotions. Repository identity is loaded from stored application and preview records, not supplied by the caller.
6
17
  */
@@ -18,6 +29,7 @@ export interface PromotionRequest {
18
29
  */
19
30
  idempotencyKey: string;
20
31
  environmentBindings?: PromotionRequestEnvironmentBindings;
32
+ bindingClassification: BindingClassificationSubject;
21
33
  }
22
34
  /**
23
35
  * Optional secret bindings for the production version. Additive: promotions without it remain valid, and a deployment plane that ignores the field behaves exactly as before. Named environmentBindings to match deploy-request.schema.json, whose preview counterpart this mirrors.
@@ -41,3 +53,43 @@ export interface PromotionRequestSecretBinding {
41
53
  */
42
54
  secretRef: string;
43
55
  }
56
+ /**
57
+ * Complete pre-allocation, value-free classification for the exact production source revision and declared production binding names. It intentionally has no production versionId; DCP allocates that identity atomically.
58
+ */
59
+ interface BindingClassificationSubject {
60
+ schema: 'harbour.binding-classification-subject/1.0';
61
+ /**
62
+ * Partial or unavailable classification cannot be represented as an admissible subject.
63
+ */
64
+ status: 'COMPLETE';
65
+ tenantId: string;
66
+ appId: string;
67
+ environment: 'preview' | 'prod';
68
+ /**
69
+ * Provider-neutral, normalized and credential-free source repository reference. Current GitHub HTTPS references and future SCM adapters use the same safe hierarchical grammar; userinfo, ports, percent encoding, query strings and fragments are unrepresentable.
70
+ */
71
+ sourceRef: string;
72
+ /**
73
+ * Provider-neutral exact revision identifier expressed as algorithm:value. Current Git adapters use git-sha1:<40 lowercase hex>; another SCM or Git hash algorithm does not require a shape change.
74
+ */
75
+ sourceRevision: string;
76
+ sourceRevisionDigest: string;
77
+ classifierRevision: string;
78
+ /**
79
+ * Exact classification set keyed by every declared environment-binding name. Object keys make missing duplication tricks and two classifications for one name unrepresentable. Literal values and secret references are intentionally unrepresentable. DCP additionally requires these keys to equal the names in the request's environmentBindings before allocation.
80
+ */
81
+ classifications: {
82
+ [k: string]: BindingClassificationEntry;
83
+ };
84
+ /**
85
+ * Exact set keyed by requested AI mapping ID of supported mappings intended to own provider-client credentials for this source revision. This is pre-PLAN request evidence, not a claim that enforcement already exists. Provider-credential classifications reference one of these exact mapping IDs.
86
+ */
87
+ requestedAiMappings: {
88
+ [k: string]: BindingClassificationAiMapping;
89
+ };
90
+ classificationDigest: string;
91
+ }
92
+ interface BindingClassificationAiMapping {
93
+ mappingVersion: string;
94
+ providerId: 'openai' | 'anthropic' | 'gemini' | 'openrouter';
95
+ }
@@ -15,6 +15,14 @@ export interface PromotionResponse {
15
15
  app: string;
16
16
  environment: 'prod';
17
17
  status: 'QUEUED';
18
+ /**
19
+ * Digest of the validated production BindingClassificationSubject.
20
+ */
21
+ classificationDigest: string;
22
+ /**
23
+ * Digest of the exact production-version SanitizedBindingEvidence persisted atomically before this response.
24
+ */
25
+ sanitizedBindingDigest: string;
18
26
  acceptedAt: string;
19
27
  statusUrl: string;
20
28
  }
@@ -0,0 +1,33 @@
1
+ /* eslint-disable */
2
+ // Code generated by tools/gen/gen.mjs from schema/*.json. DO NOT EDIT.
3
+
4
+ import { createHash } from 'node:crypto';
5
+ import type { BindingClassificationSubject } from './binding-classification-subject.js';
6
+ import type { SanitizedBindingEvidence } from './sanitized-binding-evidence.js';
7
+
8
+ const domain = 'harbour.sanitized-binding-evidence/1.0';
9
+ const token = (value: string): string => Buffer.byteLength(value, 'utf8') + ':' + value;
10
+ const record = (tag: string, values: string[]): string => tag + values.map(token).join('');
11
+
12
+ function classificationDigestForReceipt(input: BindingClassificationSubject): string { const sourcePreimage = [record('D', ['harbour.source-revision/1.0']), record('S', [input.sourceRef, input.sourceRevision])].join('\n'); if (input.sourceRevisionDigest !== 'sha256:' + createHash('sha256').update(sourcePreimage, 'utf8').digest('hex')) throw new Error('sourceRevisionDigest mismatch'); const rows = [record('D', ['harbour.binding-classification-subject/1.0']), record('H', [input.schema, input.status, input.tenantId, input.appId, input.environment, input.sourceRef, input.sourceRevision, input.sourceRevisionDigest, input.classifierRevision])]; for (const name of Object.keys(input.classifications).sort()) { const entry = input.classifications[name]!; const mapping = entry.requestedAiMappingId ? input.requestedAiMappings[entry.requestedAiMappingId] : undefined; if (entry.kind === 'provider_credential' && (!mapping || mapping.providerId !== entry.providerId)) throw new Error('requested AI mapping mismatch for ' + name); rows.push(record('C', [name, entry.kind, entry.confidence, entry.providerId ?? '', entry.category ?? '', entry.requestedAiMappingId ?? ''])); } for (const mappingId of Object.keys(input.requestedAiMappings).sort()) { const mapping = input.requestedAiMappings[mappingId]!; rows.push(record('M', [mappingId, mapping.mappingVersion, mapping.providerId])); } return 'sha256:' + createHash('sha256').update(rows.join('\n'), 'utf8').digest('hex'); }
13
+
14
+ /** Exact UTF-8 length-prefixed preimage. Maps are sorted by key; sanitizedBindingDigest is excluded. */
15
+ export function sanitizedBindingPreimage(input: SanitizedBindingEvidence): string {
16
+ const rows = [record('D', [domain]), record('H', [input.schema, input.tenantId, input.appId, input.environment, input.versionId, input.sourceRef, input.sourceRevision, input.sourceRevisionDigest, input.classificationDigest])];
17
+ for (const name of Object.keys(input.bindings).sort()) { const binding = input.bindings[name]!; if (binding.kind === 'configuration' && binding.value === undefined) throw new Error('configuration value missing for ' + name); if (binding.kind === 'secret' && binding.secretRef === undefined) throw new Error('secretRef missing for ' + name); rows.push(record('B', [name, binding.kind, binding.value ?? '', binding.secretRef ?? ''])); }
18
+ for (const mappingId of Object.keys(input.requiredAiMappings).sort()) { const mapping = input.requiredAiMappings[mappingId]!; rows.push(record('M', [mappingId, mapping.mappingVersion, mapping.providerId])); }
19
+ return rows.join('\n');
20
+ }
21
+
22
+ export function computeSanitizedBindingDigest(input: SanitizedBindingEvidence): string { return 'sha256:' + createHash('sha256').update(sanitizedBindingPreimage(input), 'utf8').digest('hex'); }
23
+
24
+ /** Verifies the atomic allocation receipt cannot relabel source/classification/mappings or retain a provider credential. */
25
+ export function assertSanitizedBindingEvidence(subject: BindingClassificationSubject, evidence: SanitizedBindingEvidence, allocatedVersionId: string): void {
26
+ for (const field of ['tenantId', 'appId', 'environment', 'sourceRef', 'sourceRevision', 'sourceRevisionDigest'] as const) if (subject[field] !== evidence[field]) throw new Error(field + ' mismatch');
27
+ if (evidence.versionId !== allocatedVersionId) throw new Error('versionId mismatch');
28
+ if (subject.classificationDigest !== classificationDigestForReceipt(subject) || evidence.classificationDigest !== subject.classificationDigest) throw new Error('classificationDigest mismatch');
29
+ const requested = Object.keys(subject.requestedAiMappings).sort(); const required = Object.keys(evidence.requiredAiMappings).sort(); if (requested.length !== required.length || requested.some((id, index) => { const a = subject.requestedAiMappings[id]!; const b = evidence.requiredAiMappings[id]; return id !== required[index] || !b || a.mappingVersion !== b.mappingVersion || a.providerId !== b.providerId; })) throw new Error('requiredAiMappings mismatch');
30
+ const expectedNames = Object.keys(subject.classifications).filter(name => subject.classifications[name]!.kind !== 'provider_credential').sort(); const actualNames = Object.keys(evidence.bindings).sort(); if (expectedNames.length !== actualNames.length || expectedNames.some((name, index) => name !== actualNames[index])) throw new Error('sanitized binding coverage mismatch');
31
+ for (const name of expectedNames) if (subject.classifications[name]!.kind !== evidence.bindings[name]!.kind) throw new Error('sanitized binding kind mismatch for ' + name);
32
+ if (evidence.sanitizedBindingDigest !== computeSanitizedBindingDigest(evidence)) throw new Error('sanitizedBindingDigest mismatch');
33
+ }
@@ -0,0 +1,51 @@
1
+ /* eslint-disable */
2
+ // Code generated by tools/gen/gen.mjs from schema/*.json. DO NOT EDIT.
3
+
4
+ export type SanitizedBindingIdentifier = string;
5
+ export type SanitizedBindingSourceRef = string;
6
+ export type SanitizedBindingSourceRevision = string;
7
+ export type SanitizedBindingDigest = string;
8
+ export type SanitizedRuntimeBinding = {
9
+ kind: 'configuration' | 'secret';
10
+ /**
11
+ * Approved non-secret configuration literal. Provider-credential values are never approved or persisted here.
12
+ */
13
+ value?: string;
14
+ /**
15
+ * Value-free app-secret reference; tenant integration and provider credential references are outside this contract.
16
+ */
17
+ secretRef?: string;
18
+ };
19
+ export type SanitizedBindingProviderId = 'openai' | 'anthropic' | 'gemini' | 'openrouter';
20
+
21
+ /**
22
+ * Immutable DCP allocation receipt and sanitized runtime-binding set for one exact allocated version and normalized source revision. DCP persists this evidence atomically with version admission before publish or provisioning. classificationDigest chains to the caller's pre-allocation BindingClassificationSubject; bindings contain only approved configuration or app-secret bindings and never an AI-mapping-owned provider credential.
23
+ */
24
+ export interface SanitizedBindingEvidence {
25
+ schema: 'harbour.sanitized-binding-evidence/1.0';
26
+ tenantId: SanitizedBindingIdentifier;
27
+ appId: SanitizedBindingIdentifier;
28
+ environment: 'preview' | 'prod';
29
+ versionId: string;
30
+ sourceRef: SanitizedBindingSourceRef;
31
+ sourceRevision: SanitizedBindingSourceRevision;
32
+ sourceRevisionDigest: SanitizedBindingDigest;
33
+ classificationDigest: SanitizedBindingDigest;
34
+ /**
35
+ * Exact approved runtime-binding set keyed by environment name. Object keys make duplicate bindings unrepresentable. A provider_credential kind and rejected binding metadata/value are unrepresentable.
36
+ */
37
+ bindings: {
38
+ [k: string]: SanitizedRuntimeBinding;
39
+ };
40
+ /**
41
+ * DCP-validated supported AI mapping requirements keyed by mapping ID. This records what later PLAN/provisioning milestones must enforce; it does not claim those later controls are already active.
42
+ */
43
+ requiredAiMappings: {
44
+ [k: string]: SanitizedBindingAiMapping;
45
+ };
46
+ sanitizedBindingDigest: SanitizedBindingDigest;
47
+ }
48
+ export interface SanitizedBindingAiMapping {
49
+ mappingVersion: string;
50
+ providerId: SanitizedBindingProviderId;
51
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fourier-labs/harbour-contracts",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Single source of truth for every data shape that crosses a boundary between Harbour components.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,