gdc-common-utils-ts 2.3.2 → 2.3.4

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.
@@ -44,6 +44,14 @@ export declare const IndividualCredentialTypes: Readonly<{
44
44
  IndividualControllerCredential: "IndividualControllerCredential";
45
45
  IndividualMemberCredential: "IndividualMemberCredential";
46
46
  IndividualSubjectCredential: "IndividualSubjectCredential";
47
+ /**
48
+ * Credential issued by a trusted portal/identity authority to bind two or
49
+ * more DIDs that identify the same individual.
50
+ *
51
+ * A physical card/support DID is not an individual alias: clients resolve
52
+ * the support document's `subject` first and bind that subject DID instead.
53
+ */
54
+ SubjectIdentityBindingCredential: "SubjectIdentityBindingCredential";
47
55
  }>;
48
56
  /**
49
57
  * Canonical credential subtype names used by inter-tenant authorization
@@ -46,6 +46,14 @@ export const IndividualCredentialTypes = Object.freeze({
46
46
  IndividualControllerCredential: 'IndividualControllerCredential',
47
47
  IndividualMemberCredential: 'IndividualMemberCredential',
48
48
  IndividualSubjectCredential: 'IndividualSubjectCredential',
49
+ /**
50
+ * Credential issued by a trusted portal/identity authority to bind two or
51
+ * more DIDs that identify the same individual.
52
+ *
53
+ * A physical card/support DID is not an individual alias: clients resolve
54
+ * the support document's `subject` first and bind that subject DID instead.
55
+ */
56
+ SubjectIdentityBindingCredential: 'SubjectIdentityBindingCredential',
49
57
  });
50
58
  /**
51
59
  * Canonical credential subtype names used by inter-tenant authorization
@@ -22,11 +22,12 @@ export function allergyIntoleranceFlatToFhirR4(claims) {
22
22
  }
23
23
  export function allergyIntoleranceFhirR4ToFlat(resource) {
24
24
  const subject = resource.patient?.reference;
25
+ const code = resource.code;
25
26
  return {
26
27
  [AllergyIntoleranceClaim.Identifier]: resource.identifier?.[0]?.value,
27
28
  [AllergyIntoleranceClaim.Subject]: subject,
28
29
  [AllergyIntoleranceClaim.Patient]: subject,
29
- [AllergyIntoleranceClaim.Code]: codingToValue(resource.code?.coding?.[0]),
30
+ [AllergyIntoleranceClaim.Code]: codingToValue(code?.coding?.[0]) || code?.text,
30
31
  [AllergyIntoleranceClaim.ClinicalStatus]: resource.clinicalStatus?.coding?.[0]?.code,
31
32
  [AllergyIntoleranceClaim.VerificationStatus]: resource.verificationStatus?.coding?.[0]?.code,
32
33
  [AllergyIntoleranceClaim.Recorder]: resource.recorder?.reference,
@@ -14,10 +14,11 @@ export function conditionFlatToFhirR4(claims) {
14
14
  };
15
15
  }
16
16
  export function conditionFhirR4ToFlat(resource) {
17
+ const code = resource.code;
17
18
  return {
18
19
  [ConditionClaim.Identifier]: resource.identifier?.[0]?.value,
19
20
  [ConditionClaim.Subject]: resource.subject?.reference,
20
- [ConditionClaim.Code]: codingToValue(resource.code?.coding?.[0]),
21
+ [ConditionClaim.Code]: codingToValue(code?.coding?.[0]) || code?.text,
21
22
  [ConditionClaim.ClinicalStatus]: resource.clinicalStatus?.coding?.[0]?.code,
22
23
  [ConditionClaim.VerificationStatus]: resource.verificationStatus?.coding?.[0]?.code,
23
24
  };
@@ -57,7 +57,6 @@ export declare function buildGaiaXLegalPersonProjectionFromOrganizationCredentia
57
57
  credentialId?: string;
58
58
  issuerId: string;
59
59
  legalRegistrationNumberCredentialId: string;
60
- addressSubdivisionCode?: string;
61
60
  addressCountryCode?: string;
62
61
  validFrom: string;
63
62
  }>): Readonly<{
@@ -67,14 +67,8 @@ export function resolveSchemaOrgOrganizationRegistrationIdentifier(credentialSub
67
67
  * @see https://docs.gaia-x.eu/technical-committee/identity-credential-access-management/25.11/semantic_model/
68
68
  */
