gdc-common-utils-ts 2.4.0 → 2.5.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.
package/README.md CHANGED
@@ -243,11 +243,18 @@ const lookupMultihash = encodeMultibaseSha3('DL|US-CA|D1234567');
243
243
  const blockchainAssetId = buildRawCidV1FromUtf8String('DL|US-CA|D1234567');
244
244
  ```
245
245
 
246
- Individual lookup tokens keep the identifier type and jurisdiction separate:
247
- `org.hl7.terminology.CodeSystem.v2-0203.NN|ES|12345678Z`. Import
248
- `buildIndividualIdentifierLedgerAssetId(...)` when a GW must derive the opaque
249
- SHA3-384 `urn:multibase` key used by the subject-identifier ledger. The raw
250
- identifier must never be written to that ledger.
246
+ Canonical Subject-collection lookup tokens use `codingSystem|codeValue`, for
247
+ example `org.hl7.terminology.CodeSystem.v2-0203.NN.ES|12345678Z` or
248
+ `urn:iso:std:iso:11784-11785|981020000123456`. Import
249
+ `buildSubjectIdentifierAssetId(...)` when a GW must derive the opaque SHA3-384
250
+ `urn:multibase` key used by the distributed subject index. The semantic
251
+ `Person`/`Animal` entry keeps the private claims and its `sameAs` points to the
252
+ stable public unified card; the raw identifier must never be written to the
253
+ ledger.
254
+
255
+ `buildIndividualIdentifierLedgerAssetId(...)` remains the older
256
+ type/jurisdiction/value compatibility helper. Do not use its former
257
+ `Organization.sameAs` convention for new Subject collection writes.
251
258
 
252
259
  Both helpers hash the exact UTF-8 bytes supplied by the caller. Identifier,
253
260
  FHIR-token or JSON canonicalization belongs to the contract that owns the
@@ -175,6 +175,8 @@ export declare enum ClaimsPersonSchemaorg {
175
175
  identifier = "org.schema.Person.identifier",// the URN (composed by the provider)
176
176
  identifierType = "org.schema.Person.identifier.additionalType",// retrieved from a form
177
177
  identifierValue = "org.schema.Person.identifier.value",// retrieved from a form
178
+ /** Stable public unified-card URI shared by private identity representations. */
179
+ sameAs = "org.schema.Person.sameAs",
178
180
  /** ICAO transliteration of official given name (including middlenames), family name and addtional surname */
179
181
  name = "org.schema.Person.name",
180
182
  memberOf = "org.schema.Person.memberOf",// for employees
@@ -183,6 +183,8 @@ export var ClaimsPersonSchemaorg;
183
183
  ClaimsPersonSchemaorg["identifier"] = "org.schema.Person.identifier";
184
184
  ClaimsPersonSchemaorg["identifierType"] = "org.schema.Person.identifier.additionalType";
185
185
  ClaimsPersonSchemaorg["identifierValue"] = "org.schema.Person.identifier.value";
186
+ /** Stable public unified-card URI shared by private identity representations. */
187
+ ClaimsPersonSchemaorg["sameAs"] = "org.schema.Person.sameAs";
186
188
  /** ICAO transliteration of official given name (including middlenames), family name and addtional surname */
187
189
  ClaimsPersonSchemaorg["name"] = "org.schema.Person.name";
188
190
  ClaimsPersonSchemaorg["memberOf"] = "org.schema.Person.memberOf";
@@ -161,7 +161,11 @@ export interface DeviceLicense {
161
161
  maxDevices?: number;
162
162
  /** All DCR installations bound to this user/member seat. */
163
163
  deviceBindings?: DeviceBinding[];
164
- /** Identity-provider subject that first activated this seat. */
164
+ /**
165
+ * Stable private actor identifier:
166
+ * `urn:multibase:<hash(normalized email or phone)>:<professional|personal>`.
167
+ * Portal DIDs, IdP subjects, wallets and DCR clients bind to this value but do not define it.
168
+ */
165
169
  activatedBy?: string;
166
170
  /**
167
171
  * Optional, pre-defined restrictions on which devices are allowed to activate this license.
@@ -51,6 +51,7 @@ export * from './resource-document';
51
51
  export * from './relationship-access';
52
52
  export * from './response';
53
53
  export * from './subject-identifier-ledger';
54
+ export * from './subject-identity';
54
55
  export * from './subject-identity-binding';
55
56
  export * from './terminology';
56
57
  export * from './urlPath';
@@ -51,6 +51,7 @@ export * from './resource-document.js';
51
51
  export * from './relationship-access.js';
52
52
  export * from './response.js';
53
53
  export * from './subject-identifier-ledger.js';
54
+ export * from './subject-identity.js';
54
55
  export * from './subject-identity-binding.js';
55
56
  export * from './terminology.js';
56
57
  export * from './urlPath.js';
@@ -6,11 +6,22 @@ export type SubjectIdentifierProviderPointer = Readonly<{
6
6
  /** Bare DNS domain: no URL scheme, path, DID or tenant identifier. */
7
7
  url: string;
8
8
  }>;
9
- /** Ledger payload. It deliberately contains no subject DID or raw identifier. */
9
+ /** Public unified-card pointer resolved from an exact known identifier. */
10
+ export type SubjectIdentifierCardPointer = Readonly<{
11
+ identifier: Readonly<{
12
+ value: string;
13
+ }>;
14
+ subjectKind: 'person' | 'animal' | 'property';
15
+ }>;
16
+ /** Ledger payload. It deliberately contains no raw private identifier. */
10
17
  export type SubjectIdentifierLedgerPayload = Readonly<{
18
+ card: SubjectIdentifierCardPointer;
11
19
  provider: SubjectIdentifierProviderPointer;
12
20
  }>;
13
- /** One individual/animal bundle entry expanded into one write per `sameAs` alias. */
21
+ /**
22
+ * @deprecated Parse canonical Person/Animal/Place Bundle entries with
23
+ * `readSubjectIdentityBundleEntry` instead. Kept for 2.x compatibility only.
24
+ */
14
25
  export type SubjectIdentifierLedgerBundleEntry = Readonly<{
15
26
  sameAs: string | readonly string[];
16
27
  }>;
@@ -0,0 +1,36 @@
1
+ import type { BundleEntry } from './bundle';
2
+ /** Subject categories supported by the neutral identity collection. */
3
+ export type SubjectKind = 'person' | 'animal' | 'property';
4
+ /** Semantic resource types stored inside the neutral Subject collection. */
5
+ export type SubjectIdentityResourceType = 'Person' | 'Animal' | 'Place';
6
+ /** Exact private identifier input linked to one public unified card. */
7
+ export type SubjectIdentityInput = Readonly<{
8
+ subjectKind: SubjectKind;
9
+ cardId: string;
10
+ /** Canonical identifier type/coding system. */
11
+ codingSystem: string;
12
+ /** ISO jurisdiction for scoped identifiers; empty for a global identifier. */
13
+ jurisdiction: string;
14
+ codeValue: string;
15
+ /** Additional encrypted claims; canonical identifier and sameAs keys cannot be overridden. */
16
+ additionalClaims?: Readonly<Record<string, unknown>>;
17
+ }>;
18
+ /** Normalized association recovered from one Subject collection entry. */
19
+ export type SubjectIdentityAssociation = SubjectIdentityInput & Readonly<{
20
+ resourceType: SubjectIdentityResourceType;
21
+ assetId: string;
22
+ }>;
23
+ /** Bundle entry used to write one semantic identity into the Subject collection. */
24
+ export type SubjectIdentityBundleEntry = BundleEntry & Readonly<{
25
+ type: 'Subject-identity-link-v1.0';
26
+ resource: NonNullable<BundleEntry['resource']> & Readonly<{
27
+ resourceType: SubjectIdentityResourceType;
28
+ meta: {
29
+ claims: Record<string, unknown>;
30
+ };
31
+ }>;
32
+ request: {
33
+ method: 'POST';
34
+ url: 'Subject';
35
+ };
36
+ }>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,36 @@
1
+ /** Cross-portal actor classes. These are intentionally broader than job roles. */
2
+ export declare const StableActorRoles: Readonly<{
3
+ readonly Professional: "professional";
4
+ readonly Personal: "personal";
5
+ }>;
6
+ export type StableActorRole = typeof StableActorRoles[keyof typeof StableActorRoles];
7
+ export type StableActorContactKind = 'email' | 'phone';
8
+ /**
9
+ * Builds the private, stable actor identifier shared by independent portals.
10
+ *
11
+ * Wire format: `urn:multibase:<multibase(multihash(SHA3-256, normalized-contact))>:<role>`.
12
+ * Examples end in `:professional` or `:personal`; the clear email/phone is
13
+ * never embedded. A portal-specific `did:web`, IdP `sub`, wallet and DCR
14
+ * `client_id` are bindings to this identifier, not replacements for it.
15
+ *
16
+ * Email is trimmed, stripped of `mailto:` and lower-cased. Phone is stripped
17
+ * of `tel:` and formatting while preserving an E.164 leading `+`.
18
+ */
19
+ export declare function buildStableActorIdentifier(input: {
20
+ contactKind: StableActorContactKind;
21
+ contact: string;
22
+ role: StableActorRole;
23
+ }): string;
24
+ /**
25
+ * Builds a portal-specific DID binding that carries the stable actor payload.
26
+ * The portal root can change while the final actor payload remains identical.
27
+ */
28
+ export declare function buildPortalActorDidWeb(input: {
29
+ portalDidWeb: string;
30
+ actorIdentifier: string;
31
+ }): string;
32
+ /**
33
+ * Extracts the stable actor URN from a canonical GDC portal actor DID.
34
+ * This is extraction, not reversal of the contact hash.
35
+ */
36
+ export declare function stableActorIdentifierFromDidWeb(didWeb: string): string;
@@ -0,0 +1,58 @@
1
+ import { normalizePhone } from './consent.js';
2
+ import { multibase58MultihashSha3_256 } from './same-as.js';
3
+ /** Cross-portal actor classes. These are intentionally broader than job roles. */
4
+ export const StableActorRoles = Object.freeze({
5
+ Professional: 'professional',
6
+ Personal: 'personal',
7
+ });
8
+ /**
9
+ * Builds the private, stable actor identifier shared by independent portals.
10
+ *
11
+ * Wire format: `urn:multibase:<multibase(multihash(SHA3-256, normalized-contact))>:<role>`.
12
+ * Examples end in `:professional` or `:personal`; the clear email/phone is
13
+ * never embedded. A portal-specific `did:web`, IdP `sub`, wallet and DCR
14
+ * `client_id` are bindings to this identifier, not replacements for it.
15
+ *
16
+ * Email is trimmed, stripped of `mailto:` and lower-cased. Phone is stripped
17
+ * of `tel:` and formatting while preserving an E.164 leading `+`.
18
+ */
19
+ export function buildStableActorIdentifier(input) {
20
+ const role = String(input.role || '').trim();
21
+ if (!Object.values(StableActorRoles).includes(role)) {
22
+ throw new Error('Stable actor role must be professional or personal.');
23
+ }
24
+ const rawContact = String(input.contact || '').trim();
25
+ const normalizedContact = input.contactKind === 'email'
26
+ ? rawContact.replace(/^mailto:/i, '').replace(/\s+/g, '').toLowerCase()
27
+ : normalizePhone(rawContact.replace(/^tel:/i, ''));
28
+ if (!normalizedContact) {
29
+ throw new Error(`Stable actor ${input.contactKind} is required.`);
30
+ }
31
+ return `urn:multibase:${multibase58MultihashSha3_256(normalizedContact)}:${role}`;
32
+ }
33
+ /**
34
+ * Builds a portal-specific DID binding that carries the stable actor payload.
35
+ * The portal root can change while the final actor payload remains identical.
36
+ */
37
+ export function buildPortalActorDidWeb(input) {
38
+ const portalDidWeb = String(input.portalDidWeb || '').trim();
39
+ const match = /^urn:multibase:(z[^:]+):(professional|personal)$/.exec(String(input.actorIdentifier || '').trim());
40
+ if (!portalDidWeb.startsWith('did:web:')) {
41
+ throw new Error('Portal actor binding requires a did:web root.');
42
+ }
43
+ if (!match) {
44
+ throw new Error('Portal actor binding requires a stable actor URN.');
45
+ }
46
+ return `${portalDidWeb}:actor:multibase:${match[1]}:${match[2]}`;
47
+ }
48
+ /**
49
+ * Extracts the stable actor URN from a canonical GDC portal actor DID.
50
+ * This is extraction, not reversal of the contact hash.
51
+ */
52
+ export function stableActorIdentifierFromDidWeb(didWeb) {
53
+ const match = /:actor:multibase:(z[^:]+):(professional|personal)(?::|$)/.exec(String(didWeb || '').trim());
54
+ if (!match) {
55
+ throw new Error('DID does not contain a canonical GDC actor binding.');
56
+ }
57
+ return `urn:multibase:${match[1]}:${match[2]}`;
58
+ }
@@ -102,8 +102,10 @@ export * from './permission-templates';
102
102
  export * from './smart-scope';
103
103
  export * from './service-act-reasons';
104
104
  export * from './same-as';
105
+ export * from './subject-identity';
105
106
  export * from './subject-identity-binding';
106
107
  export * from './activation-request';
108
+ export * from './actor-identifier';
107
109
  export * from './vp-token';
108
110
  export * from './vital-sign-day-batch';
109
111
  export * from './wallet-mem';
@@ -102,8 +102,10 @@ export * from './permission-templates.js';
102
102
  export * from './smart-scope.js';
103
103
  export * from './service-act-reasons.js';
104
104
  export * from './same-as.js';
105
+ export * from './subject-identity.js';
105
106
  export * from './subject-identity-binding.js';
106
107
  export * from './activation-request.js';
108
+ export * from './actor-identifier.js';
107
109
  export * from './vp-token.js';
108
110
  export * from './vital-sign-day-batch.js';
109
111
  export * from './wallet-mem.js';
@@ -0,0 +1,30 @@
1
+ import type { BundleEntry } from '../models/bundle';
2
+ import type { SubjectIdentityAssociation, SubjectIdentityBundleEntry, SubjectIdentityInput } from '../models/subject-identity';
3
+ /**
4
+ * Builds the exact UTF-8 token used by the distributed Subject lookup.
5
+ *
6
+ * Contract:
7
+ * - canonical input is `codingSystem|jurisdiction-or-empty|codeValue`;
8
+ * - an empty jurisdiction is explicit, producing `||` for global identifiers;
9
+ * - values use the existing individual-identifier uppercase normalization;
10
+ * - `|` is forbidden in every component to keep the encoding unambiguous.
11
+ */
12
+ export declare function buildSubjectIdentifierToken(input: Pick<SubjectIdentityInput, 'codingSystem' | 'jurisdiction' | 'codeValue'>): string;
13
+ /**
14
+ * Returns `urn:multibase:<base58btc multihash>` using SHA3-384.
15
+ *
16
+ * The value is a deterministic public lookup key, not an authorization proof.
17
+ * The raw identifier remains in the encrypted Subject collection and is never
18
+ * included in the Fabric payload.
19
+ */
20
+ export declare function buildSubjectIdentifierAssetId(input: Pick<SubjectIdentityInput, 'codingSystem' | 'jurisdiction' | 'codeValue'>): string;
21
+ /**
22
+ * Builds one semantic identity resource for the neutral Subject collection.
23
+ *
24
+ * `sameAs` always identifies the stable public unified card. The private
25
+ * coding system and code value remain claims of Person, Animal or Place; the
26
+ * collection name never replaces that semantic resource type.
27
+ */
28
+ export declare function buildSubjectIdentityBundleEntry(input: SubjectIdentityInput): SubjectIdentityBundleEntry;
29
+ /** Reads and validates one Person/Animal/Place entry from the Subject collection. */
30
+ export declare function readSubjectIdentityBundleEntry(entry: BundleEntry): SubjectIdentityAssociation;
@@ -0,0 +1,127 @@
1
+ import { UrnPrefixes } from '../constants/urn.js';
2
+ import { encodeMultibaseSha3 } from './multibasehash.js';
3
+ import { normalizeIndividualIdentifierType } from './individual-identifier.js';
4
+ const SUBJECT_RESOURCE_BY_KIND = {
5
+ person: 'Person',
6
+ animal: 'Animal',
7
+ property: 'Place',
8
+ };
9
+ const SUBJECT_KIND_BY_RESOURCE = {
10
+ Person: 'person',
11
+ Animal: 'animal',
12
+ Place: 'property',
13
+ };
14
+ const STABLE_CARD_ID_PATTERN = /^(?:did|urn|https):\S+$/i;
15
+ const ISO_3166_JURISDICTION_PATTERN = /^[A-Z]{2}(?:-[A-Z0-9]{1,3})?$/;
16
+ /**
17
+ * Builds the exact UTF-8 token used by the distributed Subject lookup.
18
+ *
19
+ * Contract:
20
+ * - canonical input is `codingSystem|jurisdiction-or-empty|codeValue`;
21
+ * - an empty jurisdiction is explicit, producing `||` for global identifiers;
22
+ * - values use the existing individual-identifier uppercase normalization;
23
+ * - `|` is forbidden in every component to keep the encoding unambiguous.
24
+ */
25
+ export function buildSubjectIdentifierToken(input) {
26
+ const codingSystem = normalizeTokenPart(input.codingSystem, 'codingSystem');
27
+ const jurisdiction = normalizeJurisdiction(input.jurisdiction);
28
+ const codeValue = normalizeTokenPart(input.codeValue, 'codeValue').toUpperCase();
29
+ return `${codingSystem}|${jurisdiction}|${codeValue}`;
30
+ }
31
+ /**
32
+ * Returns `urn:multibase:<base58btc multihash>` using SHA3-384.
33
+ *
34
+ * The value is a deterministic public lookup key, not an authorization proof.
35
+ * The raw identifier remains in the encrypted Subject collection and is never
36
+ * included in the Fabric payload.
37
+ */
38
+ export function buildSubjectIdentifierAssetId(input) {
39
+ return `${UrnPrefixes.Multibase}${encodeMultibaseSha3(buildSubjectIdentifierToken(input), 384)}`;
40
+ }
41
+ /**
42
+ * Builds one semantic identity resource for the neutral Subject collection.
43
+ *
44
+ * `sameAs` always identifies the stable public unified card. The private
45
+ * coding system and code value remain claims of Person, Animal or Place; the
46
+ * collection name never replaces that semantic resource type.
47
+ */
48
+ export function buildSubjectIdentityBundleEntry(input) {
49
+ const resourceType = SUBJECT_RESOURCE_BY_KIND[input.subjectKind];
50
+ if (!resourceType)
51
+ throw new TypeError(`Unsupported subject kind: ${input.subjectKind}`);
52
+ const cardId = input.cardId.trim();
53
+ if (!STABLE_CARD_ID_PATTERN.test(cardId)) {
54
+ throw new TypeError('Subject identity cardId must be a stable URI (did:, urn: or https:).');
55
+ }
56
+ const codingSystem = input.subjectKind === 'person'
57
+ ? normalizeIndividualIdentifierType(input.codingSystem)
58
+ : normalizeTokenPart(input.codingSystem, 'codingSystem');
59
+ const jurisdiction = normalizeJurisdiction(input.jurisdiction);
60
+ if (input.subjectKind === 'person' && !jurisdiction) {
61
+ throw new TypeError('Person identity jurisdiction is required.');
62
+ }
63
+ const codeValue = normalizeTokenPart(input.codeValue, 'codeValue').toUpperCase();
64
+ const assetId = buildSubjectIdentifierAssetId({ codingSystem, jurisdiction, codeValue });
65
+ const claimPrefix = `org.schema.${resourceType}`;
66
+ return {
67
+ id: assetId,
68
+ fullUrl: assetId,
69
+ type: 'Subject-identity-link-v1.0',
70
+ resource: {
71
+ resourceType,
72
+ id: assetId,
73
+ meta: {
74
+ claims: {
75
+ ...(input.additionalClaims || {}),
76
+ [`${claimPrefix}.identifier`]: assetId,
77
+ [`${claimPrefix}.identifier.additionalType`]: codingSystem,
78
+ [`${claimPrefix}.identifier.jurisdiction`]: jurisdiction,
79
+ [`${claimPrefix}.identifier.value`]: codeValue,
80
+ [`${claimPrefix}.sameAs`]: cardId,
81
+ },
82
+ },
83
+ },
84
+ request: { method: 'POST', url: 'Subject' },
85
+ };
86
+ }
87
+ /** Reads and validates one Person/Animal/Place entry from the Subject collection. */
88
+ export function readSubjectIdentityBundleEntry(entry) {
89
+ const resourceType = String(entry.resource?.resourceType || '');
90
+ const subjectKind = SUBJECT_KIND_BY_RESOURCE[resourceType];
91
+ if (!subjectKind) {
92
+ throw new TypeError(`Unsupported Subject identity resourceType: ${resourceType || '(missing)'}; expected Person, Animal or Place.`);
93
+ }
94
+ const claims = entry.resource?.meta?.claims || {};
95
+ const prefix = `org.schema.${resourceType}`;
96
+ const cardId = String(claims[`${prefix}.sameAs`] || '').trim();
97
+ const codingSystem = String(claims[`${prefix}.identifier.additionalType`] || '').trim();
98
+ const jurisdiction = normalizeJurisdiction(String(claims[`${prefix}.identifier.jurisdiction`] || ''));
99
+ const codeValue = String(claims[`${prefix}.identifier.value`] || '').trim();
100
+ if (!STABLE_CARD_ID_PATTERN.test(cardId))
101
+ throw new TypeError('Subject identity sameAs must contain one stable card URI.');
102
+ if (subjectKind === 'person' && !jurisdiction)
103
+ throw new TypeError('Person identity jurisdiction is required.');
104
+ const assetId = buildSubjectIdentifierAssetId({ codingSystem, jurisdiction, codeValue });
105
+ const claimedAssetId = String(claims[`${prefix}.identifier`] || entry.resource?.id || entry.id || '').trim();
106
+ if (claimedAssetId && claimedAssetId !== assetId) {
107
+ throw new TypeError('Subject identity identifier does not match codingSystem|jurisdiction|codeValue.');
108
+ }
109
+ return { subjectKind, resourceType, cardId, codingSystem, jurisdiction, codeValue, assetId };
110
+ }
111
+ function normalizeTokenPart(value, field) {
112
+ const normalized = String(value || '').trim().normalize('NFKC');
113
+ if (!normalized)
114
+ throw new TypeError(`${field} is required`);
115
+ if (normalized.includes('|'))
116
+ throw new TypeError(`${field} must not contain the '|' delimiter`);
117
+ return normalized;
118
+ }
119
+ function normalizeJurisdiction(value) {
120
+ const normalized = String(value || '').trim().normalize('NFKC').toUpperCase();
121
+ if (normalized.includes('|'))
122
+ throw new TypeError("jurisdiction must not contain the '|' delimiter");
123
+ if (normalized && !ISO_3166_JURISDICTION_PATTERN.test(normalized)) {
124
+ throw new TypeError(`Invalid ISO 3166 jurisdiction: ${value}`);
125
+ }
126
+ return normalized;
127
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdc-common-utils-ts",
3
- "version": "2.4.0",
3
+ "version": "2.5.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },