gdc-common-utils-ts 2.5.2 → 2.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -450,9 +450,10 @@ The canonical API contract should live in JSDoc on exported code. The README act
450
450
  - [`buildSmartCompositionReadScope(...)`](src/utils/smart-scope.ts)
451
451
  - Builds the current CORE GW pinned SMART root scope for `organization/Composition...` token requests.
452
452
  - This is the preferred first scope to teach when the backend only needs subject-scoped read access.
453
- - [`getOrganizationCredentialFromVpToken(...)`, `getLegalRepresentativeCredentialFromVpToken(...)`, `getOrganizationControllerCredentialFromVpToken(...)`](src/utils/vp-token.ts)
453
+ - [`getOrganizationCredentialFromVpToken(...)`, `getLegalRepresentativeCredentialFromVpToken(...)`, `getServiceControllerCredentialFromVpToken(...)`](src/utils/vp-token.ts)
454
454
  - Extract typed VC objects from a VP token when GW/SDK flows carry canonical proof only in `vp_token`.
455
- - [`readOrganizationControllerCredentialsFromResponseBody(...)`](src/utils/legal-organization-verification-result.ts)
455
+ - [`readServiceControllerCredentialsFromResponseBody(...)`](src/utils/legal-organization-verification-result.ts)
456
+ - [`extractServiceControllerRoleCodes(...)`, `extractServiceControllerOccupationCodes(...)`](src/utils/activation-policy.ts) read controller authority from `owner.additionalType` and professional occupation from `owner.hasOccupation.occupationalCategory`.
456
457
  - Reads every independently issued controller service VC from direct ICA or projected GW responses; companion helpers expose `owner.sameAs` and its JWK-thumbprint binding without falling back to the legal representative.
457
458
  - [`docs/101-VP_TOKEN.md`](docs/101-VP_TOKEN.md)
458
459
  - Step-by-step guide for building the canonical compact `vp_token` string from organization and representative VCs.