69
69
  export function buildGaiaXLegalPersonCredentialDraft(input) {
70
- const subdivision = optionalClaim(input.claims, ClaimsOrganizationSchemaorg.addressRegion);
71
- const country = optionalClaim(input.claims, ClaimsOrganizationSchemaorg.addressCountry);
72
- if (!subdivision && !country) {
73
- throw new Error('Missing organization ISO 3166-2 address subdivision or ISO 3166-1 country.');
74
- }
75
- const gaiaXAddress = subdivision
76
- ? { 'gx:countrySubdivisionCode': subdivision }
77
- : { 'gx:countryCode': country };
70
+ const country = requiredClaim(input.claims, ClaimsOrganizationSchemaorg.addressCountry, 'organization ISO 3166-1 country code').toUpperCase();
71
+ const gaiaXAddress = { 'gx:countryCode': country };
78
72
  const website = optionalClaim(input.claims, ClaimsOrganizationSchemaorg.url);
79
73
  return {
80
74
  '@context': [W3C_VC_V2_CONTEXT],
@@ -117,7 +111,6 @@ export function buildGaiaXLegalPersonProjectionFromOrganizationCredential(input)
117
111
  const claims = {
118
112
  [ClaimsOrganizationSchemaorg.legalName]: asString(subject.legalName || subject.name),
119
113
  [ClaimsOrganizationSchemaorg.url]: asString(subject.url),
120
- [ClaimsOrganizationSchemaorg.addressRegion]: input.addressSubdivisionCode || asString(address?.addressRegion),
121
114
  [ClaimsOrganizationSchemaorg.addressCountry]: input.addressCountryCode || asString(address?.addressCountry),
122
115
  [ClaimsOrganizationSchemaorg.identifierType]: registrationIdentifier.additionalType,
123
116
  [ClaimsOrganizationSchemaorg.identifierValue]: registrationIdentifier.value,
@@ -148,6 +141,10 @@ export function buildGaiaXServiceOfferingCredentialDraft(input) {
148
141
  const name = optionalClaim(input.claims, ClaimsServiceSchemaorg.name);
149
142
  const description = optionalClaim(input.claims, ClaimsServiceSchemaorg.description);
150
143
  const endpointUrl = optionalClaim(input.claims, ClaimsServiceSchemaorg.url);
144
+ const termsHash = requiredInput(input.termsAndConditionsHash, 'terms and conditions SHA-256 hash');
145
+ if (!/^[a-f0-9]{64}$/i.test(termsHash)) {
146
+ throw new Error('Gaia-X terms and conditions hash must be a 64-character SHA-256 hexadecimal digest of the published document bytes.');
147
+ }
151
148
  return {
152
149
  '@context': [W3C_VC_V2_CONTEXT],
153
150
  type: ['VerifiableCredential', 'ServiceOffering'],
@@ -160,7 +157,7 @@ export function buildGaiaXServiceOfferingCredentialDraft(input) {
160
157
  'gx:providedBy': { id: requiredInput(input.providedByCredentialId, 'providedBy credential id') },
161
158
  'gx:serviceOfferingTermsAndConditions': [{
162
159
  'gx:url': requiredInput(input.termsAndConditionsUrl, 'terms and conditions URL'),
163
- 'gx:hash': requiredInput(input.termsAndConditionsHash, 'terms and conditions hash'),
160
+ 'gx:hash': termsHash.toLowerCase(),
164
161
  }],
165
162
  ...(name ? { 'gx:name': name } : {}),
166
163
  ...(description ? { 'gx:description': description } : {}),
@@ -11,6 +11,7 @@ export * from './employee';
11
11
  export * from './license';
12
12
  export * from './invoice';
13
13
  export * from './inter-tenant-access-contract';
14
+ export * from './subject-identity-binding';
14
15
  export * from './related-person';
15
16
  export * from './consent-access';
16
17
  export * from './relationship-access';
@@ -11,6 +11,7 @@ export * from './employee.js';
11
11
  export * from './license.js';
12
12
  export * from './invoice.js';
13
13
  export * from './inter-tenant-access-contract.js';
14
+ export * from './subject-identity-binding.js';
14
15
  export * from './related-person.js';
15
16
  export * from './consent-access.js';
16
17
  export * from './relationship-access.js';
@@ -117,6 +117,8 @@ export declare const EXAMPLE_GATEWAY_PUBLIC_ORIGIN: "https://gateway.example.com
117
117
  export declare const EXAMPLE_HOST_PUBLIC_HOSTNAME: "host.example.com";
118
118
  export declare const EXAMPLE_PROVIDER_TAX_ID: "VATES-B00112233";
119
119
  export declare const EXAMPLE_PROVIDER_DOMAIN: "health-care.provider.example.org";
120
+ /** Synthetic public portal domain used to derive organization path DIDs. */
121
+ export declare const EXAMPLE_PUBLIC_PORTAL_DOMAIN: "portal.example.org";
120
122
  export declare const EXAMPLE_INDIVIDUAL_MULTIBASE_ID: string;
121
123
  export declare const EXAMPLE_INDIVIDUAL_MULTIBASE_ID_SECONDARY: string;
122
124
  export declare const EXAMPLE_INDIVIDUAL_MULTIBASE_ID_TERTIARY: string;
@@ -136,6 +136,8 @@ export const EXAMPLE_GATEWAY_PUBLIC_ORIGIN = 'https://gateway.example.com';
136
136
  export const EXAMPLE_HOST_PUBLIC_HOSTNAME = 'host.example.com';
137
137
  export const EXAMPLE_PROVIDER_TAX_ID = 'VATES-B00112233';
138
138
  export const EXAMPLE_PROVIDER_DOMAIN = 'health-care.provider.example.org';
139
+ /** Synthetic public portal domain used to derive organization path DIDs. */
140
+ export const EXAMPLE_PUBLIC_PORTAL_DOMAIN = 'portal.example.org';
139
141
  export const EXAMPLE_INDIVIDUAL_MULTIBASE_ID = encodeHexToMultibase58btc('a87e5b15aea444759c7c40aa88354b6f');
140
142
  export const EXAMPLE_INDIVIDUAL_MULTIBASE_ID_SECONDARY = encodeHexToMultibase58btc('b98f6c24bfb545849d8d51bb99465c7e');
141
143
  export const EXAMPLE_INDIVIDUAL_MULTIBASE_ID_TERTIARY = encodeHexToMultibase58btc('c39f7d35c0c65695ae9e62cca0576d8f');
@@ -0,0 +1,9 @@
1
+ /** Synthetic trusted portal DID used by binding examples and tests. */
2
+ export declare const EXAMPLE_TRUSTED_HEALTH_PORTAL_DID: "did:web:portal.example.org";
3
+ /** Synthetic individual DID exposed by one health portal. */
4
+ export declare const EXAMPLE_PORTAL_INDIVIDUAL_DID: "did:web:portal.example.org:health-care:individual:multibase:zSubjectExample";
5
+ /** Synthetic individual DID exposed by a second, independent portal. */
6
+ export declare const EXAMPLE_ALTERNATE_PORTAL_INDIVIDUAL_DID: "did:web:cards.example.org:individual:multibase:zSubjectExample";
7
+ /** Physical support/card DID used only to demonstrate that it is not an auth alias. */
8
+ export declare const EXAMPLE_PHYSICAL_SUPPORT_DID: "did:web:cards.example.org:card:personal:000-000-000-001";
9
+ export declare const EXAMPLE_SUBJECT_IDENTITY_BINDING_CREDENTIAL: Readonly<import("..").VerifiableCredentialV2>;
@@ -0,0 +1,20 @@
1
+ // Copyright 2026 Antifraud Services Inc. under the Apache License, Version 2.0.
2
+ import { DataspaceSectors } from '../constants/sectors.js';
3
+ import { buildSubjectIdentityBindingCredential } from '../utils/subject-identity-binding.js';
4
+ /** Synthetic trusted portal DID used by binding examples and tests. */
5
+ export const EXAMPLE_TRUSTED_HEALTH_PORTAL_DID = 'did:web:portal.example.org';
6
+ /** Synthetic individual DID exposed by one health portal. */
7
+ export const EXAMPLE_PORTAL_INDIVIDUAL_DID = 'did:web:portal.example.org:health-care:individual:multibase:zSubjectExample';
8
+ /** Synthetic individual DID exposed by a second, independent portal. */
9
+ export const EXAMPLE_ALTERNATE_PORTAL_INDIVIDUAL_DID = 'did:web:cards.example.org:individual:multibase:zSubjectExample';
10
+ /** Physical support/card DID used only to demonstrate that it is not an auth alias. */
11
+ export const EXAMPLE_PHYSICAL_SUPPORT_DID = 'did:web:cards.example.org:card:personal:000-000-000-001';
12
+ export const EXAMPLE_SUBJECT_IDENTITY_BINDING_CREDENTIAL = Object.freeze(buildSubjectIdentityBindingCredential({
13
+ id: 'urn:uuid:subject-identity-binding-001',
14
+ issuerDid: EXAMPLE_TRUSTED_HEALTH_PORTAL_DID,
15
+ subjectDid: EXAMPLE_PORTAL_INDIVIDUAL_DID,
16
+ aliasDids: [EXAMPLE_ALTERNATE_PORTAL_INDIVIDUAL_DID],
17
+ sectors: [DataspaceSectors.HealthCare],
18
+ validFrom: '2026-01-01T00:00:00.000Z',
19
+ validUntil: '2027-01-01T00:00:00.000Z',
20
+ }));
@@ -94,13 +94,9 @@ export interface GaiaXLegalPersonCredentialSubject extends JsonObject {
94
94
  id: string;
95
95
  };
96
96
  'gx:headquarterAddress': {
97
- 'gx:countrySubdivisionCode': string;
98
- } | {
99
97
  'gx:countryCode': string;
100
98
  };
101
99
  'gx:legalAddress': {
102
- 'gx:countrySubdivisionCode': string;
103
- } | {
104
100
  'gx:countryCode': string;
105
101
  };
106
102
  'gx:website'?: string;
@@ -48,6 +48,7 @@ export * from './resource-document';
48
48
  export * from './relationship-access';
49
49
  export * from './response';
50
50
  export * from './subject-identifier-ledger';
51
+ export * from './subject-identity-binding';
51
52
  export * from './urlPath';
52
53
  export * from './verifiable-credential';
53
54
  export * from './wallet';
@@ -48,6 +48,7 @@ export * from './resource-document.js';
48
48
  export * from './relationship-access.js';
49
49
  export * from './response.js';
50
50
  export * from './subject-identifier-ledger.js';
51
+ export * from './subject-identity-binding.js';
51
52
  export * from './urlPath.js';
52
53
  export * from './verifiable-credential.js';
53
54
  export * from './wallet.js';
@@ -0,0 +1,22 @@
1
+ /** Canonical JSON members of a subject identity binding credential. */
2
+ export declare const SubjectIdentityBindingClaims: Readonly<{
3
+ SubjectId: "id";
4
+ SameAs: "sameAs";
5
+ Sector: "sector";
6
+ }>;
7
+ /** Normalized, security-relevant projection of a subject identity binding VC. */
8
+ export type SubjectIdentityBindingSummary = Readonly<{
9
+ issuerDid: string;
10
+ subjectDid: string;
11
+ aliasDids: readonly string[];
12
+ sectors: readonly string[];
13
+ validFrom?: string;
14
+ validUntil?: string;
15
+ }>;
16
+ /** Criteria used by a verifier after the enclosing VP has been verified. */
17
+ export type SubjectIdentityBindingMatchCriteria = Readonly<{
18
+ trustedIssuerDids: readonly string[];
19
+ requiredSubjectDids: readonly string[];
20
+ sector?: string;
21
+ now?: string | Date;
22
+ }>;
@@ -0,0 +1,7 @@
1
+ // Copyright 2026 Antifraud Services Inc. under the Apache License, Version 2.0.
2
+ /** Canonical JSON members of a subject identity binding credential. */
3
+ export const SubjectIdentityBindingClaims = Object.freeze({
4
+ SubjectId: 'id',
5
+ SameAs: 'sameAs',
6
+ Sector: 'sector',
7
+ });
@@ -99,6 +99,7 @@ export * from './permission-templates';
99
99
  export * from './smart-scope';
100
100
  export * from './service-act-reasons';
101
101
  export * from './same-as';
102
+ export * from './subject-identity-binding';
102
103
  export * from './activation-request';
103
104
  export * from './vp-token';
104
105
  export * from './vital-sign-day-batch';
@@ -99,6 +99,7 @@ export * from './permission-templates.js';
99
99
  export * from './smart-scope.js';
100
100
  export * from './service-act-reasons.js';
101
101
  export * from './same-as.js';
102
+ export * from './subject-identity-binding.js';
102
103
  export * from './activation-request.js';
103
104
  export * from './vp-token.js';
104
105
  export * from './vital-sign-day-batch.js';
@@ -33,6 +33,19 @@ export type LegalOrganizationOnboardingDraftResult = Readonly<{
33
33
  export type LegalOrganizationGatewayVerificationSignatureFlow = 'certificate' | 'otp';
34
34
  export type LegalOrganizationGatewayVerificationRequestInput = Readonly<{
35
35
  controller: LegalOrganizationVerificationTransactionController;
36
+ /**
37
+ * Explicit public organization DID controlled by the registering portal.
38
+ * This is independent from the GW/service DID and from its network address.
39
+ */
40
+ publicOrganizationDid?: string;
41
+ /**
42
+ * Public portal domain used to derive the canonical path DID when
43
+ * `publicOrganizationDid` is omitted.
44
+ *
45
+ * Result:
46
+ * `did:web:<domain>:<sector>:organization:taxid:<tax-id>`
47
+ */
48
+ publicOrganizationDomain?: string;
36
49
  signatureFlow?: LegalOrganizationGatewayVerificationSignatureFlow;
37
50
  representativeSameAs?: string;
38
51
  verificationResourceType?: string;
@@ -1,5 +1,6 @@
1
1
  import { ClaimsOrganizationSchemaorg, ClaimsPersonSchemaorg, ClaimsServiceSchemaorg, } from '../constants/schemaorg.js';
2
2
  import { validateLegalOrganizationOnboardingClaims, } from './legal-organization-onboarding.js';
3
+ import { buildHostedProviderDidWeb } from './did.js';
3
4
  function normalizeText(value) {
4
5
  return typeof value === 'string' ? value.trim() : '';
5
6
  }
@@ -275,16 +276,26 @@ export function createLegalOrganizationOnboardingFacade() {
275
276
  },
276
277
  buildGatewayVerificationRequest(fields, input) {
277
278
  const controllerEmail = facade.getControllerEmail(fields);
278
- const serviceIdentifier = facade.getServiceIdentifier(fields);
279
279
  const serviceUrl = facade.getServiceUrl(fields);
280
+ const publicOrganizationDid = normalizeOptionalText(input.publicOrganizationDid);
281
+ const publicOrganizationDomain = normalizeOptionalText(input.publicOrganizationDomain);
282
+ const organizationDid = publicOrganizationDid || (publicOrganizationDomain
283
+ && facade.getServiceCategory(fields)
284
+ && facade.getTaxId(fields)
285
+ ? buildHostedProviderDidWeb({
286
+ hostDomain: publicOrganizationDomain,
287
+ sector: facade.getServiceCategory(fields),
288
+ providerTaxId: facade.getTaxId(fields),
289
+ })
290
+ : undefined);
280
291
  const signatureFlow = normalizeText(input.signatureFlow || 'certificate').toLowerCase();
281
292
  const representativeSameAs = normalizeOptionalText(input.representativeSameAs || controllerEmail);
282
293
  const signedTermsPdfUrl = normalizeOptionalText(input.signedTermsPdfUrl);
283
294
  return facade.buildVerificationTransactionInput(fields, {
284
295
  controller: input.controller,
285
- organization: serviceIdentifier || serviceUrl
296
+ organization: organizationDid || serviceUrl
286
297
  ? {
287
- ...(serviceIdentifier ? { did: serviceIdentifier } : {}),
298
+ ...(organizationDid ? { did: organizationDid } : {}),
288
299
  ...(serviceUrl ? { url: serviceUrl } : {}),
289
300
  }
290
301
  : undefined,
@@ -0,0 +1,28 @@
1
+ import { type SubjectIdentityBindingMatchCriteria, type SubjectIdentityBindingSummary } from '../models/subject-identity-binding';
2
+ import type { VerifiableCredentialV2 } from '../models/verifiable-credential';
3
+ /**
4
+ * Builds the unsigned JSON form of a subject identity binding VC.
5
+ *
6
+ * This helper only builds the canonical payload. The issuer must sign it using
7
+ * the normal VC/VP signing flow before a verifier treats it as evidence.
8
+ */
9
+ export declare function buildSubjectIdentityBindingCredential(input: Readonly<{
10
+ issuerDid: string;
11
+ subjectDid: string;
12
+ aliasDids: readonly string[];
13
+ sectors: readonly string[];
14
+ validFrom: string;
15
+ validUntil?: string;
16
+ id?: string;
17
+ }>): VerifiableCredentialV2;
18
+ /** Returns the normalized identity set asserted by one binding VC. */
19
+ export declare function summarizeSubjectIdentityBinding(credential: unknown): SubjectIdentityBindingSummary | undefined;
20
+ /** Checks issuer trust, validity, sector and exact DID membership. */
21
+ export declare function matchesSubjectIdentityBinding(summary: SubjectIdentityBindingSummary | undefined, criteria: SubjectIdentityBindingMatchCriteria): boolean;
22
+ /**
23
+ * Finds a matching binding credential in an already-verified VP token.
24
+ *
25
+ * This function decodes and matches claims; it does not verify signatures.
26
+ * Callers must first verify the enclosing VP/VC proof chain.
27
+ */
28
+ export declare function getMatchingSubjectIdentityBindingFromVpToken(vpToken: string, criteria: SubjectIdentityBindingMatchCriteria): SubjectIdentityBindingSummary | undefined;
@@ -0,0 +1,136 @@
1
+ // Copyright 2026 Antifraud Services Inc. under the Apache License, Version 2.0.
2
+ import { IndividualCredentialTypes, W3cCredentialContexts, W3cCredentialTypes, } from '../constants/verifiable-credentials.js';
3
+ import { SubjectIdentityBindingClaims, } from '../models/subject-identity-binding.js';
4
+ import { getVpCredentials } from './vp-token.js';
5
+ function uniqueStrings(value) {
6
+ const values = Array.isArray(value) ? value : value ? [value] : [];
7
+ return Array.from(new Set(values.map((item) => String(item || '').trim()).filter(Boolean)));
8
+ }
9
+ function isDidWeb(value) {
10
+ return /^did:web:[^:\s]+(?::[^:\s]+)*$/.test(value);
11
+ }
12
+ function isPhysicalSupportDid(value) {
13
+ const segments = value.toLowerCase().split(':');
14
+ return segments.includes('card') || segments.includes('petd');
15
+ }
16
+ function isIndividualIdentityDid(value) {
17
+ return isDidWeb(value) && !isPhysicalSupportDid(value);
18
+ }
19
+ function includesCredentialType(credential) {
20
+ const types = uniqueStrings(credential?.type);
21
+ return types.includes(IndividualCredentialTypes.SubjectIdentityBindingCredential);
22
+ }
23
+ function normalizeNow(input) {
24
+ if (input instanceof Date)
25
+ return input.getTime();
26
+ if (input)
27
+ return new Date(input).getTime();
28
+ return Date.now();
29
+ }
30
+ /**
31
+ * Builds the unsigned JSON form of a subject identity binding VC.
32
+ *
33
+ * This helper only builds the canonical payload. The issuer must sign it using
34
+ * the normal VC/VP signing flow before a verifier treats it as evidence.
35
+ */
36
+ export function buildSubjectIdentityBindingCredential(input) {
37
+ const issuerDid = String(input.issuerDid || '').trim();
38
+ const subjectDid = String(input.subjectDid || '').trim();
39
+ const aliasDids = uniqueStrings(input.aliasDids).filter((did) => did !== subjectDid);
40
+ const sectors = uniqueStrings(input.sectors);
41
+ if (!isDidWeb(issuerDid))
42
+ throw new Error('issuerDid must be a did:web identifier.');
43
+ if (!isIndividualIdentityDid(subjectDid)) {
44
+ throw new Error('subjectDid must be a did:web individual identifier, not a physical support DID.');
45
+ }
46
+ if (aliasDids.length === 0 || aliasDids.some((did) => !isIndividualIdentityDid(did))) {
47
+ throw new Error('aliasDids must contain at least one distinct did:web individual identifier and no physical support DID.');
48
+ }
49
+ if (sectors.length === 0)
50
+ throw new Error('sectors must contain at least one sector.');
51
+ if (!String(input.validFrom || '').trim() || Number.isNaN(Date.parse(input.validFrom))) {
52
+ throw new Error('validFrom must be an ISO date-time.');
53
+ }
54
+ if (input.validUntil && Number.isNaN(Date.parse(input.validUntil))) {
55
+ throw new Error('validUntil must be an ISO date-time.');
56
+ }
57
+ return {
58
+ '@context': [W3cCredentialContexts.V2],
59
+ ...(input.id ? { id: String(input.id).trim() } : {}),
60
+ type: [
61
+ W3cCredentialTypes.VerifiableCredential,
62
+ IndividualCredentialTypes.SubjectIdentityBindingCredential,
63
+ ],
64
+ issuer: issuerDid,
65
+ validFrom: String(input.validFrom).trim(),
66
+ ...(input.validUntil ? { validUntil: String(input.validUntil).trim() } : {}),
67
+ credentialSubject: {
68
+ [SubjectIdentityBindingClaims.SubjectId]: subjectDid,
69
+ [SubjectIdentityBindingClaims.SameAs]: aliasDids,
70
+ [SubjectIdentityBindingClaims.Sector]: sectors,
71
+ },
72
+ };
73
+ }
74
+ /** Returns the normalized identity set asserted by one binding VC. */
75
+ export function summarizeSubjectIdentityBinding(credential) {
76
+ if (!credential || typeof credential !== 'object' || !includesCredentialType(credential))
77
+ return undefined;
78
+ const source = credential;
79
+ const issuerDid = String(source.issuer?.id || source.issuer || '').trim();
80
+ const subject = source.credentialSubject || {};
81
+ const subjectDid = String(subject[SubjectIdentityBindingClaims.SubjectId] || '').trim();
82
+ const aliasDids = uniqueStrings(subject[SubjectIdentityBindingClaims.SameAs])
83
+ .filter((did) => did !== subjectDid);
84
+ const sectors = uniqueStrings(subject[SubjectIdentityBindingClaims.Sector]);
85
+ if (!isDidWeb(issuerDid) || !isIndividualIdentityDid(subjectDid) || aliasDids.length === 0)
86
+ return undefined;
87
+ if (aliasDids.some((did) => !isIndividualIdentityDid(did)) || sectors.length === 0)
88
+ return undefined;
89
+ return {
90
+ issuerDid,
91
+ subjectDid,
92
+ aliasDids,
93
+ sectors,
94
+ validFrom: String(source.validFrom || '').trim() || undefined,
95
+ validUntil: String(source.validUntil || '').trim() || undefined,
96
+ };
97
+ }
98
+ /** Checks issuer trust, validity, sector and exact DID membership. */
99
+ export function matchesSubjectIdentityBinding(summary, criteria) {
100
+ if (!summary)
101
+ return false;
102
+ if (!criteria.trustedIssuerDids.includes(summary.issuerDid))
103
+ return false;
104
+ const now = normalizeNow(criteria.now);
105
+ if (Number.isNaN(now))
106
+ return false;
107
+ if (summary.validFrom) {
108
+ const start = Date.parse(summary.validFrom);
109
+ if (Number.isNaN(start) || start > now)
110
+ return false;
111
+ }
112
+ if (summary.validUntil) {
113
+ const end = Date.parse(summary.validUntil);
114
+ if (Number.isNaN(end) || end < now)
115
+ return false;
116
+ }
117
+ if (criteria.sector && !summary.sectors.includes(criteria.sector))
118
+ return false;
119
+ const identities = new Set([summary.subjectDid, ...summary.aliasDids]);
120
+ const required = uniqueStrings(criteria.requiredSubjectDids);
121
+ return required.length > 0 && required.every((did) => identities.has(did));
122
+ }
123
+ /**
124
+ * Finds a matching binding credential in an already-verified VP token.
125
+ *
126
+ * This function decodes and matches claims; it does not verify signatures.
127
+ * Callers must first verify the enclosing VP/VC proof chain.
128
+ */
129
+ export function getMatchingSubjectIdentityBindingFromVpToken(vpToken, criteria) {
130
+ for (const credential of getVpCredentials(vpToken)) {
131
+ const summary = summarizeSubjectIdentityBinding(credential);
132
+ if (matchesSubjectIdentityBinding(summary, criteria))
133
+ return summary;
134
+ }
135
+ return undefined;
136
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdc-common-utils-ts",
3
- "version": "2.3.2",
3
+ "version": "2.3.4",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },