From c6b03b0bed425fa0a0c423687cd8305d8a29e516 Mon Sep 17 00:00:00 2001 From: Priya Date: Wed, 29 Apr 2026 16:55:27 +0530 Subject: [PATCH 1/2] docs(calling): add ai-docs specifications for Contacts module Co-Authored-By: Claude Opus 4.6 (1M context) --- .../calling/src/Contacts/ai-docs/AGENTS.md | 212 +++++++++++++ .../src/Contacts/ai-docs/ARCHITECTURE.md | 291 ++++++++++++++++++ 2 files changed, 503 insertions(+) create mode 100644 packages/calling/src/Contacts/ai-docs/AGENTS.md create mode 100644 packages/calling/src/Contacts/ai-docs/ARCHITECTURE.md diff --git a/packages/calling/src/Contacts/ai-docs/AGENTS.md b/packages/calling/src/Contacts/ai-docs/AGENTS.md new file mode 100644 index 00000000000..09c4b73616f --- /dev/null +++ b/packages/calling/src/Contacts/ai-docs/AGENTS.md @@ -0,0 +1,212 @@ +# Contacts Module + +## AI Agent Routing Instructions + +**If you are an AI assistant or automated tool:** + +Do **not** use this file as your only entry point for reasoning or code generation. + +- **How to proceed:** + - For changes within the `Contacts/` directory, use this file as your primary reference. + - For encryption-related changes, review the `encryptedFields` enum in `constants.ts` and the encrypt/decrypt methods in `ContactsClient.ts`. + - For SCIM resolution of CLOUD contacts, review `resolveCloudContacts` and the `scimQuery` utility in `common/Utils.ts`. + - For common types (`Address`, `PhoneNumber`, `URIAddress`, `SCIMListResponse`), refer to `common/types.ts`. +- **Important:** Load this module-specific doc first, then drill into source files as needed. + +--- + +## Overview + +The `ContactsClient` module provides APIs for managing personal contacts and contact groups within the Webex ecosystem. It handles CRUD operations for contacts (both CUSTOM and CLOUD types), contact group management, and transparently encrypts/decrypts contact data using Webex KMS (Key Management Service). CLOUD contacts are resolved via SCIM (System for Cross-domain Identity Management) queries. + +**Package:** `@webex/calling` + +**Entry point:** `packages/calling/src/Contacts/ContactsClient.ts` + +**Factory:** `createContactsClient(webex, logger) -> IContacts` + +--- + +### Key Capabilities + +| Capability | Description | +| ----------- | ----------- | +| **Fetch Contacts & Groups** | Retrieves all contacts and contact groups for the user, decrypting CUSTOM contacts and resolving CLOUD contacts via SCIM in batches of 50. | +| **Create Contact** | Creates a new CUSTOM or CLOUD contact with transparent encryption. Auto-assigns to default group if none specified. | +| **Delete Contact** | Deletes a contact by contactId and removes it from the local cache. | +| **Create Contact Group** | Creates a new contact group with an encrypted display name. Auto-creates KMS Resource Object if no encryption key exists. Prevents duplicate group names. | +| **Delete Contact Group** | Deletes a contact group by groupId and removes it from the local cache. | +| **Encryption/Decryption** | Transparently encrypts and decrypts contact fields: `displayName`, `firstName`, `lastName`, `emails`, `phoneNumbers`, `sipAddresses`, `addressInfo`, `avatarURL`, `companyName`, `title`. | +| **CLOUD Contact Resolution** | Resolves CLOUD contacts via SCIM to fetch display names, phone numbers, SIP addresses, department, manager, and avatar information. Processes in batches of 50. | +| **Default Group Management** | Automatically creates a default "Other contacts" group when no groups exist. | + +--- + +## Public API + +### IContacts Interface + +| Method | Signature | Description | +| ------ | --------- | ----------- | +| `getContacts` | `(): Promise` | Fetch all contacts and groups | +| `createContact` | `(contactInfo: Contact): Promise` | Create a new contact | +| `deleteContact` | `(contactId: string): Promise` | Delete a contact | +| `createContactGroup` | `(displayName: string, encryptionKeyUrl?: string, groupType?: GroupType): Promise` | Create a contact group | +| `deleteContactGroup` | `(groupId: string): Promise` | Delete a contact group | +| `getSDKConnector` | `(): ISDKConnector` | Returns the SDK connector singleton | + +### Key Types + +#### ContactType Enum + +| Value | Description | +| ----- | ----------- | +| `CUSTOM` | User-created custom contact with encrypted fields | +| `CLOUD` | Cloud-based contact resolved via SCIM | + +#### GroupType Enum + +| Value | Description | +| ----- | ----------- | +| `NORMAL` | Standard contact group | +| `EXTERNAL` | External contact group | + +#### Contact + +```typescript +type Contact = { + addressInfo?: Address; + avatarURL?: string; + avatarUrlDomain?: string; + companyName?: string; + contactId: string; + contactType: ContactType; + department?: string; + displayName?: string; + emails?: URIAddress[]; + encryptionKeyUrl: string; + firstName?: string; + groups: string[]; + kmsResourceObjectUrl?: string; + lastName?: string; + manager?: string; + ownerId?: string; + phoneNumbers?: PhoneNumber[]; + primaryContactMethod?: string; + schemas?: string; + sipAddresses?: URIAddress[]; + resolved: boolean; +}; +``` + +#### ContactResponse + +```typescript +type ContactResponse = { + statusCode: number; + data: { + contacts?: Contact[]; + groups?: ContactGroup[]; + contact?: Contact; + group?: ContactGroup; + error?: string; + }; + message: string | null; +}; +``` + +--- + +## Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `webex` | `WebexSDK` | Yes | Initialized Webex SDK with access to `internal.encryption`, `internal.encryption.kms`, and `internal.services` | +| `logger` | `LoggerInterface` | Yes | Logger interface with a `level` property | + +The `contactsServiceUrl` is automatically resolved from `webex.internal.services._serviceUrls.contactsService`. + +--- + +## Examples and Use Cases + +### Create a ContactsClient + +```typescript +import {createContactsClient} from '@webex/calling'; + +const contactClient = createContactsClient(webex, {level: 'info'}); +``` + +### Fetch All Contacts and Groups + +```typescript +const response = await contactClient.getContacts(); +if (response.statusCode === 200) { + console.log('Contacts:', response.data.contacts); + console.log('Groups:', response.data.groups); +} +``` + +### Create a Custom Contact + +```typescript +const response = await contactClient.createContact({ + contactType: ContactType.CUSTOM, + displayName: 'Jane Doe', + firstName: 'Jane', + lastName: 'Doe', + emails: [{type: 'work', value: 'jane@example.com'}], + phoneNumbers: [{type: 'mobile', value: '+15551234567'}], +}); +``` + +### Create a Cloud Contact + +```typescript +const response = await contactClient.createContact({ + contactType: ContactType.CLOUD, + contactId: 'scim-user-uuid', +}); +``` + +### Delete a Contact + +```typescript +await contactClient.deleteContact('contact-uuid'); +``` + +### Create and Delete Contact Groups + +```typescript +const groupResponse = await contactClient.createContactGroup('Work Colleagues'); +const groupId = groupResponse.data.group?.groupId; + +await contactClient.deleteContactGroup(groupId); +``` + +--- + +## Dependencies + +### Runtime Dependencies + +| Package | Purpose | +| ------- | ------- | +| `webex` (SDK) | HTTP requests, KMS encryption/decryption, SCIM queries, service URL resolution | + +### Internal Dependencies + +| Module | Purpose | +| ------ | ------- | +| `SDKConnector` | Singleton bridge to Webex SDK | +| `Logger` | Structured logging with file/method context | +| `scimQuery` | Utility for querying SCIM to resolve CLOUD contacts | +| `serviceErrorCodeHandler` | Standardized error response formatting | +| `uploadLogs` | Uploads diagnostic logs on errors | + +--- + +## Related Documentation + +- [Architecture](./ARCHITECTURE.md) — Component overview, data flows, sequence diagrams diff --git a/packages/calling/src/Contacts/ai-docs/ARCHITECTURE.md b/packages/calling/src/Contacts/ai-docs/ARCHITECTURE.md new file mode 100644 index 00000000000..b777d0544f5 --- /dev/null +++ b/packages/calling/src/Contacts/ai-docs/ARCHITECTURE.md @@ -0,0 +1,291 @@ +# Contacts Module — Architecture + +## Component Overview + +The Contacts module manages encrypted personal contacts and groups via the contacts-service API, with CLOUD contact resolution through SCIM. Architecture: **Application -> ContactsClient -> Contacts Service / KMS / SCIM**. + +### Component Table + +| Layer | Component | File | Key Responsibilities | +|-------|-----------|------|---------------------| +| **Client** | `ContactsClient` | `ContactsClient.ts` | CRUD for contacts/groups, encryption/decryption, SCIM resolution, default group management | +| **SDK Bridge** | `SDKConnector` | `SDKConnector/` | Webex SDK access for HTTP requests and KMS | + +### Singletons and Factories + +| Component | Access Pattern | Lifecycle | +|-----------|---------------|-----------| +| `ContactsClient` | `createContactsClient(webex, logger)` factory | One per application | +| `SDKConnector` | Frozen singleton via `import SDKConnector` | Global, set once via `setWebex()` | + +### File Structure + +``` +Contacts/ +├── ContactsClient.ts # Main class with all public and private methods +├── ContactsClient.test.ts # Unit tests +├── types.ts # IContacts, Contact, ContactGroup, ContactResponse, enums +├── constants.ts # Endpoint filters, encrypted fields enum, SCIM constants +├── contactFixtures.ts # Test fixtures +└── ai-docs/ + ├── AGENTS.md # Module agent doc + └── ARCHITECTURE.md # This file +``` + +--- + +## Data Flows + +### Component Interaction Flow + +```mermaid +flowchart TB + subgraph Application + App[Application Code] + end + + subgraph ContactsModule + CC[ContactsClient] + end + + subgraph External + CS[Contacts Service API] + KMS[Webex KMS\nEncryption/Decryption] + SCIM[SCIM API\nCloud Contact Resolution] + end + + App -->|createContactsClient| CC + CC -->|getContacts / createContact / deleteContact| CS + CC -->|createContactGroup / deleteContactGroup| CS + CC -->|encryptText / decryptText| KMS + CC -->|createUnboundKeys / createResource| KMS + CC -->|scimQuery| SCIM +``` + +--- + +## Sequence Diagrams + +### 1. Fetching Contacts and Groups + +```mermaid +sequenceDiagram + participant App as Application + participant CC as ContactsClient + participant CS as Contacts Service + participant KMS as Webex KMS + participant SCIM as SCIM API + + App->>CC: getContacts() + activate CC + CC->>CS: GET /encrypt/Users/contacts + CS-->>CC: {contacts: [...], groups: [...]} + + par Decrypt CUSTOM contacts + loop Each CUSTOM contact + CC->>KMS: decryptText(encryptionKeyUrl, field) + KMS-->>CC: decrypted value + end + and Collect CLOUD contacts + CC->>CC: Build cloudContactsMap by contactId + end + + alt CLOUD contacts exist + loop Batches of 50 + CC->>SCIM: scimQuery('id eq "uuid1" or id eq "uuid2"...') + SCIM-->>CC: {Resources: [...]} + CC->>CC: resolveCloudContacts(map, scimResponse) + end + end + + par Decrypt group names + loop Each group + CC->>KMS: decryptText(encryptionKeyUrl, displayName) + KMS-->>CC: decrypted displayName + end + end + + CC-->>App: {statusCode, data: {contacts, groups}} + deactivate CC +``` + +### 2. Creating a CUSTOM Contact + +```mermaid +sequenceDiagram + participant App as Application + participant CC as ContactsClient + participant KMS as Webex KMS + participant CS as Contacts Service + + App->>CC: createContact({contactType: CUSTOM, ...}) + activate CC + + alt No encryptionKeyUrl + CC->>CC: fetchEncryptionKeyUrl() + alt No groups exist + CC->>KMS: createUnboundKeys({count: 1}) + KMS-->>CC: key URI + CC->>KMS: createResource({keyUris: [uri]}) + CC->>CS: POST /encrypt/Users/groups (create default group) + CS-->>CC: group created + end + end + + alt No groups assigned + CC->>CC: fetchDefaultGroup() + end + + CC->>KMS: encryptText(key, displayName/firstName/lastName/...) + KMS-->>CC: encrypted values + CC->>CS: POST /encrypt/Users/contacts + CS-->>CC: {contactId: 'new-uuid'} + + CC-->>App: {statusCode, data: {contact}} + deactivate CC +``` + +### 3. Creating a CLOUD Contact + +```mermaid +sequenceDiagram + participant App as Application + participant CC as ContactsClient + participant CS as Contacts Service + participant SCIM as SCIM API + + App->>CC: createContact({contactType: CLOUD, contactId: 'uuid'}) + activate CC + + alt No contactId + CC-->>App: {statusCode: 400, error: 'contactId is required'} + end + + CC->>CS: POST /encrypt/Users/contacts + CS-->>CC: {contactId: 'uuid'} + + CC->>SCIM: scimQuery('id eq "uuid"') + SCIM-->>CC: {Resources: [resolved contact]} + CC->>CC: resolveCloudContacts(map, scimResponse) + + CC-->>App: {statusCode, data: {contact}} + deactivate CC +``` + +### 4. Creating a Contact Group + +```mermaid +sequenceDiagram + participant App as Application + participant CC as ContactsClient + participant KMS as Webex KMS + participant CS as Contacts Service + + App->>CC: createContactGroup('Team Alpha') + activate CC + + CC->>CC: Check for duplicate group name + + alt Duplicate found + CC-->>App: {statusCode: 400, error: 'Group displayName already exists'} + end + + CC->>CC: fetchEncryptionKeyUrl() + CC->>KMS: encryptText(key, 'Team Alpha') + KMS-->>CC: encrypted displayName + + CC->>CS: POST /encrypt/Users/groups + CS-->>CC: {groupId: 'new-group-uuid', ...} + + CC-->>App: {statusCode, data: {group}} + deactivate CC +``` + +--- + +## Key Constants + +### API Path Segments + +| Constant | Value | Description | +|----------|-------|-------------| +| `ENCRYPT_FILTER` | `'encrypt'` | Encryption-aware API path segment | +| `USERS` | `'Users'` | Users path segment | +| `CONTACT_FILTER` | `'contacts'` | Contacts resource path | +| `GROUP_FILTER` | `'groups'` | Groups resource path | +| `DEFAULT_GROUP_NAME` | `'Other contacts'` | Name for auto-created default group | +| `CONTACTS_SCHEMA` | `'urn:cisco:codev:identity:contact:core:1.0'` | Schema for contact/group creation | + +### Encrypted Fields + +| Field | Constant | Description | +|-------|----------|-------------| +| `addressInfo` | `encryptedFields.ADDRESS_INFO` | Contact address (each sub-field encrypted) | +| `avatarURL` | `encryptedFields.AVATAR_URL` | Avatar URL | +| `companyName` | `encryptedFields.COMPANY` | Company name | +| `displayName` | `encryptedFields.DISPLAY_NAME` | Display name | +| `emails` | `encryptedFields.EMAILS` | Email addresses (each value encrypted) | +| `firstName` | `encryptedFields.FIRST_NAME` | First name | +| `lastName` | `encryptedFields.LAST_NAME` | Last name | +| `phoneNumbers` | `encryptedFields.PHONE_NUMBERS` | Phone numbers (each value encrypted) | +| `sipAddresses` | `encryptedFields.SIP_ADDRESSES` | SIP addresses (each value encrypted) | +| `title` | `encryptedFields.TITLE` | Contact title | + +### SCIM Constants + +| Constant | Value | Description | +|----------|-------|-------------| +| `SCIM_ID_FILTER` | `'id eq'` | SCIM filter prefix for ID queries | +| `OR` | `' or '` | SCIM filter OR operator | +| Max contacts per query | `50` | Batch size for SCIM resolution | + +--- + +## Troubleshooting Guide + +### 1. Contacts Return Empty + +**Symptoms:** `getContacts` returns empty contacts array + +**Possible Causes:** +- Contacts service URL not resolved +- No contacts exist for the user +- Decryption failures (KMS key issues) + +### 2. CLOUD Contacts Show `resolved: false` + +**Symptoms:** CLOUD contacts have no display name, phone numbers, etc. + +**Possible Causes:** +- SCIM query failed for that contact's ID +- Contact was deleted from the organization directory +- SCIM service unavailable (non-fatal; unresolved contacts are returned with `resolved: false`) + +### 3. Group Creation Fails with 400 + +**Symptoms:** `createContactGroup` returns `statusCode: 400` + +**Possible Causes:** +- Duplicate group name already exists +- KMS key creation failed + +### 4. Encryption/Decryption Errors + +**Symptoms:** Contact fields appear as encrypted ciphertext or operations fail + +**Possible Causes:** +- `encryptionKeyUrl` is invalid or expired +- KMS service unreachable +- Webex SDK encryption plugin not initialized + +### 5. Create Contact Returns 400 for CLOUD Type + +**Symptoms:** `createContact` with `contactType: CLOUD` returns error + +**Fix:** Ensure `contactId` is provided — it is required for CLOUD contacts. + +--- + +## Related Documentation + +- [AGENTS.md](./AGENTS.md) — Overview, examples, public API From d83f358eb27fe7c39ae96af97dee703be8d5c9d5 Mon Sep 17 00:00:00 2001 From: Priya Date: Wed, 6 May 2026 13:23:15 +0530 Subject: [PATCH 2/2] docs(calling): updates the .md files after self-review round 1 --- .../calling/src/Contacts/ai-docs/AGENTS.md | 78 ++++++++++++++++++- .../src/Contacts/ai-docs/ARCHITECTURE.md | 74 +++++++++++++++++- 2 files changed, 145 insertions(+), 7 deletions(-) diff --git a/packages/calling/src/Contacts/ai-docs/AGENTS.md b/packages/calling/src/Contacts/ai-docs/AGENTS.md index 09c4b73616f..cdca5211035 100644 --- a/packages/calling/src/Contacts/ai-docs/AGENTS.md +++ b/packages/calling/src/Contacts/ai-docs/AGENTS.md @@ -32,7 +32,7 @@ The `ContactsClient` module provides APIs for managing personal contacts and con | Capability | Description | | ----------- | ----------- | | **Fetch Contacts & Groups** | Retrieves all contacts and contact groups for the user, decrypting CUSTOM contacts and resolving CLOUD contacts via SCIM in batches of 50. | -| **Create Contact** | Creates a new CUSTOM or CLOUD contact with transparent encryption. Auto-assigns to default group if none specified. | +| **Create Contact** | Creates a new CUSTOM or CLOUD contact with transparent encryption (both types are encrypted). Auto-assigns to default group if none specified. CLOUD contacts are additionally resolved via SCIM after creation. | | **Delete Contact** | Deletes a contact by contactId and removes it from the local cache. | | **Create Contact Group** | Creates a new contact group with an encrypted display name. Auto-creates KMS Resource Object if no encryption key exists. Prevents duplicate group names. | | **Delete Contact Group** | Deletes a contact group by groupId and removes it from the local cache. | @@ -99,6 +99,19 @@ type Contact = { }; ``` +#### ContactGroup + +```typescript +type ContactGroup = { + displayName: string; + encryptionKeyUrl: string; + groupId: string; + groupType: GroupType; + members?: string[]; + ownerId?: string; +}; +``` + #### ContactResponse ```typescript @@ -187,13 +200,72 @@ await contactClient.deleteContactGroup(groupId); --- +## Implementation Notes + +### HTTP Client Usage + +All operations use `this.webex.request()` exclusively (no browser `fetch`). Auth is handled automatically by the SDK. + +### URL Patterns + +All API URLs follow the pattern: +``` +{contactsServiceUrl}/encrypt/Users/{resource}[/{id}] +``` + +| Operation | URL | Method | +| --------- | --- | ------ | +| Get contacts | `/encrypt/Users/contacts` | GET | +| Create contact | `/encrypt/Users/contacts` | POST | +| Delete contact | `/encrypt/Users/contacts/{contactId}` | DELETE | +| Create group | `/encrypt/Users/groups` | POST | +| Delete group | `/encrypt/Users/groups/{groupId}` | DELETE | + +Note: `USERS` constant is `'Users'` (capital U), not lowercase. + +### Encryption Applies to Both Contact Types + +Both `CUSTOM` and `CLOUD` contacts go through `encryptContact()` before being posted to the contacts service. The difference is: +- **CUSTOM**: Fully encrypted, then stored. Retrieved and decrypted locally. +- **CLOUD**: Encrypted and posted, then additionally resolved via SCIM to populate display details (`displayName`, `phoneNumbers`, `sipAddresses`, etc.). + +### Local Cache + +The client maintains in-memory caches: +- `this.contacts: Contact[]` — Updated on get/create/delete +- `this.groups: ContactGroup[]` — Updated on get/create/delete +- `this.encryptionKeyUrl: string` — Cached after first resolution +- `this.defaultGroupId: string` — Cached default group ID + +### Encryption Key Resolution Logic + +1. If `this.encryptionKeyUrl` is already cached, return it +2. If `this.groups` is undefined, call `getContacts()` to populate +3. If groups exist, use `groups[0].encryptionKeyUrl` +4. If no groups exist: + - Create unbound KMS key via `this.webex.internal.encryption.kms.createUnboundKeys({count: 1})` + - Create KMS resource via `this.webex.internal.encryption.kms.createResource({keyUris: [uri]})` + - Create default group named "Other contacts" + +### SCIM Query Format + +CLOUD contacts are resolved via SCIM with filter queries: +``` +id eq "uuid1" or id eq "uuid2" or id eq "uuid3"... +``` +Batched in groups of 50. Uses the `scimQuery` utility from `common/Utils.ts`. + +Resolved SCIM fields: `displayName`, `emails`, `phoneNumbers`, `photos` (avatar), `name.givenName`, `name.familyName`, `sipAddresses` (from `urn:scim:schemas:extension:cisco:webexidentity:2.0:User`), `manager`, `department` (from `urn:ietf:params:scim:schemas:extension:enterprise:2.0:User`). + +--- + ## Dependencies ### Runtime Dependencies | Package | Purpose | | ------- | ------- | -| `webex` (SDK) | HTTP requests, KMS encryption/decryption, SCIM queries, service URL resolution | +| `webex` (SDK) | HTTP requests via `webex.request()`, KMS encryption/decryption via `webex.internal.encryption`, SCIM queries, service URL resolution | ### Internal Dependencies @@ -201,7 +273,7 @@ await contactClient.deleteContactGroup(groupId); | ------ | ------- | | `SDKConnector` | Singleton bridge to Webex SDK | | `Logger` | Structured logging with file/method context | -| `scimQuery` | Utility for querying SCIM to resolve CLOUD contacts | +| `scimQuery` | Utility for querying SCIM to resolve CLOUD contacts (from `common/Utils.ts`) | | `serviceErrorCodeHandler` | Standardized error response formatting | | `uploadLogs` | Uploads diagnostic logs on errors | diff --git a/packages/calling/src/Contacts/ai-docs/ARCHITECTURE.md b/packages/calling/src/Contacts/ai-docs/ARCHITECTURE.md index b777d0544f5..11c32257da8 100644 --- a/packages/calling/src/Contacts/ai-docs/ARCHITECTURE.md +++ b/packages/calling/src/Contacts/ai-docs/ARCHITECTURE.md @@ -151,6 +151,7 @@ sequenceDiagram sequenceDiagram participant App as Application participant CC as ContactsClient + participant KMS as Webex KMS participant CS as Contacts Service participant SCIM as SCIM API @@ -158,13 +159,25 @@ sequenceDiagram activate CC alt No contactId - CC-->>App: {statusCode: 400, error: 'contactId is required'} + CC-->>App: {statusCode: 400, error: 'contactId is required for contactType:CLOUD.'} + end + + alt No encryptionKeyUrl + CC->>CC: fetchEncryptionKeyUrl() end + alt No groups assigned + CC->>CC: fetchDefaultGroup() + end + + CC->>KMS: encryptContact(contact) + Note over CC,KMS: CLOUD contacts are also encrypted before posting + KMS-->>CC: encrypted contact + CC->>CS: POST /encrypt/Users/contacts - CS-->>CC: {contactId: 'uuid'} + CS-->>CC: {contactId: 'new-uuid'} - CC->>SCIM: scimQuery('id eq "uuid"') + CC->>SCIM: scimQuery('id eq "new-uuid"') SCIM-->>CC: {Resources: [resolved contact]} CC->>CC: resolveCloudContacts(map, scimResponse) @@ -210,12 +223,24 @@ sequenceDiagram | Constant | Value | Description | |----------|-------|-------------| | `ENCRYPT_FILTER` | `'encrypt'` | Encryption-aware API path segment | -| `USERS` | `'Users'` | Users path segment | +| `USERS` | `'Users'` | Users path segment (capital U) | | `CONTACT_FILTER` | `'contacts'` | Contacts resource path | | `GROUP_FILTER` | `'groups'` | Groups resource path | | `DEFAULT_GROUP_NAME` | `'Other contacts'` | Name for auto-created default group | | `CONTACTS_SCHEMA` | `'urn:cisco:codev:identity:contact:core:1.0'` | Schema for contact/group creation | +### URL Patterns + +All operations use `this.webex.request()` (not browser `fetch`): + +``` +GET {contactsServiceUrl}/encrypt/Users/contacts — fetch all contacts & groups +POST {contactsServiceUrl}/encrypt/Users/contacts — create contact +DELETE {contactsServiceUrl}/encrypt/Users/contacts/{contactId} — delete contact +POST {contactsServiceUrl}/encrypt/Users/groups — create group +DELETE {contactsServiceUrl}/encrypt/Users/groups/{groupId} — delete group +``` + ### Encrypted Fields | Field | Constant | Description | @@ -241,6 +266,47 @@ sequenceDiagram --- +## Implementation Details + +### Local Cache Management + +The `ContactsClient` maintains in-memory state that is updated during CRUD operations: +- `this.contacts: Contact[]` — Full contact list (both CUSTOM and resolved CLOUD) +- `this.groups: ContactGroup[]` — All contact groups +- `this.encryptionKeyUrl: string` — Cached encryption key URL +- `this.defaultGroupId: string` — Cached default group ID + +On delete operations, the item is removed from the local cache by `findIndex` + `splice`. + +### Both Contact Types Are Encrypted + +The `encryptContact()` method is called for **both** `CUSTOM` and `CLOUD` contact types before posting to the contacts service. This is important: CLOUD contacts are stored encrypted server-side, then resolved via SCIM client-side for display purposes. + +### Encryption Key Resolution Order + +`fetchEncryptionKeyUrl()` follows this logic: +1. Return cached `this.encryptionKeyUrl` if available +2. If `this.groups` is undefined, trigger `getContacts()` to populate +3. If groups exist, return `groups[0].encryptionKeyUrl` +4. If no groups exist: create KMS keys → create default "Other contacts" group → return new key URL + +### SCIM Resolution Details + +Resolved SCIM fields mapped to Contact: +- `displayName` → `contact.displayName` +- `name.givenName` → `contact.firstName` +- `name.familyName` → `contact.lastName` +- `emails` → `contact.emails` +- `phoneNumbers` → `contact.phoneNumbers` +- `photos[0].value` → `contact.avatarURL` +- `urn:scim:schemas:extension:cisco:webexidentity:2.0:User.sipAddresses` → `contact.sipAddresses` +- `urn:ietf:params:scim:schemas:extension:enterprise:2.0:User.manager.displayName` → `contact.manager` +- `urn:ietf:params:scim:schemas:extension:enterprise:2.0:User.department` → `contact.department` + +Unresolved contacts (SCIM ID not found in response) are returned with `resolved: false`. + +--- + ## Troubleshooting Guide ### 1. Contacts Return Empty