@@ -155,6 +155,8 @@ export declare enum ClaimsOrderSchemaorg {
155
155
  * conventions and international contexts.
156
156
  */
157
157
  export declare enum ClaimsPersonSchemaorg {
158
+ /** Schema.org additional type; service-controller VCs use bare `RESPRSN`. */
159
+ additionalType = "org.schema.Person.additionalType",
158
160
  /** Second surname or mother's maiden name */
159
161
  additionalName = "org.schema.Person.additionalName",
160
162
  /** Short friendly name */
@@ -165,6 +167,8 @@ export declare enum ClaimsPersonSchemaorg {
165
167
  gender = "org.schema.Person.gender",
166
168
  givenName = "org.schema.Person.givenName",
167
169
  hasOccupation = "org.schema.Person.hasOccupation",
170
+ /** ISCO taxonomy token carried by `Occupation.occupationalCategory`. */
171
+ hasOccupationalCategory = "org.schema.Person.hasOccupation.occupationalCategory",
168
172
  /**
169
173
  * Canonical nested role value used in ICA/VC credential subjects and
170
174
  * supported by activation/normalization helpers.
@@ -163,6 +163,8 @@ export var ClaimsOrderSchemaorg;
163
163
  */
164
164
  export var ClaimsPersonSchemaorg;
165
165
  (function (ClaimsPersonSchemaorg) {
166
+ /** Schema.org additional type; service-controller VCs use bare `RESPRSN`. */
167
+ ClaimsPersonSchemaorg["additionalType"] = "org.schema.Person.additionalType";
166
168
  /** Second surname or mother's maiden name */
167
169
  ClaimsPersonSchemaorg["additionalName"] = "org.schema.Person.additionalName";
168
170
  /** Short friendly name */
@@ -173,6 +175,8 @@ export var ClaimsPersonSchemaorg;
173
175
  ClaimsPersonSchemaorg["gender"] = "org.schema.Person.gender";
174
176
  ClaimsPersonSchemaorg["givenName"] = "org.schema.Person.givenName";
175
177
  ClaimsPersonSchemaorg["hasOccupation"] = "org.schema.Person.hasOccupation";
178
+ /** ISCO taxonomy token carried by `Occupation.occupationalCategory`. */
179
+ ClaimsPersonSchemaorg["hasOccupationalCategory"] = "org.schema.Person.hasOccupation.occupationalCategory";
176
180
  /**
177
181
  * Canonical nested role value used in ICA/VC credential subjects and
178
182
  * supported by activation/normalization helpers.
@@ -28,6 +28,8 @@ export declare const ActivationCredentialTypes: Readonly<{
28
28
  OrganizationCredential: "OrganizationCredential";
29
29
  LegalOrganizationCredential: "LegalOrganizationCredential";
30
30
  LegalRepresentativeCredential: "LegalRepresentativeCredential";
31
+ ServiceControllerCredential: "ServiceControllerCredential";
32
+ /** @deprecated Read-only compatibility with pre-2.6 controller VCs. */
31
33
  OrganizationControllerCredential: "OrganizationControllerCredential";
32
34
  PersonCredential: "PersonCredential";
33
35
  }>;
@@ -69,4 +71,6 @@ export declare const REPRESENTATIVE_ACTIVATION_VC_TYPES: readonly ("LegalReprese
69
71
  * Canonical credential subtype used to bind an organization tenant service to
70
72
  * one independently authenticated controller actor and its signing key.
71
73
  */
72
- export declare const ORGANIZATION_CONTROLLER_ACTIVATION_VC_TYPES: readonly "OrganizationControllerCredential"[];
74
+ export declare const SERVICE_CONTROLLER_ACTIVATION_VC_TYPES: readonly ("ServiceControllerCredential" | "OrganizationControllerCredential")[];
75
+ /** @deprecated Use `SERVICE_CONTROLLER_ACTIVATION_VC_TYPES`. */
76
+ export declare const ORGANIZATION_CONTROLLER_ACTIVATION_VC_TYPES: readonly ("ServiceControllerCredential" | "OrganizationControllerCredential")[];
@@ -30,6 +30,8 @@ export const ActivationCredentialTypes = Object.freeze({
30
30
  OrganizationCredential: 'OrganizationCredential',
31
31
  LegalOrganizationCredential: 'LegalOrganizationCredential',
32
32
  LegalRepresentativeCredential: 'LegalRepresentativeCredential',
33
+ ServiceControllerCredential: 'ServiceControllerCredential',
34
+ /** @deprecated Read-only compatibility with pre-2.6 controller VCs. */
33
35
  OrganizationControllerCredential: 'OrganizationControllerCredential',
34
36
  PersonCredential: 'PersonCredential',
35
37
  });
@@ -77,6 +79,9 @@ export const REPRESENTATIVE_ACTIVATION_VC_TYPES = Object.freeze([
77
79
  * Canonical credential subtype used to bind an organization tenant service to
78
80
  * one independently authenticated controller actor and its signing key.
79
81
  */
80
- export const ORGANIZATION_CONTROLLER_ACTIVATION_VC_TYPES = Object.freeze([
82
+ export const SERVICE_CONTROLLER_ACTIVATION_VC_TYPES = Object.freeze([
83
+ ActivationCredentialTypes.ServiceControllerCredential,
81
84
  ActivationCredentialTypes.OrganizationControllerCredential,
82
85
  ]);
86
+ /** @deprecated Use `SERVICE_CONTROLLER_ACTIVATION_VC_TYPES`. */
87
+ export const ORGANIZATION_CONTROLLER_ACTIVATION_VC_TYPES = SERVICE_CONTROLLER_ACTIVATION_VC_TYPES;
@@ -5,9 +5,9 @@
5
5
  * - controller-signing/audience ids and VC subtype names must be imported from
6
6
  * this module instead of re-hardcoded
7
7
  * inline
8
- * - the representative `hasCredential.material` shape below reflects the
9
- * current `activation-policy` helper contract; if ICA finalizes a different
10
- * VC shape, update this module first and then the dependent helpers/tests
8
+ * - the representative VC carries the legal occupation, while the separate
9
+ * service-controller VC carries `RESPRSN`, the technical ISCO occupation and the
10
+ * controller key binding
11
11
  *
12
12
  * Modeling note:
13
13
  * - this onboarding example intentionally anchors the business subject on the
@@ -24,6 +24,8 @@ export declare const EXAMPLE_ORG_CONTROLLER_SIGNING_KEY_ID: "urn:ietf:params:oau
24
24
  export declare const EXAMPLE_PRESENTATION_AUDIENCE_HOST_ID: "host:node-operator-es";
25
25
  export declare const EXAMPLE_ORGANIZATION_TAX_ID: "ESB00112233";
26
26
  export declare const EXAMPLE_REPRESENTATIVE_ROLE_CODE: "RESPRSN";
27
+ export declare const EXAMPLE_LEGAL_REPRESENTATIVE_ISCO_CODE: "1120";
28
+ export declare const EXAMPLE_TECHNICAL_CONTROLLER_ISCO_CODE: "1330";
27
29
  export declare const EXAMPLE_REPRESENTATIVE_IDENTIFIER: "IDCES-99999999R";
28
30
  export declare const EXAMPLE_REPRESENTATIVE_EMAIL: "legal.rep@example.org";
29
31
  export declare const EXAMPLE_REPRESENTATIVE_SUBJECT_URN: "urn:person:identifier:IDCES-99999999R";
@@ -55,15 +57,37 @@ export declare const EXAMPLE_ORG_ACTIVATION_LEGAL_REPRESENTATIVE_CREDENTIAL: Rea
55
57
  taxID: "ESB00112233";
56
58
  };
57
59
  hasOccupation: {
58
- identifier: "RESPRSN";
59
- };
60
- hasCredential: {
61
- material: "urn:ietf:params:oauth:jwk-thumbprint:sha-256:Q0ZfM0V4YW1wbGVUaHVtYnByaW50X2Jhc2U2NHVybA";
60
+ '@type': string;
61
+ occupationalCategory: string;
62
62
  };
63
63
  identifier: "IDCES-99999999R";
64
64
  sameAs: "urn:multibase:zControllerHash";
65
65
  };
66
66
  }>;
67
+ export declare const EXAMPLE_ORG_ACTIVATION_CONTROLLER_CREDENTIAL: Readonly<{
68
+ '@context': string[];
69
+ type: ("VerifiableCredential" | "ServiceControllerCredential")[];
70
+ credentialSubject: {
71
+ id: "ESB00112233";
72
+ '@type': string;
73
+ serviceType: string;
74
+ provider: {
75
+ taxID: "ESB00112233";
76
+ };
77
+ owner: {
78
+ '@type': string;
79
+ additionalType: "RESPRSN";
80
+ sameAs: "urn:multibase:zControllerHash";
81
+ hasOccupation: {
82
+ '@type': string;
83
+ occupationalCategory: string;
84
+ };
85
+ hasCredential: {
86
+ material: "urn:ietf:params:oauth:jwk-thumbprint:sha-256:Q0ZfM0V4YW1wbGVUaHVtYnByaW50X2Jhc2U2NHVybA";
87
+ };
88
+ };
89
+ };
90
+ }>;
67
91
  export declare const EXAMPLE_ORG_ACTIVATION_PROOF_VP_PAYLOAD: Readonly<{
68
92
  iss: "urn:ietf:params:oauth:jwk-thumbprint:sha-256:Q0ZfM0V4YW1wbGVUaHVtYnByaW50X2Jhc2U2NHVybA";
69
93
  sub: "ESB00112233";
@@ -11,9 +11,9 @@ import { DataspaceSectors } from '../constants/sectors.js';
11
11
  * - controller-signing/audience ids and VC subtype names must be imported from
12
12
  * this module instead of re-hardcoded
13
13
  * inline
14
- * - the representative `hasCredential.material` shape below reflects the
15
- * current `activation-policy` helper contract; if ICA finalizes a different
16
- * VC shape, update this module first and then the dependent helpers/tests
14
+ * - the representative VC carries the legal occupation, while the separate
15
+ * service-controller VC carries `RESPRSN`, the technical ISCO occupation and the
16
+ * controller key binding
17
17
  *
18
18
  * Modeling note:
19
19
  * - this onboarding example intentionally anchors the business subject on the
@@ -30,6 +30,8 @@ export const EXAMPLE_ORG_CONTROLLER_SIGNING_KEY_ID = `${UrnPrefixes.JwkThumbprin
30
30
  export const EXAMPLE_PRESENTATION_AUDIENCE_HOST_ID = 'host:node-operator-es';
31
31
  export const EXAMPLE_ORGANIZATION_TAX_ID = 'ESB00112233';
32
32
  export const EXAMPLE_REPRESENTATIVE_ROLE_CODE = 'RESPRSN';
33
+ export const EXAMPLE_LEGAL_REPRESENTATIVE_ISCO_CODE = '1120';
34
+ export const EXAMPLE_TECHNICAL_CONTROLLER_ISCO_CODE = '1330';
33
35
  export const EXAMPLE_REPRESENTATIVE_IDENTIFIER = 'IDCES-99999999R';
34
36
  export const EXAMPLE_REPRESENTATIVE_EMAIL = 'legal.rep@example.org';
35
37
  export const EXAMPLE_REPRESENTATIVE_SUBJECT_URN = `urn:person:identifier:${EXAMPLE_REPRESENTATIVE_IDENTIFIER}`;
@@ -67,15 +69,36 @@ export const EXAMPLE_ORG_ACTIVATION_LEGAL_REPRESENTATIVE_CREDENTIAL = Object.fre
67
69
  taxID: EXAMPLE_ORGANIZATION_TAX_ID,
68
70
  },
69
71
  hasOccupation: {
70
- identifier: EXAMPLE_REPRESENTATIVE_ROLE_CODE,
71
- },
72
- hasCredential: {
73
- material: EXAMPLE_ORG_CONTROLLER_SIGNING_KEY_ID,
72
+ '@type': 'Occupation',
73
+ occupationalCategory: `ISCO-08|${EXAMPLE_LEGAL_REPRESENTATIVE_ISCO_CODE}`,
74
74
  },
75
75
  identifier: EXAMPLE_REPRESENTATIVE_IDENTIFIER,
76
76
  sameAs: EXAMPLE_REPRESENTATIVE_SAME_AS,
77
77
  },
78
78
  });
79
+ export const EXAMPLE_ORG_ACTIVATION_CONTROLLER_CREDENTIAL = Object.freeze({
80
+ '@context': [W3cCredentialContexts.V2, 'https://schema.org'],
81
+ type: [
82
+ W3cCredentialTypes.VerifiableCredential,
83
+ ActivationCredentialTypes.ServiceControllerCredential,
84
+ ],
85
+ credentialSubject: {
86
+ id: EXAMPLE_ORGANIZATION_ID,
87
+ '@type': 'Service',
88
+ serviceType: 'OrganizationControllerService',
89
+ provider: { taxID: EXAMPLE_ORGANIZATION_TAX_ID },
90
+ owner: {
91
+ '@type': 'Person',
92
+ additionalType: EXAMPLE_REPRESENTATIVE_ROLE_CODE,
93
+ sameAs: EXAMPLE_REPRESENTATIVE_SAME_AS,
94
+ hasOccupation: {
95
+ '@type': 'Occupation',
96
+ occupationalCategory: `ISCO-08|${EXAMPLE_TECHNICAL_CONTROLLER_ISCO_CODE}`,
97
+ },
98
+ hasCredential: { material: EXAMPLE_ORG_CONTROLLER_SIGNING_KEY_ID },
99
+ },
100
+ },
101
+ });
79
102
  export const EXAMPLE_ORG_ACTIVATION_PROOF_VP_PAYLOAD = Object.freeze({
80
103
  iss: EXAMPLE_ORG_CONTROLLER_SIGNING_KEY_ID,
81
104
  sub: EXAMPLE_ORGANIZATION_TAX_ID,
@@ -87,6 +110,7 @@ export const EXAMPLE_ORG_ACTIVATION_PROOF_VP_PAYLOAD = Object.freeze({
87
110
  verifiableCredential: [
88
111
  JSON.stringify(EXAMPLE_ORG_ACTIVATION_ORGANIZATION_CREDENTIAL),
89
112
  JSON.stringify(EXAMPLE_ORG_ACTIVATION_LEGAL_REPRESENTATIVE_CREDENTIAL),
113
+ JSON.stringify(EXAMPLE_ORG_ACTIVATION_CONTROLLER_CREDENTIAL),
90
114
  ],
91
115
  },
92
116
  });
@@ -40,11 +40,10 @@ export interface IcaVerifyResponseExampleOutcome {
40
40
  * Contract notes:
41
41
  * - `body.data[0]` carries the organization credential plus optional generated
42
42
  * organization signing keypair
43
- * - `body.data[1]` carries the legal representative credential plus the
44
- * controller binding public key
45
- * - `credentialSubject.sameAs` expresses public identity continuity
46
- * - `credentialSubject.hasCredential.material` expresses controller
47
- * signing/binding key continuity as an RFC 9278 JWK-thumbprint URN
43
+ * - `body.data[1]` carries the legal representative credential and its ISCO
44
+ * occupation; legal representation alone does not grant tenant control
45
+ * - `body.data[2]` carries the independently issued organization-controller
46
+ * credential, including `RESPRSN`, controller ISCO occupation and JWK binding
48
47
  */
49
48
  export interface IcaVerifyTermsResponseExample {
50
49
  jti: string;
@@ -74,14 +73,19 @@ export declare const EXAMPLE_VERIFY_RESPONSE_PROOF_DATE: "2026-03-12T21:12:57.53
74
73
  export declare const EXAMPLE_VERIFY_RESPONSE_VERSION_ID: "zPdfVersionHash001";
75
74
  export declare const EXAMPLE_VERIFY_RESPONSE_ORG_VC_ID: "urn:uuid:org-vc-001";
76
75
  export declare const EXAMPLE_VERIFY_RESPONSE_PERSON_VC_ID: "urn:uuid:person-vc-001";
76
+ export declare const EXAMPLE_VERIFY_RESPONSE_CONTROLLER_VC_ID: "urn:uuid:controller-vc-001";
77
77
  export declare const EXAMPLE_VERIFY_RESPONSE_ORG_ENTRY_TYPE: "Organization-verification-v1.0";
78
78
  export declare const EXAMPLE_VERIFY_RESPONSE_PERSON_ENTRY_TYPE: "LegalRepresentative-verification-v1.0";
79
+ export declare const EXAMPLE_VERIFY_RESPONSE_CONTROLLER_ENTRY_TYPE: "ServiceController-verification-v1.0";
79
80
  export declare const EXAMPLE_VERIFY_RESPONSE_ORG_ATTACHMENT_ID: "vc-jwt-1";
80
81
  export declare const EXAMPLE_VERIFY_RESPONSE_PERSON_ATTACHMENT_ID: "vc-jwt-2";
82
+ export declare const EXAMPLE_VERIFY_RESPONSE_CONTROLLER_ATTACHMENT_ID: "vc-jwt-3";
81
83
  export declare const EXAMPLE_VERIFY_RESPONSE_ORG_ATTACHMENT_FILENAME: "Organization-verification-v1.0-1.jwt";
82
84
  export declare const EXAMPLE_VERIFY_RESPONSE_PERSON_ATTACHMENT_FILENAME: "LegalRepresentative-verification-v1.0-2.jwt";
85
+ export declare const EXAMPLE_VERIFY_RESPONSE_CONTROLLER_ATTACHMENT_FILENAME: "ServiceController-verification-v1.0-3.jwt";
83
86
  export declare const EXAMPLE_VERIFY_RESPONSE_ORG_JWT: "<vc-jwt-organization>";
84
87
  export declare const EXAMPLE_VERIFY_RESPONSE_PERSON_JWT: "<vc-jwt-legal-representative>";
88
+ export declare const EXAMPLE_VERIFY_RESPONSE_CONTROLLER_JWT: "<vc-jwt-organization-controller>";
85
89
  export declare const EXAMPLE_VERIFY_RESPONSE_MEDIA_TYPE: "application/vc+jwt";
86
90
  export declare const EXAMPLE_VERIFY_RESPONSE_ATTACHMENT_FORMAT: "vc+jwt";
87
91
  export declare const EXAMPLE_VERIFY_RESPONSE_STATUS_OK: "200";
@@ -98,19 +102,24 @@ export declare const EXAMPLE_VERIFY_RESPONSE_ORG_ADDITIONAL_TYPE: "sector=onehea
98
102
  export declare const EXAMPLE_VERIFY_RESPONSE_ADDRESS_TYPE: "PostalAddress";
99
103
  export declare const EXAMPLE_VERIFY_RESPONSE_ADDRESS_COUNTRY: "ES";
100
104
  export declare const EXAMPLE_VERIFY_RESPONSE_OCCUPATION_TYPE: "Occupation";
105
+ /** @deprecated Display-only legacy constant; no longer emitted in signed VCs. */
101
106
  export declare const EXAMPLE_VERIFY_RESPONSE_OCCUPATION_NAME: "LegalRepresentative";
102
- export declare const EXAMPLE_VERIFY_RESPONSE_OCCUPATION_IDENTIFIER: "RESPRSN";
107
+ export declare const EXAMPLE_VERIFY_RESPONSE_REPRESENTATIVE_ISCO: "1120";
108
+ export declare const EXAMPLE_VERIFY_RESPONSE_CONTROLLER_ISCO: "1330";
109
+ export declare const EXAMPLE_VERIFY_RESPONSE_CONTROLLER_ROLE: "RESPRSN";
103
110
  export declare const EXAMPLE_VERIFY_RESPONSE_PROOF_TYPE: "JsonWebSignature2020";
104
111
  export declare const EXAMPLE_VERIFY_RESPONSE_PROOF_PURPOSE: "assertionMethod";
105
112
  export declare const EXAMPLE_VERIFY_RESPONSE_PROOF_VERIFICATION_METHOD: "did:web:localhost%3A3310#verification-key-001";
106
113
  export declare const EXAMPLE_VERIFY_RESPONSE_ORG_PROOF_JWS: "<detached-jws-organization-truncated>";
107
114
  export declare const EXAMPLE_VERIFY_RESPONSE_PERSON_PROOF_JWS: "<detached-jws-person-truncated>";
115
+ export declare const EXAMPLE_VERIFY_RESPONSE_CONTROLLER_PROOF_JWS: "<detached-jws-controller-truncated>";
108
116
  /**
109
117
  * Shared success outcome reused at bundle level and item level.
110
118
  */
111
119
  export declare const EXAMPLE_VERIFY_RESPONSE_SUCCESS_OUTCOME: IcaVerifyResponseExampleOutcome;
112
120
  export declare const EXAMPLE_VERIFY_RESPONSE_ORG_ITEM_OUTCOME: IcaVerifyResponseExampleOutcome;
113
121
  export declare const EXAMPLE_VERIFY_RESPONSE_PERSON_ITEM_OUTCOME: IcaVerifyResponseExampleOutcome;
122
+ export declare const EXAMPLE_VERIFY_RESPONSE_CONTROLLER_ITEM_OUTCOME: IcaVerifyResponseExampleOutcome;
114
123
  export declare const EXAMPLE_VERIFY_RESPONSE_ORG_PUBLIC_KEY_JWK: Readonly<{
115
124
  kty: "EC";
116
125
  crv: "P-384";
@@ -196,8 +205,7 @@ export declare const EXAMPLE_VERIFY_RESPONSE_PERSON_CREDENTIAL: Readonly<{
196
205
  };
197
206
  hasOccupation: {
198
207
  '@type': "Occupation";
199
- name: "LegalRepresentative";
200
- identifier: "RESPRSN";
208
+ occupationalCategory: string;
201
209
  };
202
210
  memberOf: {
203
211
  '@type': "Organization";
@@ -216,8 +224,49 @@ export declare const EXAMPLE_VERIFY_RESPONSE_PERSON_CREDENTIAL: Readonly<{
216
224
  jws: "<detached-jws-person-truncated>";
217
225
  };
218
226
  }>;
227
+ export declare const EXAMPLE_VERIFY_RESPONSE_CONTROLLER_CREDENTIAL: Readonly<{
228
+ id: "urn:uuid:controller-vc-001";
229
+ '@context': ("https://www.w3.org/ns/credentials/v2" | "https://schema.org")[];
230
+ type: string[];
231
+ issuer: "did:web:ica.example.org";
232
+ validFrom: "2026-03-12T21:12:26.646Z";
233
+ meta: {
234
+ versionId: "zPdfVersionHash001";
235
+ };
236
+ credentialSubject: {
237
+ id: "did:web:globaldatacare.es:onehealth:organization:taxid:VATES-B00112233";
238
+ '@type': string;
239
+ serviceType: string;
240
+ provider: {
241
+ '@type': "Organization";
242
+ legalName: "ACME Health Provider";
243
+ taxID: "VATES-B00112233";
244
+ };
245
+ owner: {
246
+ '@type': "Person";
247
+ additionalType: "RESPRSN";
248
+ sameAs: "urn:multibase:zControllerHash";
249
+ hasOccupation: {
250
+ '@type': "Occupation";
251
+ occupationalCategory: string;
252
+ };
253
+ hasCredential: {
254
+ material: "urn:ietf:params:oauth:jwk-thumbprint:sha-256:Q0ZfM0V4YW1wbGVUaHVtYnByaW50X2Jhc2U2NHVybA";
255
+ };
256
+ };
257
+ };
258
+ evidence: never[];
259
+ proof: {
260
+ type: "JsonWebSignature2020";
261
+ created: "2026-03-12T21:12:57.534Z";
262
+ proofPurpose: "assertionMethod";
263
+ verificationMethod: "did:web:localhost%3A3310#verification-key-001";
264
+ jws: "<detached-jws-controller-truncated>";
265
+ };
266
+ }>;
219
267
  export declare const EXAMPLE_VERIFY_RESPONSE_ORG_ATTACHMENT: IcaVerifyResponseExampleAttachment;
220
268
  export declare const EXAMPLE_VERIFY_RESPONSE_PERSON_ATTACHMENT: IcaVerifyResponseExampleAttachment;
269
+ export declare const EXAMPLE_VERIFY_RESPONSE_CONTROLLER_ATTACHMENT: IcaVerifyResponseExampleAttachment;
221
270
  /**
222
271
  * Canonical `_verify-response` success example shared across ICA repos.
223
272
  */
@@ -19,14 +19,19 @@ export const EXAMPLE_VERIFY_RESPONSE_PROOF_DATE = '2026-03-12T21:12:57.534Z';
19
19
  export const EXAMPLE_VERIFY_RESPONSE_VERSION_ID = 'zPdfVersionHash001';
20
20
  export const EXAMPLE_VERIFY_RESPONSE_ORG_VC_ID = 'urn:uuid:org-vc-001';
21
21
  export const EXAMPLE_VERIFY_RESPONSE_PERSON_VC_ID = 'urn:uuid:person-vc-001';
22
+ export const EXAMPLE_VERIFY_RESPONSE_CONTROLLER_VC_ID = 'urn:uuid:controller-vc-001';
22
23
  export const EXAMPLE_VERIFY_RESPONSE_ORG_ENTRY_TYPE = 'Organization-verification-v1.0';
23
24
  export const EXAMPLE_VERIFY_RESPONSE_PERSON_ENTRY_TYPE = 'LegalRepresentative-verification-v1.0';
25
+ export const EXAMPLE_VERIFY_RESPONSE_CONTROLLER_ENTRY_TYPE = 'ServiceController-verification-v1.0';
24
26
  export const EXAMPLE_VERIFY_RESPONSE_ORG_ATTACHMENT_ID = 'vc-jwt-1';
25
27
  export const EXAMPLE_VERIFY_RESPONSE_PERSON_ATTACHMENT_ID = 'vc-jwt-2';
28
+ export const EXAMPLE_VERIFY_RESPONSE_CONTROLLER_ATTACHMENT_ID = 'vc-jwt-3';
26
29
  export const EXAMPLE_VERIFY_RESPONSE_ORG_ATTACHMENT_FILENAME = 'Organization-verification-v1.0-1.jwt';
27
30
  export const EXAMPLE_VERIFY_RESPONSE_PERSON_ATTACHMENT_FILENAME = 'LegalRepresentative-verification-v1.0-2.jwt';
31
+ export const EXAMPLE_VERIFY_RESPONSE_CONTROLLER_ATTACHMENT_FILENAME = 'ServiceController-verification-v1.0-3.jwt';
28
32
  export const EXAMPLE_VERIFY_RESPONSE_ORG_JWT = '<vc-jwt-organization>';
29
33
  export const EXAMPLE_VERIFY_RESPONSE_PERSON_JWT = '<vc-jwt-legal-representative>';
34
+ export const EXAMPLE_VERIFY_RESPONSE_CONTROLLER_JWT = '<vc-jwt-organization-controller>';
30
35
  export const EXAMPLE_VERIFY_RESPONSE_MEDIA_TYPE = 'application/vc+jwt';
31
36
  export const EXAMPLE_VERIFY_RESPONSE_ATTACHMENT_FORMAT = 'vc+jwt';
32
37
  export const EXAMPLE_VERIFY_RESPONSE_STATUS_OK = '200';
@@ -43,13 +48,17 @@ export const EXAMPLE_VERIFY_RESPONSE_ORG_ADDITIONAL_TYPE = 'sector=onehealth;sec
43
48
  export const EXAMPLE_VERIFY_RESPONSE_ADDRESS_TYPE = 'PostalAddress';
44
49
  export const EXAMPLE_VERIFY_RESPONSE_ADDRESS_COUNTRY = 'ES';
45
50
  export const EXAMPLE_VERIFY_RESPONSE_OCCUPATION_TYPE = 'Occupation';
51
+ /** @deprecated Display-only legacy constant; no longer emitted in signed VCs. */
46
52
  export const EXAMPLE_VERIFY_RESPONSE_OCCUPATION_NAME = 'LegalRepresentative';
47
- export const EXAMPLE_VERIFY_RESPONSE_OCCUPATION_IDENTIFIER = 'RESPRSN';
53
+ export const EXAMPLE_VERIFY_RESPONSE_REPRESENTATIVE_ISCO = '1120';
54
+ export const EXAMPLE_VERIFY_RESPONSE_CONTROLLER_ISCO = '1330';
55
+ export const EXAMPLE_VERIFY_RESPONSE_CONTROLLER_ROLE = 'RESPRSN';
48
56
  export const EXAMPLE_VERIFY_RESPONSE_PROOF_TYPE = 'JsonWebSignature2020';
49
57
  export const EXAMPLE_VERIFY_RESPONSE_PROOF_PURPOSE = 'assertionMethod';
50
58
  export const EXAMPLE_VERIFY_RESPONSE_PROOF_VERIFICATION_METHOD = 'did:web:localhost%3A3310#verification-key-001';
51
59
  export const EXAMPLE_VERIFY_RESPONSE_ORG_PROOF_JWS = '<detached-jws-organization-truncated>';
52
60
  export const EXAMPLE_VERIFY_RESPONSE_PERSON_PROOF_JWS = '<detached-jws-person-truncated>';
61
+ export const EXAMPLE_VERIFY_RESPONSE_CONTROLLER_PROOF_JWS = '<detached-jws-controller-truncated>';
53
62
  /**
54
63
  * Shared success outcome reused at bundle level and item level.
55
64
  */
@@ -83,6 +92,14 @@ export const EXAMPLE_VERIFY_RESPONSE_PERSON_ITEM_OUTCOME = {
83
92
  },
84
93
  ],
85
94
  };
95
+ export const EXAMPLE_VERIFY_RESPONSE_CONTROLLER_ITEM_OUTCOME = {
96
+ resourceType: EXAMPLE_VERIFY_RESPONSE_OPERATION_OUTCOME_RESOURCE_TYPE,
97
+ issue: [{
98
+ severity: IssueSeverity.Information,
99
+ code: IssueType.Informational,
100
+ diagnostics: 'Organization controller credential extracted from verified document and JWK binding.',
101
+ }],
102
+ };
86
103
  export const EXAMPLE_VERIFY_RESPONSE_ORG_PUBLIC_KEY_JWK = Object.freeze({
87
104
  kty: 'EC',
88
105
  crv: 'P-384',
@@ -167,8 +184,7 @@ export const EXAMPLE_VERIFY_RESPONSE_PERSON_CREDENTIAL = Object.freeze({
167
184
  },
168
185
  hasOccupation: {
169
186
  '@type': EXAMPLE_VERIFY_RESPONSE_OCCUPATION_TYPE,
170
- name: EXAMPLE_VERIFY_RESPONSE_OCCUPATION_NAME,
171
- identifier: EXAMPLE_VERIFY_RESPONSE_OCCUPATION_IDENTIFIER,
187
+ occupationalCategory: `ISCO-08|${EXAMPLE_VERIFY_RESPONSE_REPRESENTATIVE_ISCO}`,
172
188
  },
173
189
  memberOf: {
174
190
  '@type': EXAMPLE_VERIFY_RESPONSE_ORGANIZATION_TYPE,
@@ -187,6 +203,46 @@ export const EXAMPLE_VERIFY_RESPONSE_PERSON_CREDENTIAL = Object.freeze({
187
203
  jws: EXAMPLE_VERIFY_RESPONSE_PERSON_PROOF_JWS,
188
204
  },
189
205
  });
206
+ export const EXAMPLE_VERIFY_RESPONSE_CONTROLLER_CREDENTIAL = Object.freeze({
207
+ id: EXAMPLE_VERIFY_RESPONSE_CONTROLLER_VC_ID,
208
+ '@context': [W3cCredentialContexts.V2, EXAMPLE_VERIFY_RESPONSE_SCHEMA_ORG_CONTEXT],
209
+ type: [
210
+ W3cCredentialTypes.VerifiableCredential,
211
+ 'ServiceCredential',
212
+ ActivationCredentialTypes.ServiceControllerCredential,
213
+ ],
214
+ issuer: EXAMPLE_DEFAULT_ICA_DID,
215
+ validFrom: EXAMPLE_VERIFY_RESPONSE_DATE,
216
+ meta: { versionId: EXAMPLE_VERIFY_RESPONSE_VERSION_ID },
217
+ credentialSubject: {
218
+ id: EXAMPLE_VERIFY_RESPONSE_ORG_DID,
219
+ '@type': 'Service',
220
+ serviceType: 'OrganizationControllerService',
221
+ provider: {
222
+ '@type': EXAMPLE_VERIFY_RESPONSE_ORGANIZATION_TYPE,
223
+ legalName: EXAMPLE_PROVIDER_LEGAL_NAME,
224
+ taxID: EXAMPLE_PROVIDER_TAX_ID,
225
+ },
226
+ owner: {
227
+ '@type': EXAMPLE_VERIFY_RESPONSE_PERSON_TYPE,
228
+ additionalType: EXAMPLE_VERIFY_RESPONSE_CONTROLLER_ROLE,
229
+ sameAs: EXAMPLE_REPRESENTATIVE_SAME_AS,
230
+ hasOccupation: {
231
+ '@type': EXAMPLE_VERIFY_RESPONSE_OCCUPATION_TYPE,
232
+ occupationalCategory: `ISCO-08|${EXAMPLE_VERIFY_RESPONSE_CONTROLLER_ISCO}`,
233
+ },
234
+ hasCredential: { material: EXAMPLE_ORG_CONTROLLER_SIGNING_KEY_ID },
235
+ },
236
+ },
237
+ evidence: [],
238
+ proof: {
239
+ type: EXAMPLE_VERIFY_RESPONSE_PROOF_TYPE,
240
+ created: EXAMPLE_VERIFY_RESPONSE_PROOF_DATE,
241
+ proofPurpose: EXAMPLE_VERIFY_RESPONSE_PROOF_PURPOSE,
242
+ verificationMethod: EXAMPLE_VERIFY_RESPONSE_PROOF_VERIFICATION_METHOD,
243
+ jws: EXAMPLE_VERIFY_RESPONSE_CONTROLLER_PROOF_JWS,
244
+ },
245
+ });
190
246
  export const EXAMPLE_VERIFY_RESPONSE_ORG_ATTACHMENT = Object.freeze({
191
247
  id: EXAMPLE_VERIFY_RESPONSE_ORG_ATTACHMENT_ID,
192
248
  format: EXAMPLE_VERIFY_RESPONSE_ATTACHMENT_FORMAT,
@@ -211,6 +267,18 @@ export const EXAMPLE_VERIFY_RESPONSE_PERSON_ATTACHMENT = Object.freeze({
211
267
  },
212
268
  },
213
269
  });
270
+ export const EXAMPLE_VERIFY_RESPONSE_CONTROLLER_ATTACHMENT = Object.freeze({
271
+ id: EXAMPLE_VERIFY_RESPONSE_CONTROLLER_ATTACHMENT_ID,
272
+ format: EXAMPLE_VERIFY_RESPONSE_ATTACHMENT_FORMAT,
273
+ media_type: EXAMPLE_VERIFY_RESPONSE_MEDIA_TYPE,
274
+ filename: EXAMPLE_VERIFY_RESPONSE_CONTROLLER_ATTACHMENT_FILENAME,
275
+ data: {
276
+ json: {
277
+ format: EXAMPLE_VERIFY_RESPONSE_ATTACHMENT_FORMAT,
278
+ jwt: EXAMPLE_VERIFY_RESPONSE_CONTROLLER_JWT,
279
+ },
280
+ },
281
+ });
214
282
  /**
215
283
  * Canonical `_verify-response` success example shared across ICA repos.
216
284
  */
@@ -223,11 +291,12 @@ export const EXAMPLE_ICA_VERIFY_TERMS_RESPONSE_SUCCESS = {
223
291
  attachments: [
224
292
  EXAMPLE_VERIFY_RESPONSE_ORG_ATTACHMENT,
225
293
  EXAMPLE_VERIFY_RESPONSE_PERSON_ATTACHMENT,
294
+ EXAMPLE_VERIFY_RESPONSE_CONTROLLER_ATTACHMENT,
226
295
  ],
227
296
  body: {
228
297
  resourceType: EXAMPLE_BUNDLE_RESOURCE_TYPE,
229
298
  type: EXAMPLE_VERIFY_RESPONSE_BATCH_RESPONSE_TYPE,
230
- total: 2,
299
+ total: 3,
231
300
  issues: EXAMPLE_VERIFY_RESPONSE_SUCCESS_OUTCOME,
232
301
  data: [
233
302
  {
@@ -250,6 +319,15 @@ export const EXAMPLE_ICA_VERIFY_TERMS_RESPONSE_SUCCESS = {
250
319
  },
251
320
  resource: EXAMPLE_VERIFY_RESPONSE_PERSON_CREDENTIAL,
252
321
  },
322
+ {
323
+ type: EXAMPLE_VERIFY_RESPONSE_CONTROLLER_ENTRY_TYPE,
324
+ publicKeyJwk: EXAMPLE_VERIFY_RESPONSE_PERSON_PUBLIC_KEY_JWK,
325
+ response: {
326
+ status: EXAMPLE_VERIFY_RESPONSE_STATUS_OK,
327
+ outcome: EXAMPLE_VERIFY_RESPONSE_CONTROLLER_ITEM_OUTCOME,
328
+ },
329
+ resource: EXAMPLE_VERIFY_RESPONSE_CONTROLLER_CREDENTIAL,
330
+ },
253
331
  ],
254
332
  },
255
333
  };
@@ -18,6 +18,7 @@
18
18
  * field in this first inter-tenant fixture
19
19
  */
20
20
  export declare const EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_ID: "urn:uuid:inter-tenant-access-contract-001";
21
+ export declare const EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_TYPE: "https://example.org/fhir/CodeSystem/contract-type|data-sharing";
21
22
  export declare const EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_VALID_FROM: "2026-06-29T00:00:00.000Z";
22
23
  export declare const EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_VALID_UNTIL: "2027-06-29T00:00:00.000Z";
23
24
  export declare const EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_PURPOSE: "RESEARCH";
@@ -30,6 +31,7 @@ export declare const EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_SMART_SCOPE: "organiza
30
31
  export declare const EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_CLAIMS: Readonly<{
31
32
  readonly "Contract.identifier": "urn:uuid:inter-tenant-access-contract-001";
32
33
  readonly "Contract.status": "executed";
34
+ readonly "Contract.type": "https://example.org/fhir/CodeSystem/contract-type|data-sharing";
33
35
  readonly "Contract.issued": "2026-06-29T00:00:00.000Z";
34
36
  readonly "Contract.applies-start": "2026-06-29T00:00:00.000Z";
35
37
  readonly "Contract.applies-end": "2027-06-29T00:00:00.000Z";
@@ -27,6 +27,7 @@ import { EXAMPLE_API_ORGANIZATION_DID, EXAMPLE_CONTROLLER_DID, EXAMPLE_HEALTHCAR
27
27
  * field in this first inter-tenant fixture
28
28
  */
29
29
  export const EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_ID = 'urn:uuid:inter-tenant-access-contract-001';
30
+ export const EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_TYPE = 'https://example.org/fhir/CodeSystem/contract-type|data-sharing';
30
31
  export const EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_VALID_FROM = '2026-06-29T00:00:00.000Z';
31
32
  export const EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_VALID_UNTIL = '2027-06-29T00:00:00.000Z';
32
33
  export const EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_PURPOSE = 'RESEARCH';
@@ -39,6 +40,7 @@ export const EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_SMART_SCOPE = `${EXAMPLE_INTER
39
40
  export const EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_CLAIMS = Object.freeze({
40
41
  [ClaimInterTenantAccessContract.identifier]: EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_ID,
41
42
  [ClaimInterTenantAccessContract.status]: 'executed',
43
+ [ClaimInterTenantAccessContract.type]: EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_TYPE,
42
44
  [ClaimInterTenantAccessContract.issued]: EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_VALID_FROM,
43
45
  [ClaimInterTenantAccessContract.appliesStart]: EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_VALID_FROM,
44
46
  [ClaimInterTenantAccessContract.appliesEnd]: EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_VALID_UNTIL,
@@ -16,6 +16,12 @@ export declare enum ClaimInterTenantAccessContract {
16
16
  identifier = "Contract.identifier",
17
17
  /** FHIR Contract lifecycle status, for example `executed` or `amended`. */
18
18
  status = "Contract.status",
19
+ /**
20
+ * FHIR `Contract.type` CodeableConcept encoded as `system|code` in the flat
21
+ * claims transport. The terminology authority is supplied by the adopting
22
+ * governance domain rather than hardcoded in this product-neutral package.
23
+ */
24
+ type = "Contract.type",
19
25
  /** When the agreement was formally issued. */
20
26
  issued = "Contract.issued",
21
27
  /** Agreement validity start date/time. */
@@ -18,6 +18,12 @@ export var ClaimInterTenantAccessContract;
18
18
  ClaimInterTenantAccessContract["identifier"] = "Contract.identifier";
19
19
  /** FHIR Contract lifecycle status, for example `executed` or `amended`. */
20
20
  ClaimInterTenantAccessContract["status"] = "Contract.status";
21
+ /**
22
+ * FHIR `Contract.type` CodeableConcept encoded as `system|code` in the flat
23
+ * claims transport. The terminology authority is supplied by the adopting
24
+ * governance domain rather than hardcoded in this product-neutral package.
25
+ */
26
+ ClaimInterTenantAccessContract["type"] = "Contract.type";
21
27
  /** When the agreement was formally issued. */
22
28
  ClaimInterTenantAccessContract["issued"] = "Contract.issued";
23
29
  /** Agreement validity start date/time. */
@@ -5,6 +5,7 @@
5
5
  export declare enum Sector {
6
6
  TEST = "test",// For mock/demo endpoints and host registry in tests
7
7
  SYSTEM = "system",// Reserved for the host's bootstrap operation (TODO: deprecate)
8
+ ANTIFRAUD = "antifraud",
8
9
  HEALTH_CARE = "health-care",
9
10
  HEALTH_INSURANCE = "health-insurance",
10
11
  EMERGENCY = "emergency",
@@ -8,6 +8,7 @@ export var Sector;
8
8
  (function (Sector) {
9
9
  Sector["TEST"] = "test";
10
10
  Sector["SYSTEM"] = "system";
11
+ Sector["ANTIFRAUD"] = "antifraud";
11
12
  Sector["HEALTH_CARE"] = "health-care";
12
13
  Sector["HEALTH_INSURANCE"] = "health-insurance";
13
14
  Sector["EMERGENCY"] = "emergency";
@@ -1,4 +1,4 @@
1
- export type ActivationRepresentativePolicyErrorCode = 'MISSING_REPRESENTATIVE_SUBJECT_ID' | 'MISSING_REPRESENTATIVE_ROLE_RESPRSN' | 'MISSING_REPRESENTATIVE_CREDENTIAL_BINDING' | 'REPRESENTATIVE_TAXID_MISMATCH';
1
+ export type ActivationRepresentativePolicyErrorCode = 'MISSING_REPRESENTATIVE_SUBJECT_ID' | 'MISSING_REPRESENTATIVE_ROLE_RESPRSN' | 'MISSING_REPRESENTATIVE_CREDENTIAL_BINDING' | 'MISSING_CONTROLLER_ROLE_RESPRSN' | 'MISSING_CONTROLLER_CREDENTIAL_BINDING' | 'CONTROLLER_TAXID_MISMATCH' | 'REPRESENTATIVE_TAXID_MISMATCH';
2
2
  export type ActivationServiceAuthorizationPolicyErrorCode = 'MISSING_ORGANIZATION_SERVICE_CATEGORY' | 'MISSING_ORGANIZATION_SERVICE_TYPE' | 'UNAUTHORIZED_ORGANIZATION_SERVICE_CATEGORY' | 'UNAUTHORIZED_ORGANIZATION_SERVICE_TYPE';
3
3
  export type ActivationRepresentativePolicyError = {
4
4
  code: ActivationRepresentativePolicyErrorCode;
@@ -38,6 +38,20 @@ export declare function extractRepresentativeMemberOfTaxId(representativeCredent
38
38
  * @param representativeCredential Candidate representative credential.
39
39
  */
40
40
  export declare function extractRepresentativeRoleCode(representativeCredential: unknown): string | undefined;
41
+ /**
42
+ * Reads controller-authorization codes from an ICA-issued controller VC.
43
+ * Professional ISCO occupations are deliberately excluded.
44
+ */
45
+ export declare function extractOrganizationControllerRoleCodes(controllerCredential: unknown): string[];
46
+ /** Canonical name for reading controller authority from a service-controller VC. */
47
+ export declare const extractServiceControllerRoleCodes: typeof extractOrganizationControllerRoleCodes;
48
+ /**
49
+ * Reads professional ISCO occupations from an ICA-issued controller VC as
50
+ * canonical `ISCO-08|code` tokens.
51
+ */
52
+ export declare function extractOrganizationControllerOccupationCodes(controllerCredential: unknown): string[];
53
+ /** Canonical name for reading ISCO occupations from a service-controller VC. */
54
+ export declare const extractServiceControllerOccupationCodes: typeof extractOrganizationControllerOccupationCodes;
41
55
  /**
42
56
  * Checks whether a role code contains the required code, ignoring prefixes such as `SYSTEM|`.
43
57
  *
@@ -154,22 +168,30 @@ export declare function buildMemberDidWeb(ownerDidWeb: string, memberId: string,
154
168
  */
155
169
  export declare function isMemberDidWebUnderOwner(memberDidWeb: string, ownerDidWeb: string): boolean;
156
170
  /**
157
- * Validates the activation representative policy against organization and representative credentials.
171
+ * Validates controller authorization for organization activation.
158
172
  *
159
- * The representative proof model is intentionally two-dimensional:
160
- * - `credentialSubject.sameAs` expresses public identity continuity
161
- * - `credentialSubject.hasCredential.material` expresses signing-key continuity
173
+ * Canonical three-credential flow:
174
+ * - `LegalRepresentativeCredential` proves legal representation and carries
175
+ * the representative's professional ISCO occupation
176
+ * - `ServiceControllerCredential` independently carries `RESPRSN` in
177
+ * `owner.additionalType` and `owner.hasCredential.material` for the
178
+ * controller actor key
162
179
  *
163
- * For GW activation, the key-binding dimension is the hard requirement
164
- * enforced here. The public-identity dimension may still be used by higher
165
- * layers for stronger demo/production matching and auditability.
180
+ * Legacy two-credential compatibility is deliberately narrow. When no
181
+ * no service-controller credential exists, the representative credential is
182
+ * accepted as the controller proof only if that old credential itself carries
183
+ * both `RESPRSN` and `hasCredential` binding material. A modern representative
184
+ * credential containing only an ISCO occupation such as `ISCO-08|1120` is not
185
+ * promoted to controller and fails this policy.
166
186
  *
167
187
  * @param input.organizationCredential Candidate organization credential.
168
- * @param input.representativeCredential Candidate representative credential.
169
- * @param input.requiredRoleCode Required representative role, defaults to `RESPRSN`.
188
+ * @param input.representativeCredential Legal-representative credential.
189
+ * @param input.controllerCredential Canonical controller-authority credential.
190
+ * @param input.requiredRoleCode Required controller role, defaults to `RESPRSN`.
170
191
  */
171
192
  export declare function validateActivationRepresentativePolicy(input: {
172
193
  organizationCredential?: unknown;
173
194
  representativeCredential?: unknown;
195
+ controllerCredential?: unknown;
174
196
  requiredRoleCode?: string;
175
197
  }): ActivationRepresentativePolicyError[];
@@ -88,6 +88,91 @@ export function extractRepresentativeRoleCode(representativeCredential) {
88
88
  }
89
89
  return undefined;
90
90
  }
91
+ function readOccupationEntries(value) {
92
+ const values = Array.isArray(value) ? value : value ? [value] : [];
93
+ return values.map((entry) => {
94
+ if (typeof entry === 'string')
95
+ return { identifier: entry };
96
+ return asObject(entry);
97
+ }).filter((entry) => Boolean(entry));
98
+ }
99
+ function readCodedIdentifier(entry) {
100
+ const identifier = entry.identifier;
101
+ if (typeof identifier === 'string') {
102
+ const token = identifier.trim();
103
+ const separator = token.lastIndexOf('|');
104
+ return separator >= 0
105
+ ? { system: token.slice(0, separator), value: token.slice(separator + 1) }
106
+ : { value: token };
107
+ }
108
+ const coded = asObject(identifier) || {};
109
+ return {
110
+ system: String(coded.additionalType || coded.system || '').trim() || undefined,
111
+ value: String(coded.value || '').trim() || undefined,
112
+ };
113
+ }
114
+ /**
115
+ * Reads controller-authorization codes from an ICA-issued controller VC.
116
+ * Professional ISCO occupations are deliberately excluded.
117
+ */
118
+ export function extractOrganizationControllerRoleCodes(controllerCredential) {
119
+ const subject = extractCredentialSubject(controllerCredential) || {};
120
+ const owner = asObject(subject.owner) || {};
121
+ const canonical = (Array.isArray(owner.additionalType) ? owner.additionalType : [owner.additionalType])
122
+ .map((value) => String(value || '').trim())
123
+ .filter(Boolean);
124
+ if (canonical.length)
125
+ return canonical;
126
+ return readOccupationEntries(owner.hasOccupation)
127
+ .filter((entry) => String(entry['@type'] || '').toLowerCase() === 'role'
128
+ || readCodedIdentifier(entry).system?.toUpperCase() !== 'ISCO-08')
129
+ .map((entry) => readCodedIdentifier(entry).value)
130
+ .filter((value) => Boolean(value));
131
+ }
132
+ /** Canonical name for reading controller authority from a service-controller VC. */
133
+ export const extractServiceControllerRoleCodes = extractOrganizationControllerRoleCodes;
134
+ /**
135
+ * Reads professional ISCO occupations from an ICA-issued controller VC as
136
+ * canonical `ISCO-08|code` tokens.
137
+ */
138
+ export function extractOrganizationControllerOccupationCodes(controllerCredential) {
139
+ const subject = extractCredentialSubject(controllerCredential) || {};
140
+ const owner = asObject(subject.owner) || {};
141
+ const occupation = asObject(owner.hasOccupation);
142
+ const occupationalCategory = occupation?.occupationalCategory;
143
+ const canonical = (Array.isArray(occupationalCategory) ? occupationalCategory : [occupationalCategory])
144
+ .map((value) => {
145
+ if (typeof value === 'string')
146
+ return value.trim();
147
+ const category = asObject(value) || {};
148
+ const codeValue = String(category.codeValue || '').trim();
149
+ const codeSet = asObject(category.inCodeSet) || {};
150
+ const system = String(codeSet.name || '').trim();
151
+ return codeValue ? `${system || 'ISCO-08'}|${codeValue}` : '';
152
+ })
153
+ .filter(Boolean);
154
+ if (canonical.length)
155
+ return canonical;
156
+ return readOccupationEntries(owner.hasOccupation)
157
+ .map(readCodedIdentifier)
158
+ .filter((identifier) => identifier.system?.toUpperCase() === 'ISCO-08' && identifier.value)
159
+ .map((identifier) => `ISCO-08|${identifier.value}`);
160
+ }
161
+ /** Canonical name for reading ISCO occupations from a service-controller VC. */
162
+ export const extractServiceControllerOccupationCodes = extractOrganizationControllerOccupationCodes;
163
+ function extractOrganizationControllerBinding(controllerCredential) {
164
+ const subject = extractCredentialSubject(controllerCredential) || {};
165
+ const owner = asObject(subject.owner) || {};
166
+ return extractCredentialBindingValue(owner.hasCredential);
167
+ }
168
+ function extractOrganizationControllerProviderTaxId(controllerCredential) {
169
+ const subject = extractCredentialSubject(controllerCredential) || {};
170
+ const provider = asObject(subject.provider) || {};
171
+ const identifier = asObject(provider.identifier);
172
+ return normalizeTaxIdentifier(provider.taxID)
173
+ || normalizeTaxIdentifier(provider.taxId)
174
+ || normalizeTaxIdentifier(identifier?.value);
175
+ }
91
176
  /**
92
177
  * Checks whether a role code contains the required code, ignoring prefixes such as `SYSTEM|`.
93
178
  *
@@ -307,19 +392,26 @@ export function isMemberDidWebUnderOwner(memberDidWeb, ownerDidWeb) {
307
392
  return did.startsWith(`${owner}:member:`);
308
393
  }
309
394
  /**
310
- * Validates the activation representative policy against organization and representative credentials.
395
+ * Validates controller authorization for organization activation.
311
396
  *
312
- * The representative proof model is intentionally two-dimensional:
313
- * - `credentialSubject.sameAs` expresses public identity continuity
314
- * - `credentialSubject.hasCredential.material` expresses signing-key continuity
397
+ * Canonical three-credential flow:
398
+ * - `LegalRepresentativeCredential` proves legal representation and carries
399
+ * the representative's professional ISCO occupation
400
+ * - `ServiceControllerCredential` independently carries `RESPRSN` in
401
+ * `owner.additionalType` and `owner.hasCredential.material` for the
402
+ * controller actor key
315
403
  *
316
- * For GW activation, the key-binding dimension is the hard requirement
317
- * enforced here. The public-identity dimension may still be used by higher
318
- * layers for stronger demo/production matching and auditability.
404
+ * Legacy two-credential compatibility is deliberately narrow. When no
405
+ * no service-controller credential exists, the representative credential is
406
+ * accepted as the controller proof only if that old credential itself carries
407
+ * both `RESPRSN` and `hasCredential` binding material. A modern representative
408
+ * credential containing only an ISCO occupation such as `ISCO-08|1120` is not
409
+ * promoted to controller and fails this policy.
319
410
  *
320
411
  * @param input.organizationCredential Candidate organization credential.
321
- * @param input.representativeCredential Candidate representative credential.
322
- * @param input.requiredRoleCode Required representative role, defaults to `RESPRSN`.
412
+ * @param input.representativeCredential Legal-representative credential.
413
+ * @param input.controllerCredential Canonical controller-authority credential.
414
+ * @param input.requiredRoleCode Required controller role, defaults to `RESPRSN`.
323
415
  */
324
416
  export function validateActivationRepresentativePolicy(input) {
325
417
  const errors = [];
@@ -332,16 +424,41 @@ export function validateActivationRepresentativePolicy(input) {
332
424
  message: 'ICA-issued representative credential is missing credentialSubject.id.',
333
425
  });
334
426
  }
335
- if (!input.representativeCredential)
336
- return errors;
337
427
  const orgTax = extractOrganizationTaxId(input.organizationCredential);
338
- const repTax = extractRepresentativeMemberOfTaxId(input.representativeCredential);
339
- if (orgTax && repTax && orgTax !== repTax) {
340
- errors.push({
341
- code: 'REPRESENTATIVE_TAXID_MISMATCH',
342
- message: 'ICA-issued representative credential memberOf.taxID must match organization credential taxID.',
343
- });
428
+ if (input.representativeCredential) {
429
+ const repTax = extractRepresentativeMemberOfTaxId(input.representativeCredential);
430
+ if (orgTax && repTax && orgTax !== repTax) {
431
+ errors.push({
432
+ code: 'REPRESENTATIVE_TAXID_MISMATCH',
433
+ message: 'ICA-issued representative credential memberOf.taxID must match organization credential taxID.',
434
+ });
435
+ }
436
+ }
437
+ if (input.controllerCredential) {
438
+ const controllerTax = extractOrganizationControllerProviderTaxId(input.controllerCredential);
439
+ if (orgTax && controllerTax && orgTax !== controllerTax) {
440
+ errors.push({
441
+ code: 'CONTROLLER_TAXID_MISMATCH',
442
+ message: 'ICA-issued controller credential provider.taxID must match organization credential taxID.',
443
+ });
444
+ }
445
+ const controllerRoles = extractOrganizationControllerRoleCodes(input.controllerCredential);
446
+ if (!controllerRoles.some((role) => hasActivationRepresentativeRole(role, input.requiredRoleCode || 'RESPRSN'))) {
447
+ errors.push({
448
+ code: 'MISSING_CONTROLLER_ROLE_RESPRSN',
449
+ message: 'ICA-issued service controller credential must include RESPRSN in credentialSubject.owner.additionalType.',
450
+ });
451
+ }
452
+ if (!extractOrganizationControllerBinding(input.controllerCredential)) {
453
+ errors.push({
454
+ code: 'MISSING_CONTROLLER_CREDENTIAL_BINDING',
455
+ message: 'ICA-issued service controller credential is missing credentialSubject.owner.hasCredential binding data.',
456
+ });
457
+ }
458
+ return errors;
344
459
  }
460
+ if (!input.representativeCredential)
461
+ return errors;
345
462
  const roleCode = extractRepresentativeRoleCode(input.representativeCredential);
346
463
  if (!hasActivationRepresentativeRole(roleCode, input.requiredRoleCode || 'RESPRSN')) {
347
464
  errors.push({
@@ -15,6 +15,20 @@ function splitCsv(value) {
15
15
  .map((item) => item.trim())
16
16
  .filter(Boolean)));
17
17
  }
18
+ function buildContractTypeCodeableConcept(value) {
19
+ const normalized = String(value || '').trim();
20
+ if (!normalized)
21
+ return undefined;
22
+ const separator = normalized.indexOf('|');
23
+ if (separator < 1 || separator === normalized.length - 1) {
24
+ return { text: normalized };
25
+ }
26
+ const system = normalized.slice(0, separator).trim();
27
+ const code = normalized.slice(separator + 1).trim();
28
+ if (!system || !code)
29
+ return { text: normalized };
30
+ return { coding: [{ system, code }] };
31
+ }
18
32
  function readClaimWithAliases(claims, canonicalKey, aliases = []) {
19
33
  const direct = String(claims[canonicalKey] || '').trim();
20
34
  if (direct)
@@ -124,6 +138,7 @@ function normalizeNow(input) {
124
138
  export function buildInterTenantAccessContractResource(claims) {
125
139
  const identifier = readClaimWithAliases(claims, ClaimInterTenantAccessContract.identifier) || '';
126
140
  const status = readClaimWithAliases(claims, ClaimInterTenantAccessContract.status) || 'executed';
141
+ const type = buildContractTypeCodeableConcept(readClaimWithAliases(claims, ClaimInterTenantAccessContract.type));
127
142
  const issued = readClaimWithAliases(claims, ClaimInterTenantAccessContract.issued) || '';
128
143
  const appliesStart = readClaimWithAliases(claims, ClaimInterTenantAccessContract.appliesStart, ['Contract.applies.start']) || '';
129
144
  const appliesEnd = readClaimWithAliases(claims, ClaimInterTenantAccessContract.appliesEnd, ['Contract.applies.end']) || '';
@@ -139,6 +154,7 @@ export function buildInterTenantAccessContractResource(claims) {
139
154
  id: identifier || undefined,
140
155
  identifier: identifier ? [{ value: identifier }] : undefined,
141
156
  status,
157
+ type,
142
158
  issued: issued || undefined,
143
159
  instantiatesUri: instantiatesUri || undefined,
144
160
  applies: {
@@ -64,19 +64,27 @@ export declare function readLegalRepresentativeBindingFromResponseBody(responseB
64
64
  * and legal-representative credential pair.
65
65
  */
66
66
  export declare function readOrganizationControllerCredentialsFromResponseBody(responseBody: unknown): LegalOrganizationVerificationCredential[];
67
+ /** Canonical name for reading ICA-issued service-controller credentials. */
68
+ export declare const readServiceControllerCredentialsFromResponseBody: typeof readOrganizationControllerCredentialsFromResponseBody;
67
69
  /**
68
70
  * Returns one organization-controller service credential in an ICA or
69
71
  * projected GW verification response. Pass a stable actor `sameAs` to select
70
72
  * one controller independently; otherwise the first controller is returned.
71
73
  */
72
74
  export declare function readOrganizationControllerCredentialFromResponseBody(responseBody: unknown, controllerSameAs?: string): LegalOrganizationVerificationCredential | undefined;
75
+ /** Canonical name for selecting one ICA-issued service-controller credential. */
76
+ export declare const readServiceControllerCredentialFromResponseBody: typeof readOrganizationControllerCredentialFromResponseBody;
73
77
  /**
74
78
  * Reads `credentialSubject.owner.sameAs` from the first
75
79
  * organization-controller service credential when present.
76
80
  */
77
81
  export declare function readOrganizationControllerSameAsFromResponseBody(responseBody: unknown): string | undefined;
82
+ /** Canonical name for reading a service controller's stable actor identifier. */
83
+ export declare const readServiceControllerSameAsFromResponseBody: typeof readOrganizationControllerSameAsFromResponseBody;
78
84
  /**
79
85
  * Reads `credentialSubject.owner.hasCredential.material` from the first
80
86
  * organization-controller service credential when present.
81
87
  */
82
88
  export declare function readOrganizationControllerBindingFromResponseBody(responseBody: unknown): string | undefined;
89
+ /** Canonical name for reading a service controller's actor-key binding. */
90
+ export declare const readServiceControllerBindingFromResponseBody: typeof readOrganizationControllerBindingFromResponseBody;
@@ -152,10 +152,13 @@ export function readLegalRepresentativeBindingFromResponseBody(responseBody) {
152
152
  */
153
153
  export function readOrganizationControllerCredentialsFromResponseBody(responseBody) {
154
154
  return getLegalOrganizationVerificationEntriesFromResponseBody(responseBody)
155
- .filter((entry) => hasCredentialType(entry, 'OrganizationControllerCredential'))
155
+ .filter((entry) => hasCredentialType(entry, 'ServiceControllerCredential')
156
+ || hasCredentialType(entry, 'OrganizationControllerCredential'))
156
157
  .map((entry) => asObject(asObject(entry)?.resource) || asObject(entry))
157
158
  .filter((entry) => Boolean(entry));
158
159
  }
160
+ /** Canonical name for reading ICA-issued service-controller credentials. */
161
+ export const readServiceControllerCredentialsFromResponseBody = readOrganizationControllerCredentialsFromResponseBody;
159
162
  /**
160
163
  * Returns one organization-controller service credential in an ICA or
161
164
  * projected GW verification response. Pass a stable actor `sameAs` to select
@@ -168,6 +171,8 @@ export function readOrganizationControllerCredentialFromResponseBody(responseBod
168
171
  ? credentials.find((credential) => extractOrganizationControllerSameAs(credential) === expectedSameAs)
169
172
  : credentials[0];
170
173
  }
174
+ /** Canonical name for selecting one ICA-issued service-controller credential. */
175
+ export const readServiceControllerCredentialFromResponseBody = readOrganizationControllerCredentialFromResponseBody;
171
176
  /**
172
177
  * Reads `credentialSubject.owner.sameAs` from the first
173
178
  * organization-controller service credential when present.
@@ -175,6 +180,8 @@ export function readOrganizationControllerCredentialFromResponseBody(responseBod
175
180
  export function readOrganizationControllerSameAsFromResponseBody(responseBody) {
176
181
  return extractOrganizationControllerSameAs(readOrganizationControllerCredentialFromResponseBody(responseBody));
177
182
  }
183
+ /** Canonical name for reading a service controller's stable actor identifier. */
184
+ export const readServiceControllerSameAsFromResponseBody = readOrganizationControllerSameAsFromResponseBody;
178
185
  /**
179
186
  * Reads `credentialSubject.owner.hasCredential.material` from the first
180
187
  * organization-controller service credential when present.
@@ -182,3 +189,5 @@ export function readOrganizationControllerSameAsFromResponseBody(responseBody) {
182
189
  export function readOrganizationControllerBindingFromResponseBody(responseBody) {
183
190
  return extractOrganizationControllerBinding(readOrganizationControllerCredentialFromResponseBody(responseBody));
184
191
  }
192
+ /** Canonical name for reading a service controller's actor-key binding. */
193
+ export const readServiceControllerBindingFromResponseBody = readOrganizationControllerBindingFromResponseBody;
@@ -84,11 +84,16 @@ export type LegalOrganizationVerificationTransactionInput = Readonly<{
84
84
  }>;
85
85
  export type LegalOrganizationVerificationTransactionEntry = Readonly<{
86
86
  type?: string;
87
+ /** @deprecated Read-only compatibility with payloads emitted before 2.5.x. */
87
88
  meta?: {
88
89
  claims?: ClaimsRecord;
89
90
  [key: string]: unknown;
90
91
  };
91
92
  resource?: {
93
+ meta?: {
94
+ claims?: ClaimsRecord;
95
+ [key: string]: unknown;
96
+ };
92
97
  controller?: LegalOrganizationVerificationTransactionController;
93
98
  organization?: LegalOrganizationVerificationTransactionOrganization;
94
99
  /** @deprecated Legacy demo/OTP compatibility input. */
@@ -106,7 +111,7 @@ export type LegalOrganizationVerificationTransactionEntry = Readonly<{
106
111
  * verification transactions.
107
112
  *
108
113
  * Contract notes:
109
- * - business claims remain in `meta.claims`
114
+ * - business claims live in `resource.meta.claims`
110
115
  * - controller binding material remains in `resource.controller.*`
111
116
  * - organization signing material remains in `resource.organization.*`
112
117
  * - PDF evidence or URL attachments stay at the DIDComm-message level
@@ -31,7 +31,7 @@ function asRecord(value) {
31
31
  * verification transactions.
32
32
  *
33
33
  * Contract notes:
34
- * - business claims remain in `meta.claims`
34
+ * - business claims live in `resource.meta.claims`
35
35
  * - controller binding material remains in `resource.controller.*`
36
36
  * - organization signing material remains in `resource.organization.*`
37
37
  * - PDF evidence or URL attachments stay at the DIDComm-message level
@@ -49,10 +49,10 @@ export function buildLegalOrganizationVerificationTransactionBundle(input) {
49
49
  total: 1,
50
50
  data: [{
51
51
  type: LegalOrganizationVerificationTransactionEntryTypes.Request,
52
- meta: {
53
- claims: input.claims,
54
- },
55
52
  resource: {
53
+ meta: {
54
+ claims: input.claims,
55
+ },
56
56
  controller: input.controller,
57
57
  ...(input.organization ? { organization: input.organization } : {}),
58
58
  ...(input.legalRepresentativePayload
@@ -103,12 +103,14 @@ export declare function getOrganizationCredentialFromVpToken(vpToken: string): V
103
103
  */
104
104
  export declare function getLegalRepresentativeCredentialFromVpToken(vpToken: string): VpCredential | undefined;
105
105
  /**
106
- * Extracts the organization-controller service credential from a VP token
106
+ * Extracts the service-controller credential from a VP token
107
107
  * when present. It never substitutes a legal-representative credential.
108
108
  *
109
109
  * @param vpToken Compact VP token or raw JSON string.
110
110
  */
111
111
  export declare function getOrganizationControllerCredentialFromVpToken(vpToken: string): VpCredential | undefined;
112
+ /** Canonical name for `getOrganizationControllerCredentialFromVpToken`. */
113
+ export declare const getServiceControllerCredentialFromVpToken: typeof getOrganizationControllerCredentialFromVpToken;
112
114
  /**
113
115
  * Appends an organization activation credential after validating its VC type.
114
116
  *
@@ -129,6 +131,8 @@ export declare function addLegalRepresentativeCredential(vpPayload: VpTokenPaylo
129
131
  * Accepts compact VC strings, raw JSON VC strings, or VC JSON objects.
130
132
  */
131
133
  export declare function addOrganizationControllerCredential(vpPayload: VpTokenPayload, vc: VpCredentialInput): VpTokenPayload;
134
+ /** Canonical name for `addOrganizationControllerCredential`. */
135
+ export declare const addServiceControllerCredential: typeof addOrganizationControllerCredential;
132
136
  export declare function prepareForSignature(header: VpTokenHeader, payload: VpTokenPayload): {
133
137
  encodedHeader: string;
134
138
  encodedPayload: string;
@@ -190,7 +190,7 @@ export function getLegalRepresentativeCredentialFromVpToken(vpToken) {
190
190
  return getVpCredentialByAnyType(vpToken, [...REPRESENTATIVE_ACTIVATION_VC_TYPES]);
191
191
  }
192
192
  /**
193
- * Extracts the organization-controller service credential from a VP token
193
+ * Extracts the service-controller credential from a VP token
194
194
  * when present. It never substitutes a legal-representative credential.
195
195
  *
196
196
  * @param vpToken Compact VP token or raw JSON string.
@@ -198,6 +198,8 @@ export function getLegalRepresentativeCredentialFromVpToken(vpToken) {
198
198
  export function getOrganizationControllerCredentialFromVpToken(vpToken) {
199
199
  return getVpCredentialByAnyType(vpToken, [...ORGANIZATION_CONTROLLER_ACTIVATION_VC_TYPES]);
200
200
  }
201
+ /** Canonical name for `getOrganizationControllerCredentialFromVpToken`. */
202
+ export const getServiceControllerCredentialFromVpToken = getOrganizationControllerCredentialFromVpToken;
201
203
  function vcHasAnyType(vcPayload, acceptedTypes) {
202
204
  if (!vcPayload)
203
205
  return false;
@@ -238,8 +240,10 @@ export function addLegalRepresentativeCredential(vpPayload, vc) {
238
240
  * Accepts compact VC strings, raw JSON VC strings, or VC JSON objects.
239
241
  */
240
242
  export function addOrganizationControllerCredential(vpPayload, vc) {
241
- return addTypedVC(vpPayload, vc, [...ORGANIZATION_CONTROLLER_ACTIVATION_VC_TYPES], 'OrganizationController');
243
+ return addTypedVC(vpPayload, vc, [...ORGANIZATION_CONTROLLER_ACTIVATION_VC_TYPES], 'ServiceController');
242
244
  }
245
+ /** Canonical name for `addOrganizationControllerCredential`. */
246
+ export const addServiceControllerCredential = addOrganizationControllerCredential;
243
247
  export function prepareForSignature(header, payload) {
244
248
  return prepareJwtForSignature(header, payload);
245
249
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdc-common-utils-ts",
3
- "version": "2.5.2",
3
+ "version": "2.5.3",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },