From 777268878623fb8253eb549ce1d91a5a79f11731 Mon Sep 17 00:00:00 2001 From: Jake Richter Date: Tue, 23 Jun 2026 22:32:11 -0400 Subject: [PATCH 1/2] chore: release 0.3.3 --- .husky/commit-msg | 0 .husky/pre-commit | 0 CHANGELOG.md | 12 + README.md | 94 +- messages/jawn.user.provision.md | 50 +- package.json | 2 +- src/commands/jawn/user/provision.ts | 97 +- src/userProvisioning/planner.ts | 301 +++++- test/commands/jawn/user/provision.test.ts | 1022 ++++++++++++++++++++- test/fixtures/user-def.json | 6 +- test/userProvisioning/planner.test.ts | 350 ++++++- yarn.lock | 661 +++++-------- 12 files changed, 2053 insertions(+), 542 deletions(-) mode change 100644 => 100755 .husky/commit-msg mode change 100644 => 100755 .husky/pre-commit diff --git a/.husky/commit-msg b/.husky/commit-msg old mode 100644 new mode 100755 diff --git a/.husky/pre-commit b/.husky/pre-commit old mode 100644 new mode 100755 diff --git a/CHANGELOG.md b/CHANGELOG.md index 32ba3c3..fa75591 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0/). +## [0.3.3] - 2026-06-23 + +### Added + +- Multiple personas and default configurations + +## [0.3.2] - 2026-06-02 + +### Changed + +- Internal maintenance and tooling updates + ## [0.3.1] - 2026-06-02 ### Changed diff --git a/README.md b/README.md index 0bb49e7..6e6b7c6 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ FLAG DESCRIPTIONS Override the api version used for api requests made by this command ``` -_See code: [src/commands/jawn/aep/generate.ts](https://github.com/Syntax-Syllogism/jawn/blob/v0.3.1/src/commands/jawn/aep/generate.ts)_ +_See code: [src/commands/jawn/aep/generate.ts](https://github.com/Syntax-Syllogism/jawn/blob/v0.3.3/src/commands/jawn/aep/generate.ts)_ ## `sf jawn aep generate action` @@ -158,7 +158,7 @@ FLAG DESCRIPTIONS Override the api version used for api requests made by this command ``` -_See code: [src/commands/jawn/aep/generate/action.ts](https://github.com/Syntax-Syllogism/jawn/blob/v0.3.1/src/commands/jawn/aep/generate/action.ts)_ +_See code: [src/commands/jawn/aep/generate/action.ts](https://github.com/Syntax-Syllogism/jawn/blob/v0.3.3/src/commands/jawn/aep/generate/action.ts)_ ## `sf jawn aep generate criteria` @@ -203,7 +203,7 @@ FLAG DESCRIPTIONS Override the api version used for api requests made by this command ``` -_See code: [src/commands/jawn/aep/generate/criteria.ts](https://github.com/Syntax-Syllogism/jawn/blob/v0.3.1/src/commands/jawn/aep/generate/criteria.ts)_ +_See code: [src/commands/jawn/aep/generate/criteria.ts](https://github.com/Syntax-Syllogism/jawn/blob/v0.3.3/src/commands/jawn/aep/generate/criteria.ts)_ ## `sf jawn aep generate domain` @@ -244,7 +244,7 @@ FLAG DESCRIPTIONS Override the api version used for api requests made by this command ``` -_See code: [src/commands/jawn/aep/generate/domain.ts](https://github.com/Syntax-Syllogism/jawn/blob/v0.3.1/src/commands/jawn/aep/generate/domain.ts)_ +_See code: [src/commands/jawn/aep/generate/domain.ts](https://github.com/Syntax-Syllogism/jawn/blob/v0.3.3/src/commands/jawn/aep/generate/domain.ts)_ ## `sf jawn aep generate selector` @@ -289,7 +289,7 @@ FLAG DESCRIPTIONS Override the api version used for api requests made by this command ``` -_See code: [src/commands/jawn/aep/generate/selector.ts](https://github.com/Syntax-Syllogism/jawn/blob/v0.3.1/src/commands/jawn/aep/generate/selector.ts)_ +_See code: [src/commands/jawn/aep/generate/selector.ts](https://github.com/Syntax-Syllogism/jawn/blob/v0.3.3/src/commands/jawn/aep/generate/selector.ts)_ ## `sf jawn aep generate selector field-injection` @@ -324,7 +324,7 @@ EXAMPLES $ sf jawn aep generate selector field-injection -s Account --fields Name,Industry ``` -_See code: [src/commands/jawn/aep/generate/selector/field-injection.ts](https://github.com/Syntax-Syllogism/jawn/blob/v0.3.1/src/commands/jawn/aep/generate/selector/field-injection.ts)_ +_See code: [src/commands/jawn/aep/generate/selector/field-injection.ts](https://github.com/Syntax-Syllogism/jawn/blob/v0.3.3/src/commands/jawn/aep/generate/selector/field-injection.ts)_ ## `sf jawn aep generate selector method` @@ -356,7 +356,7 @@ DESCRIPTION EXAMPLES Generate selector method scaffolding: - $ sf jawn aep generate selector method -c SelectBySloganMethod --sobject-selector-class-name AccountsSelector -s ^ + $ sf jawn aep generate selector method -c SelectBySloganMethod --sobject-selector-class-name AccountsSelector -s \ Account FLAG DESCRIPTIONS @@ -365,7 +365,7 @@ FLAG DESCRIPTIONS Override the api version used for api requests made by this command ``` -_See code: [src/commands/jawn/aep/generate/selector/method.ts](https://github.com/Syntax-Syllogism/jawn/blob/v0.3.1/src/commands/jawn/aep/generate/selector/method.ts)_ +_See code: [src/commands/jawn/aep/generate/selector/method.ts](https://github.com/Syntax-Syllogism/jawn/blob/v0.3.3/src/commands/jawn/aep/generate/selector/method.ts)_ ## `sf jawn aep generate service` @@ -406,7 +406,7 @@ FLAG DESCRIPTIONS Override the api version used for api requests made by this command ``` -_See code: [src/commands/jawn/aep/generate/service.ts](https://github.com/Syntax-Syllogism/jawn/blob/v0.3.1/src/commands/jawn/aep/generate/service.ts)_ +_See code: [src/commands/jawn/aep/generate/service.ts](https://github.com/Syntax-Syllogism/jawn/blob/v0.3.3/src/commands/jawn/aep/generate/service.ts)_ ## `sf jawn aep generate unitofwork` @@ -448,7 +448,7 @@ FLAG DESCRIPTIONS Override the api version used for api requests made by this command ``` -_See code: [src/commands/jawn/aep/generate/unitofwork.ts](https://github.com/Syntax-Syllogism/jawn/blob/v0.3.1/src/commands/jawn/aep/generate/unitofwork.ts)_ +_See code: [src/commands/jawn/aep/generate/unitofwork.ts](https://github.com/Syntax-Syllogism/jawn/blob/v0.3.3/src/commands/jawn/aep/generate/unitofwork.ts)_ ## `sf jawn user access` @@ -497,7 +497,7 @@ FLAG DESCRIPTIONS Override the api version used for api requests made by this command ``` -_See code: [src/commands/jawn/user/access.ts](https://github.com/Syntax-Syllogism/jawn/blob/v0.3.1/src/commands/jawn/user/access.ts)_ +_See code: [src/commands/jawn/user/access.ts](https://github.com/Syntax-Syllogism/jawn/blob/v0.3.3/src/commands/jawn/user/access.ts)_ ## `sf jawn user provision` @@ -525,18 +525,19 @@ GLOBAL FLAGS DESCRIPTION Provision users from user and persona definition files. - Provisions Salesforce users by merging persona defaults with user overrides, enforcing optional profile and role, - activating and unfreezing users, and planning or applying assignment changes. + Provisions Salesforce users by merging multiple personas per user into an effective persona (unioning assignment + lists, enforcing singular-value agreement), applying insert-only Username and Alias defaults, enforcing optional + profile and role, activating and unfreezing users, and planning or applying assignment changes. EXAMPLES Dry run with explicit external id: - $ sf jawn user provision --users-def config/user-def.json --personas-def config/persona-def.json --external-id ^ + $ sf jawn user provision --users-def config/user-def.json --personas-def config/persona-def.json --external-id \ FederationIdentifier --target-org myOrg --dry-run Apply provisioning with no prompt: - $ sf jawn user provision --users-def config/user-def.json --personas-def config/persona-def.json --target-org ^ + $ sf jawn user provision --users-def config/user-def.json --personas-def config/persona-def.json --target-org \ myOrg --no-prompt FLAG DESCRIPTIONS @@ -545,7 +546,7 @@ FLAG DESCRIPTIONS Override the api version used for api requests made by this command ``` -_See code: [src/commands/jawn/user/provision.ts](https://github.com/Syntax-Syllogism/jawn/blob/v0.3.1/src/commands/jawn/user/provision.ts)_ +_See code: [src/commands/jawn/user/provision.ts](https://github.com/Syntax-Syllogism/jawn/blob/v0.3.3/src/commands/jawn/user/provision.ts)_ @@ -560,6 +561,43 @@ User definitions and persona definitions live in two separate JSON files so that - User-level fields override persona `userAttributes` when both supply a value. - A user may also include a `match` meta key to choose its own lookup field. +#### Multiple personas per user + +Each user entry must include a `personas` array listing one or more persona names defined in `persona-def.json`. The command merges all named personas into a single effective persona before planning: + +- **Assignment lists** (`permissionSets`, `permissionSetGroups`, `publicGroups`, `queues`) are unioned across all personas, with duplicates removed in first-seen order. +- **Singular values** (`profile`, `role`, per-category modes) must agree across all personas. If two personas specify different values for the same singular field, that user fails with a per-row conflict error — the rest of the batch continues. +- **`userAttributes` keys** are also conflict-checked. If two personas supply the same key with different values, the user fails unless the user entry itself overrides that field (user-level value always wins). + +> **Breaking change from earlier versions:** the old single-string `"persona"` key is no longer supported. A user entry with `"persona": "..."` produces a per-row migration error. Replace it with `"personas": ["..."]`. + +#### Username and Alias defaults (insert only) + +When creating a new user (no matching existing record), `Username` and `Alias` are automatically derived if omitted: + +- **`Username`** — defaults to `.`, where `` is the first DNS label of the org's My Domain hostname (e.g. `jdoe@acme.com` + org `mycompany.my.salesforce.com` → `jdoe@acme.com.mycompany`). If the domain cannot be derived, the field is left unset and the normal missing-required-field error is reported. +- **`Alias`** — derived from `FirstName` and `LastName`: take up to 3 letters from each; when one name is shorter than 3 letters, borrow from the other to target 6 total; lowercase; truncated to Salesforce's 8-char limit. Examples: `John`/`Doe` → `johdoe`; `Jo`/`Anderson` → `joande`; `Al`/`Bo` → `albo`. + +Defaults are never applied to users matched for update, and explicit `Username`/`Alias` values in the user entry are always used as-is. + +#### Field precedence + +User-level values override persona values for every overlapping field. The only place a persona "wins" outright is by contributing assignment-list entries (which union) and by supplying a value the user left unset. + +| Setting | Set on a user (`user-def.json`) | Set on a persona (`persona-def.json`) | Who wins | Notes | +| ------------------------------------------------------------- | --------------------------------------------------------- | ------------------------------------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | +| **Profile** | `profile` (name or 15/18-char Id) **or** raw `ProfileId` | `profile` (name or Id) | user `profile` → user `ProfileId` → persona `profile` | `Profile.Name` resolution. Setting both `profile` and `ProfileId` on one user is a per-row error. Required on insert. | +| **Role** | `role` (DeveloperName/Name or Id) **or** raw `UserRoleId` | `role` | user `role` → user `UserRoleId` → persona `role` | `UserRole.DeveloperName`/`Name` resolution. Both `role`+`UserRoleId` is a per-row error. Optional. | +| **Username** | `Username` | persona `userAttributes.Username` | explicit value > computed default | Default `.`, **insert only**. | +| **Alias** | `Alias` | persona `userAttributes.Alias` | explicit value > computed default | Default 3+3 name derivation, **insert only**. | +| **Permission-flag fields** (`UserPermissionKnowledgeUser`, …) | direct boolean field | persona `userAttributes` | user field > persona `userAttributes` | Written onto `User`; subject to org licenses. | +| **Other writeable `User` fields** (`Title`, `Department`, …) | direct field | persona `userAttributes` | user field > persona `userAttributes` | Canonicalized case-insensitively against `User` describe. | +| **Assignment lists** (permission sets, groups, queues) | — (not settable per user) | persona lists + per-category `…Mode` | union across all personas | Resolved by name/Id; `additive` keeps existing, `sync` removes unlisted. | +| **Match field** (upsert key) | `match` meta key | — | per-user `match` > `--external-id` flag | Must be an external-Id field or `Username`/`Email`/`FederationIdentifier`. | +| **IsActive** | — | — | always `true` | Provisioning always activates and unfreezes. | + +A user-level `profile` or `role` also suppresses the corresponding persona-vs-persona conflict error for that field — if you name your own profile it no longer matters that two personas disagreed. + #### Match resolution - `--external-id` sets the default field used to match existing users. @@ -571,24 +609,26 @@ User definitions and persona definitions live in two separate JSON files so that ```json { "users": [ - { "persona": "ops", "match": "FederationIdentifier", "FederationIdentifier": "ABC123", "LastName": "Park" }, - { "persona": "csr", "match": "Username", "Username": "bob@acme.com.dev", "LastName": "Bob" }, - { "persona": "finance", "match": "Employee_ID__c", "Employee_ID__c": "E-9981", "LastName": "Su" }, - { "persona": "creator", "Username": "alice@acme.com.dev", "LastName": "Alice" } + { "personas": ["ops"], "match": "FederationIdentifier", "FederationIdentifier": "ABC123", "LastName": "Park" }, + { "personas": ["csr"], "match": "Username", "Username": "bob@acme.com.dev", "LastName": "Bob" }, + { "personas": ["finance"], "match": "Employee_ID__c", "Employee_ID__c": "E-9981", "LastName": "Su" }, + { "personas": ["creator"], "Username": "alice@acme.com.dev", "LastName": "Alice" } ] } ``` #### Practical required fields for new user creation -- `Username` +The following fields are required when inserting a new user. `Username` and `Alias` can be omitted and will be defaulted automatically (see above); all others must be explicitly provided. + +- `Username` _(auto-defaulted from `Email` + org My Domain if omitted)_ - `LastName` -- `Alias` +- `Alias` _(auto-defaulted from `FirstName`/`LastName` if omitted)_ - `TimeZoneSidKey` - `LocaleSidKey` - `EmailEncodingKey` - `LanguageLocaleKey` -- `ProfileId` when persona `profile` is omitted +- `ProfileId` when no persona specifies `profile` #### Reference lookup behavior @@ -607,7 +647,7 @@ Missing optional assignment targets produce a warning and are skipped. Missing r #### Assignment modes -Each assignment category has its own mode property. The default for every category is `additive`. +Each assignment category has its own mode property. The default for every category is `additive`. When multiple personas are merged, the mode for each category must agree across all personas that specify it; a disagreement is a per-row conflict error. | Mode property | Behavior | | ------------------------ | ------------------------------------------------------------------------------------ | @@ -632,13 +672,11 @@ Password set/reset is intentionally out of scope. { "users": [ { - "Username": "jdoe@email.com.mySalesforceOrg", + "personas": ["admin"], "FederationIdentifier": "ABCD1234", - "persona": "admin", "FirstName": "John", "LastName": "Doe", "Email": "jdoe@email.com", - "Alias": "jdoe", "LocaleSidKey": "en_US", "EmailEncodingKey": "UTF-8", "LanguageLocaleKey": "en_US", @@ -648,6 +686,8 @@ Password set/reset is intentionally out of scope. } ``` +`Username` and `Alias` are omitted above and will be defaulted on insert to `jdoe@email.com.` and `johdoe` respectively. + #### Example `persona-def.json` ```json diff --git a/messages/jawn.user.provision.md b/messages/jawn.user.provision.md index c0c0d50..490eb74 100644 --- a/messages/jawn.user.provision.md +++ b/messages/jawn.user.provision.md @@ -4,7 +4,7 @@ Provision users from user and persona definition files. # description -Provisions Salesforce users by merging persona defaults with user overrides, enforcing optional profile and role, activating and unfreezing users, and planning or applying assignment changes. +Provisions Salesforce users by merging multiple personas per user into an effective persona (unioning assignment lists, enforcing singular-value agreement), applying insert-only Username and Alias defaults, enforcing optional profile and role, activating and unfreezing users, and planning or applying assignment changes. # flags.target-org.summary @@ -90,6 +90,54 @@ Save operation returned no user id. Cross-reference update candidates for this user: %s +# errorNoPersonas + +Each user must include a non-empty personas array. + +# errorLegacyPersonaKey + +"persona" is no longer supported; use "personas": [ ... ]. + +# errorUnknownPersona + +Unknown persona "%s". + +# errorPersonaConflictProfile + +Personas conflict on profile: %s. + +# errorPersonaConflictRole + +Personas conflict on role: %s. + +# errorPersonaConflictUserAttribute + +Personas conflict on userAttribute "%s". + +# errorPersonaConflictMode + +Personas conflict on %s. + +# errorUserProfileConflict + +Set either "profile" or "ProfileId" on a user, not both. + +# errorUserRoleConflict + +Set either "role" or "UserRoleId" on a user, not both. + +# errorInvalidUserProfile + +user "profile" must be a string (a profile name or Id). Got: %s + +# errorInvalidUserRole + +user "role" must be a string (a role name/DeveloperName or Id). Got: %s + +# warningUserFailed + +%s failed: %s + # info.summary Processed %s users: %s created, %s updated, %s failed. diff --git a/package.json b/package.json index e534232..21a7d13 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@syntax-syllogism/jawn", "description": "a sf cli plugin for admin and developer workflows", - "version": "0.3.1", + "version": "0.3.3", "repository": { "type": "git", "url": "git+https://github.com/Syntax-Syllogism/jawn.git" diff --git a/src/commands/jawn/user/provision.ts b/src/commands/jawn/user/provision.ts index d102a53..59e31a5 100644 --- a/src/commands/jawn/user/provision.ts +++ b/src/commands/jawn/user/provision.ts @@ -2,8 +2,11 @@ import { readFile } from 'node:fs/promises'; import { Connection, Messages, SfError } from '@salesforce/core'; import { Flags, SfCommand } from '@salesforce/sf-plugins-core'; import { + buildDefaultAlias, + buildDefaultUsername, buildFieldMap, CanonicalizedUser, + deriveMyDomain, isSalesforceId, missingRequiredFieldsForInsert, normalizeMode, @@ -39,7 +42,8 @@ type UserPlan = { planId: string; order: number; key: string; - persona: string; + personas: string[]; + effectivePersona: PersonaDefinition; matchedBy: string | null; target: JsonRecord; existing?: ExistingUser; @@ -58,7 +62,7 @@ type OrderedUserResult = UserResult & { order: number; planId: string }; type UserResult = { key: string; id?: string; - persona: string; + personas: string[]; matchedBy: string | null; status: 'created' | 'updated' | 'failed' | 'planned'; actions: string[]; @@ -191,10 +195,15 @@ const resolveByRoleRef = async ( const resolveReferences = async ( conn: Connection, - personas: Record + personas: Record, + users: CanonicalizedUser[] ): Promise => { const warnings: string[] = []; const refs = collectPersonaRefs(personas); + for (const user of users) { + if (user.profileRef) refs.profiles.add(user.profileRef); + if (user.roleRef) refs.roles.add(user.roleRef); + } const [ profilesByRef, rolesByRef, @@ -280,19 +289,25 @@ const ensureWritableFields = ( } }; -const buildTarget = ( - user: CanonicalizedUser, - persona: PersonaDefinition, - refs: ResolvedRefs, - errors: string[] -): JsonRecord => { +const buildTarget = (user: CanonicalizedUser, refs: ResolvedRefs, errors: string[]): JsonRecord => { + const persona = user.effectivePersona; const target: JsonRecord = { ...user.fields, IsActive: true }; - if (persona.profile) { + // Profile: user profileRef > user raw ProfileId (already in target) > persona profile + if (user.profileRef) { + const profileId = refs.profilesByRef.get(user.profileRef); + if (!profileId) errors.push(messages.getMessage('errorReferenceRequiredMissing', ['Profile', user.profileRef])); + else target.ProfileId = profileId; + } else if (!target.ProfileId && persona.profile) { const profileId = refs.profilesByRef.get(persona.profile); if (!profileId) errors.push(messages.getMessage('errorReferenceRequiredMissing', ['Profile', persona.profile])); else target.ProfileId = profileId; } - if (persona.role) { + // Role: user roleRef > user raw UserRoleId (already in target) > persona role + if (user.roleRef) { + const roleId = refs.rolesByRef.get(user.roleRef); + if (!roleId) errors.push(messages.getMessage('errorReferenceRequiredMissing', ['UserRole', user.roleRef])); + else target.UserRoleId = roleId; + } else if (!target.UserRoleId && persona.role) { const roleId = refs.rolesByRef.get(persona.role); if (!roleId) errors.push(messages.getMessage('errorReferenceRequiredMissing', ['UserRole', persona.role])); else target.UserRoleId = roleId; @@ -553,6 +568,17 @@ const executeBulkUserSaves = async (conn: Connection, plans: UserPlan[]): Promis return outcomes; }; +const applyInsertDefaults = (target: JsonRecord, myDomain: string | undefined): void => { + if (!target.Username && myDomain) { + const u = buildDefaultUsername(target.Email, myDomain); + if (u) target.Username = u; + } + if (!target.Alias) { + const a = buildDefaultAlias(target.FirstName, target.LastName); + if (a) target.Alias = a; + } +}; + export default class UserProvision extends SfCommand { public static readonly summary = messages.getMessage('summary'); public static readonly description = messages.getMessage('description'); @@ -596,6 +622,7 @@ export default class UserProvision extends SfCommand { validatePersonaModes(personas); validateExternalIdFieldForFlag(flags['external-id'], fieldMap); const users = validateAndCanonicalizeUsers(usersDoc.users, personas, fieldMap); + const myDomain = deriveMyDomain(conn.instanceUrl); const defaultExternalIdField = flags['external-id'] ? fieldMap.get(flags['external-id'].toLowerCase())?.name : undefined; @@ -607,7 +634,11 @@ export default class UserProvision extends SfCommand { const validUsers = userEntries.filter(({ user }) => !user.validationErrors || user.validationErrors.length === 0); const [refs, existingResolution] = await Promise.all([ - resolveReferences(conn, personas), + resolveReferences( + conn, + personas, + validUsers.map(({ user }) => user) + ), getExistingUsers( conn, validUsers.map(({ user }) => user), @@ -620,9 +651,9 @@ export default class UserProvision extends SfCommand { } const plans: UserPlan[] = validUsers.map(({ user, order }) => { - const persona = personas[user.persona]; + const effectivePersona = user.effectivePersona; const errors: string[] = []; - const target = buildTarget(user, persona, refs, errors); + const target = buildTarget(user, refs, errors); const matchedBy = matchFieldFor(user) ?? null; const matchValue = matchedBy ? target[matchedBy] : undefined; const existing = @@ -637,7 +668,9 @@ export default class UserProvision extends SfCommand { errors.push(messages.getMessage('errorDuplicateExternalIdMatch', [matchedBy, matchValue])); } if (!existing) { - const missing = missingRequiredFieldsForInsert(target, persona); + applyInsertDefaults(target, myDomain); + const profileIntended = Boolean(user.profileRef) || Boolean(effectivePersona.profile); + const missing = missingRequiredFieldsForInsert(target, profileIntended); if (missing.length > 0) errors.push(messages.getMessage('errorMissingRequiredFields', [missing.join(', ')])); } ensureWritableFields(target, existing, fieldMap, errors); @@ -646,10 +679,11 @@ export default class UserProvision extends SfCommand { ]; if (!existing || existing.IsActive !== true) actions.push(flags['dry-run'] ? 'wouldActivate' : 'activated'); return { - planId: `${order}:${user.inputKey}:${user.persona}`, + planId: `${order}:${user.inputKey}:${user.personas.join('+')}`, order, key: user.inputKey, - persona: user.persona, + personas: user.personas, + effectivePersona, matchedBy, target, existing, @@ -659,10 +693,10 @@ export default class UserProvision extends SfCommand { }); const validationResults: OrderedUserResult[] = validationFailureUsers.map(({ user, order }) => ({ - planId: `${order}:${user.inputKey}:${user.persona}:validation`, + planId: `${order}:${user.inputKey}:${user.personas.join('+')}:validation`, order, key: user.inputKey, - persona: user.persona, + personas: user.personas, matchedBy: user.matchField ?? null, status: 'failed', actions: [], @@ -675,14 +709,13 @@ export default class UserProvision extends SfCommand { planId: plan.planId, order: plan.order, key: plan.key, - persona: plan.persona, + personas: plan.personas, matchedBy: plan.matchedBy, status: 'failed', actions: plan.actions, errors: plan.errors, }; } - const persona = personas[plan.persona]; if (flags['dry-run']) { const dryRunId = plan.existing?.Id ?? DRY_RUN_CREATE_ID; if (plan.existing) { @@ -693,13 +726,13 @@ export default class UserProvision extends SfCommand { ).records; if (frozenRows.length > 0) plan.actions.push('wouldUnfreeze'); } - await applyAssignments(conn, dryRunId, persona, refs, true, plan.actions, plan.errors); + await applyAssignments(conn, dryRunId, plan.effectivePersona, refs, true, plan.actions, plan.errors); return { planId: plan.planId, order: plan.order, key: plan.key, id: plan.existing?.Id, - persona: plan.persona, + personas: plan.personas, matchedBy: plan.matchedBy, status: 'planned', actions: plan.actions, @@ -715,14 +748,13 @@ export default class UserProvision extends SfCommand { planId: plan.planId, order: plan.order, key: plan.key, - persona: plan.persona, + personas: plan.personas, matchedBy: plan.matchedBy, status: 'failed', actions: plan.actions, errors: [messages.getMessage('errorMissingSaveId')], }; } - const persona = personas[plan.persona]; const frozenRows = ( await conn.query<{ Id: string }>(`SELECT Id FROM UserLogin WHERE UserId = '${esc(id)}' AND IsFrozen = true`) ).records; @@ -736,14 +768,14 @@ export default class UserProvision extends SfCommand { if (unfreezeErrors.length > 0) plan.errors.push(...unfreezeErrors); else plan.actions.push('unfrozen'); } - await applyAssignments(conn, id, persona, refs, false, plan.actions, plan.errors); + await applyAssignments(conn, id, plan.effectivePersona, refs, false, plan.actions, plan.errors); const status = plan.errors.length > 0 ? 'failed' : plan.existing ? 'updated' : 'created'; return { planId: plan.planId, order: plan.order, key: plan.key, id, - persona: plan.persona, + personas: plan.personas, matchedBy: plan.matchedBy, status, actions: plan.actions, @@ -757,7 +789,7 @@ export default class UserProvision extends SfCommand { planId: p.planId, order: p.order, key: p.key, - persona: p.persona, + personas: p.personas, matchedBy: p.matchedBy, status: 'failed', actions: p.actions, @@ -776,7 +808,7 @@ export default class UserProvision extends SfCommand { planId: o.plan.planId, order: o.plan.order, key: o.plan.key, - persona: o.plan.persona, + personas: o.plan.personas, matchedBy: o.plan.matchedBy, status: 'failed' as const, actions: o.plan.actions, @@ -795,7 +827,7 @@ export default class UserProvision extends SfCommand { .map((result) => ({ key: result.key, id: result.id, - persona: result.persona, + personas: result.personas, matchedBy: result.matchedBy ?? null, status: result.status, actions: result.actions, @@ -804,6 +836,11 @@ export default class UserProvision extends SfCommand { const output: ProvisionResult = { summary: summarize(results, refs.warnings.length), users: results }; if (!this.jsonEnabled()) { + for (const user of results) { + if (user.errors.length > 0) { + this.warn(messages.getMessage('warningUserFailed', [user.key, user.errors.join('; ')])); + } + } this.log( messages.getMessage('info.summary', [ output.summary.total, diff --git a/src/userProvisioning/planner.ts b/src/userProvisioning/planner.ts index 11145bb..915de91 100644 --- a/src/userProvisioning/planner.ts +++ b/src/userProvisioning/planner.ts @@ -14,17 +14,33 @@ export type PersonaDefinition = { userAttributes?: Record; }; -export type UserInput = Record & { persona?: string }; +export type UserInput = Record & { personas?: string[] }; export type ValidationWarning = { message: string; userKey?: string }; export type ValidationError = { - messageKey: 'errorInvalidUserMatchField' | 'errorUserMatchFieldEmpty'; + messageKey: + | 'errorInvalidUserMatchField' + | 'errorUserMatchFieldEmpty' + | 'errorNoPersonas' + | 'errorLegacyPersonaKey' + | 'errorUnknownPersona' + | 'errorPersonaConflictProfile' + | 'errorPersonaConflictRole' + | 'errorPersonaConflictUserAttribute' + | 'errorPersonaConflictMode' + | 'errorUserProfileConflict' + | 'errorUserRoleConflict' + | 'errorInvalidUserProfile' + | 'errorInvalidUserRole'; messageArgs: string[]; }; export type CanonicalizedUser = { inputKey: string; - persona: string; + personas: string[]; + effectivePersona: PersonaDefinition; + profileRef?: string; + roleRef?: string; matchField?: string; fields: Record; validationErrors?: ValidationError[]; @@ -33,7 +49,7 @@ export type CanonicalizedUser = { export const modeKeys = ['permissionSetMode', 'permissionSetGroupMode', 'publicGroupMode', 'queueMode'] as const; export const assignmentListKeys = ['permissionSets', 'permissionSetGroups', 'publicGroups', 'queues'] as const; -const reservedUserKeys = new Set(['persona', 'match']); +const reservedUserKeys = new Set(['personas', 'match', 'profile', 'role']); export type UserFieldMeta = { name: string; @@ -143,6 +159,211 @@ export const mergeUserFields = ( ...userFields, }); +const unionArray = (arrays: Array): T[] => { + const seen = new Set(); + const result: T[] = []; + for (const arr of arrays) { + for (const item of arr ?? []) { + if (!seen.has(item)) { + seen.add(item); + result.push(item); + } + } + } + return result; +}; + +// Extracted to keep mergePersonas under the eslint complexity ceiling (max 20). +const mergePersonaAttributes = ( + validDefs: PersonaDefinition[], + userOverrideFields: Record, + fieldMap: Map +): { userAttributes: Record | undefined; errors: ValidationError[] } => { + const attrMap = new Map(); + const attrConflicts = new Set(); + for (const def of validDefs) { + for (const [rawKey, value] of Object.entries(def.userAttributes ?? {})) { + const canonicalKey = fieldMap.get(rawKey.toLowerCase())?.name ?? rawKey; + if (attrMap.has(canonicalKey) && attrMap.get(canonicalKey) !== value) { + attrConflicts.add(canonicalKey); + } else { + attrMap.set(canonicalKey, value); + } + } + } + const errors: ValidationError[] = []; + for (const conflictKey of attrConflicts) { + if (!(conflictKey in userOverrideFields)) { + errors.push(buildValidationError('errorPersonaConflictUserAttribute', [conflictKey])); + } + } + return { userAttributes: attrMap.size > 0 ? Object.fromEntries(attrMap) : undefined, errors }; +}; + +export const mergePersonas = ( + names: string[], + personas: Record, + userOverrideFields: Record, + fieldMap: Map, + hasProfileOverride = false, + hasRoleOverride = false +): { effective: PersonaDefinition; errors: ValidationError[] } => { + const errors: ValidationError[] = []; + const validDefs: PersonaDefinition[] = []; + + for (const name of names) { + const def = personas[name]; + if (!def) { + errors.push(buildValidationError('errorUnknownPersona', [name])); + } else { + validDefs.push(def); + } + } + + const effective: PersonaDefinition = {}; + + // Union assignment lists + for (const listKey of assignmentListKeys) { + const unioned = unionArray(validDefs.map((d) => d[listKey])); + if (unioned.length > 0) effective[listKey] = unioned; + } + + // Singular: profile + const profiles = [...new Set(validDefs.map((d) => d.profile).filter((v): v is string => Boolean(v)))]; + if (profiles.length > 1 && !hasProfileOverride) { + errors.push(buildValidationError('errorPersonaConflictProfile', [profiles.join(', ')])); + } else if (profiles.length === 1) { + effective.profile = profiles[0]; + } + + // Singular: role + const roles = [...new Set(validDefs.map((d) => d.role).filter((v): v is string => Boolean(v)))]; + if (roles.length > 1 && !hasRoleOverride) { + errors.push(buildValidationError('errorPersonaConflictRole', [roles.join(', ')])); + } else if (roles.length === 1) { + effective.role = roles[0]; + } + + // Modes + for (const modeKey of modeKeys) { + const modes = [...new Set(validDefs.map((d) => d[modeKey]).filter((v): v is AssignmentMode => v !== undefined))]; + if (modes.length > 1) { + errors.push(buildValidationError('errorPersonaConflictMode', [modeKey])); + } else if (modes.length === 1) { + effective[modeKey] = modes[0]; + } + } + + const { userAttributes, errors: attrErrors } = mergePersonaAttributes(validDefs, userOverrideFields, fieldMap); + errors.push(...attrErrors); + if (userAttributes) effective.userAttributes = userAttributes; + + return { effective, errors }; +}; + +const sanitizeAliasPart = (s: unknown): string => (typeof s === 'string' ? s : '').replace(/[^a-z0-9]/gi, ''); + +export const buildDefaultAlias = (firstName?: unknown, lastName?: unknown): string | undefined => { + const first = sanitizeAliasPart(firstName); + const last = sanitizeAliasPart(lastName); + if (!first && !last) return undefined; + const firstDeficit = Math.max(0, 3 - first.length); + const lastDeficit = Math.max(0, 3 - last.length); + const firstPart = first.slice(0, 3 + lastDeficit); + const lastPart = last.slice(0, 3 + firstDeficit); + const alias = (firstPart + lastPart).toLowerCase().slice(0, 8); + return alias || undefined; +}; + +export const buildDefaultUsername = (email: unknown, myDomain: string): string | undefined => + typeof email === 'string' && email.length > 0 ? `${email}.${myDomain}` : undefined; + +export const deriveMyDomain = (instanceUrl: string): string | undefined => { + try { + const host = new URL(instanceUrl).hostname; + const firstLabel = host.split('.')[0]; + return firstLabel || undefined; + } catch { + return undefined; + } +}; + +const resolveInputKey = (fields: Record, fallback: string): string => + (typeof fields.FederationIdentifier === 'string' && fields.FederationIdentifier) || + (typeof fields.Username === 'string' && fields.Username) || + (typeof fields.Email === 'string' && fields.Email) || + fallback; + +// Reads a user-level profile/role meta key. Empty/whitespace and null are treated as +// "not provided"; any other non-string value is reported so malformed input is not silently dropped. +const extractUserRef = ( + value: unknown, + invalidKey: 'errorInvalidUserProfile' | 'errorInvalidUserRole', + errors: ValidationError[] +): string | undefined => { + if (value === null || value === undefined) return undefined; + if (typeof value !== 'string') { + errors.push(buildValidationError(invalidKey, [String(value)])); + return undefined; + } + return value.trim() === '' ? undefined : value; +}; + +const canonicalizeValidUser = ( + input: UserInput, + names: string[], + personas: Record, + fieldMap: Map, + fallback: string +): CanonicalizedUser => { + const candidateFields: Record = {}; + const errors: ValidationError[] = []; + let profileRef: string | undefined; + let roleRef: string | undefined; + for (const [k, v] of Object.entries(input)) { + const lower = k.toLowerCase(); + if (lower === 'profile') profileRef = extractUserRef(v, 'errorInvalidUserProfile', errors); + else if (lower === 'role') roleRef = extractUserRef(v, 'errorInvalidUserRole', errors); + else if (!reservedUserKeys.has(lower)) candidateFields[k] = v; + } + const userFields = canonicalizeFieldObject(candidateFields, fieldMap, `user personas=${names.join('+')}`); + if (profileRef !== undefined && 'ProfileId' in userFields) { + errors.push(buildValidationError('errorUserProfileConflict', [])); + profileRef = undefined; + } + if (roleRef !== undefined && 'UserRoleId' in userFields) { + errors.push(buildValidationError('errorUserRoleConflict', [])); + roleRef = undefined; + } + // A raw ProfileId/UserRoleId is also a user-level override, so it must suppress + // a persona profile/role conflict the same way the profile/role meta keys do. + const hasProfileOverride = profileRef !== undefined || 'ProfileId' in userFields; + const hasRoleOverride = roleRef !== undefined || 'UserRoleId' in userFields; + const { effective, errors: mergeErrors } = mergePersonas( + names, + personas, + userFields, + fieldMap, + hasProfileOverride, + hasRoleOverride + ); + const personaFields = canonicalizeFieldObject(effective.userAttributes, fieldMap, 'effective persona'); + const merged = mergeUserFields(personaFields, userFields); + const rawMatch = Object.entries(input).find(([k]) => k.toLowerCase() === 'match')?.[1]; + const { matchField, validationErrors: matchErrors } = resolveUserMatchField(rawMatch, merged, fieldMap); + const allErrors = [...errors, ...mergeErrors, ...matchErrors]; + return { + inputKey: resolveInputKey(merged, `${names.join('+')}:${fallback}`), + personas: names, + effectivePersona: effective, + profileRef, + roleRef, + matchField, + fields: merged, + validationErrors: allErrors.length > 0 ? allErrors : undefined, + }; +}; + export const validateAndCanonicalizeUsers = ( rawUsers: unknown, personas: Record, @@ -153,31 +374,45 @@ export const validateAndCanonicalizeUsers = ( for (const rawUser of rawUsers) { if (!rawUser || typeof rawUser !== 'object') throw new Error('Each user entry must be an object.'); const input = rawUser as UserInput; - const personaName = input.persona; - if (!personaName || typeof personaName !== 'string') throw new Error('Each user entry must include persona.'); - const persona = personas[personaName]; - if (!persona) throw new Error(`Unknown persona "${personaName}".`); - const candidateFields: Record = {}; - for (const [k, v] of Object.entries(input)) { - if (!reservedUserKeys.has(k.toLowerCase())) candidateFields[k] = v; + const fallback = String(users.length + 1); + + const hasLegacyPersonaKey = Object.keys(input).some((k) => k.toLowerCase() === 'persona'); + if (hasLegacyPersonaKey) { + const fields: Record = {}; + for (const [k, v] of Object.entries(input)) { + if (k.toLowerCase() !== 'persona') fields[k] = v; + } + const userFields = canonicalizeFieldObject(fields, fieldMap, 'user'); + users.push({ + inputKey: resolveInputKey(userFields, `unknown:${fallback}`), + personas: [], + effectivePersona: {}, + fields: userFields, + validationErrors: [buildValidationError('errorLegacyPersonaKey', [])], + }); + continue; } - const personaFields = canonicalizeFieldObject(persona.userAttributes, fieldMap, `persona ${personaName}`); - const userFields = canonicalizeFieldObject(candidateFields, fieldMap, `user persona=${personaName}`); - const merged = mergeUserFields(personaFields, userFields); - const rawMatch = Object.entries(input).find(([k]) => k.toLowerCase() === 'match')?.[1]; - const { matchField, validationErrors } = resolveUserMatchField(rawMatch, merged, fieldMap); - const inputKey = - (typeof merged.FederationIdentifier === 'string' && merged.FederationIdentifier) || - (typeof merged.Username === 'string' && merged.Username) || - (typeof merged.Email === 'string' && merged.Email) || - `${personaName}:${users.length + 1}`; - users.push({ - inputKey, - persona: personaName, - matchField, - fields: merged, - validationErrors: validationErrors.length > 0 ? validationErrors : undefined, - }); + + const personaNames = input.personas; + const validNames = + Array.isArray(personaNames) && personaNames.length > 0 && personaNames.every((n) => typeof n === 'string'); + if (!validNames) { + const fields: Record = {}; + for (const [k, v] of Object.entries(input)) { + if (!reservedUserKeys.has(k.toLowerCase())) fields[k] = v; + } + const userFields = canonicalizeFieldObject(fields, fieldMap, 'user'); + users.push({ + inputKey: resolveInputKey(userFields, `unknown:${fallback}`), + personas: [], + effectivePersona: {}, + fields: userFields, + validationErrors: [buildValidationError('errorNoPersonas', [])], + }); + continue; + } + + users.push(canonicalizeValidUser(input, personaNames, personas, fieldMap, fallback)); } return users; }; @@ -204,11 +439,11 @@ export const practicalRequiredUserFields = [ 'LanguageLocaleKey', ] as const; -export const missingRequiredFieldsForInsert = ( - fields: Record, - persona: PersonaDefinition -): string[] => { +// `profileIntended` is true when a profile was specified from any source (user `profile` meta key, +// raw `ProfileId`, or persona `profile`). When intended but unresolved, buildTarget already reports a +// reference-missing error, so we must not also report ProfileId as missing here (avoids a double error). +export const missingRequiredFieldsForInsert = (fields: Record, profileIntended: boolean): string[] => { const missing: string[] = practicalRequiredUserFields.filter((field) => !fields[field]); - if (!persona.profile && !fields.ProfileId) missing.push('ProfileId'); + if (!profileIntended && !fields.ProfileId) missing.push('ProfileId'); return missing; }; diff --git a/test/commands/jawn/user/provision.test.ts b/test/commands/jawn/user/provision.test.ts index e091940..110a3d1 100644 --- a/test/commands/jawn/user/provision.test.ts +++ b/test/commands/jawn/user/provision.test.ts @@ -12,6 +12,7 @@ type FakeConnection = { query: sinon.SinonStub; sobject: sinon.SinonStub; sobjectMap: Record; + instanceUrl: string; }; const makeSuccessResults = (items: unknown, prefix: string): Array<{ success: true; id: string; errors: [] }> => { @@ -26,7 +27,7 @@ const makeSuccessResults = (items: unknown, prefix: string): Array<{ success: tr const bulkSuccessStub = (prefix: string): sinon.SinonStub => sinon.stub().callsFake(async (items: unknown) => makeSuccessResults(items, prefix)); -const createFakeConnection = (): FakeConnection => { +const createFakeConnection = (instanceUrl = 'https://myorg.my.salesforce.com'): FakeConnection => { const sobjectMap: Record = { User: { create: bulkSuccessStub('005xx000000000'), @@ -50,6 +51,7 @@ const createFakeConnection = (): FakeConnection => { }, }; return { + instanceUrl, describe: sinon.stub().resolves({ fields: [ { name: 'Username', createable: true, updateable: true, externalId: false }, @@ -62,8 +64,10 @@ const createFakeConnection = (): FakeConnection => { { name: 'EmailEncodingKey', createable: true, updateable: true, externalId: false }, { name: 'LanguageLocaleKey', createable: true, updateable: true, externalId: false }, { name: 'ProfileId', createable: true, updateable: true, externalId: false }, + { name: 'UserRoleId', createable: true, updateable: true, externalId: false }, { name: 'Title', createable: true, updateable: true, externalId: false }, { name: 'Department', createable: true, updateable: true, externalId: false }, + { name: 'UserPermissionKnowledgeUser', createable: true, updateable: true, externalId: false }, { name: 'FederationIdentifier', createable: true, updateable: true, externalId: true }, ], }), @@ -184,7 +188,7 @@ describe('jawn user provision command', () => { { Username: 'new.user@example.test', FederationIdentifier: 'A001', - persona: 'default', + personas: ['default'], FirstName: 'New', LastName: 'User', Alias: 'nuser', @@ -196,7 +200,7 @@ describe('jawn user provision command', () => { { Username: 'existing.user@example.test', FederationIdentifier: 'A002', - persona: 'default', + personas: ['default'], FirstName: 'Existing', LastName: 'User', Alias: 'euser', @@ -266,7 +270,7 @@ describe('jawn user provision command', () => { { match: 'FederationIdentifier', FederationIdentifier: 'A101', - persona: 'default', + personas: ['default'], LastName: 'One', Alias: 'one', TimeZoneSidKey: 'America/Los_Angeles', @@ -277,7 +281,7 @@ describe('jawn user provision command', () => { { match: 'Username', Username: 'two@example.test', - persona: 'default', + personas: ['default'], LastName: 'Two', Alias: 'two', TimeZoneSidKey: 'America/Los_Angeles', @@ -336,7 +340,7 @@ describe('jawn user provision command', () => { { match: 'FederationIdentifier', FederationIdentifier: 'A200', - persona: 'default', + personas: ['default'], LastName: 'One', Alias: 'one', TimeZoneSidKey: 'America/Los_Angeles', @@ -347,7 +351,7 @@ describe('jawn user provision command', () => { { match: 'Username', Username: 'A200', - persona: 'default', + personas: ['default'], LastName: 'Two', Alias: 'two', TimeZoneSidKey: 'America/Los_Angeles', @@ -399,7 +403,7 @@ describe('jawn user provision command', () => { { match: 'FederationIdentifier', FederationIdentifier: '', - persona: 'default', + personas: ['default'], LastName: 'Bad', Alias: 'bad', TimeZoneSidKey: 'America/Los_Angeles', @@ -409,7 +413,7 @@ describe('jawn user provision command', () => { }, { Username: 'insert-only@example.test', - persona: 'default', + personas: ['default'], LastName: 'Good', Alias: 'good', TimeZoneSidKey: 'America/Los_Angeles', @@ -461,7 +465,7 @@ describe('jawn user provision command', () => { match: 'Username', Username: 'override@example.test', FederationIdentifier: 'FLAG-IGNORED', - persona: 'default', + personas: ['default'], LastName: 'Override', Alias: 'over', TimeZoneSidKey: 'America/Los_Angeles', @@ -471,7 +475,7 @@ describe('jawn user provision command', () => { }, { FederationIdentifier: 'FLAG-001', - persona: 'default', + personas: ['default'], LastName: 'Flag', Alias: 'flag', TimeZoneSidKey: 'America/Los_Angeles', @@ -549,7 +553,7 @@ describe('jawn user provision command', () => { { Username: 'existing.user@example.test', FederationIdentifier: 'A002', - persona: 'default', + personas: ['default'], LastName: 'User', Alias: 'euser', TimeZoneSidKey: 'America/Los_Angeles', @@ -619,7 +623,7 @@ describe('jawn user provision command', () => { { Username: 'dup@example.test', FederationIdentifier: 'A100', - persona: 'default', + personas: ['default'], LastName: 'Dup', Alias: 'dup', TimeZoneSidKey: 'America/Los_Angeles', @@ -674,7 +678,7 @@ describe('jawn user provision command', () => { users: [ { Username: 'u@example.test', - persona: 'default', + personas: ['default'], LastName: 'U', Alias: 'u', TimeZoneSidKey: 'America/Los_Angeles', @@ -737,7 +741,7 @@ describe('jawn user provision command', () => { users: [ { FederationIdentifier: 'A200', - persona: 'default', + personas: ['default'], LastName: 'Nope', Alias: 'np', TimeZoneSidKey: 'America/Los_Angeles', @@ -782,7 +786,7 @@ describe('jawn user provision command', () => { users: [ { FederationIdentifier: 'A002', - persona: 'default', + personas: ['default'], LastName: 'User', Alias: 'euser', TimeZoneSidKey: 'America/Los_Angeles', @@ -835,4 +839,990 @@ describe('jawn user provision command', () => { expect(result.users[0].errors.join(' ')).to.include('ProfileId='); expect(result.users[0].errors.join(' ')).to.include('UserRoleId='); }); + + // --- Multi-persona tests --- + + it('multi-persona happy path: unions assignment lists', async () => { + const fakeConn = createFakeConnection(); + const dir = mkdtempSync(join(tmpdir(), 'jawn-provision-test-')); + const usersPath = join(dir, 'users-multi.json'); + const personasPath = join(dir, 'personas-multi.json'); + writeFileSync( + usersPath, + JSON.stringify({ + users: [ + { + personas: ['base', 'support'], + FirstName: 'John', + LastName: 'Doe', + Email: 'jdoe@example.test', + Username: 'jdoe@example.test.myorg', + Alias: 'jdoe', + TimeZoneSidKey: 'America/Los_Angeles', + LocaleSidKey: 'en_US', + EmailEncodingKey: 'UTF-8', + LanguageLocaleKey: 'en_US', + }, + ], + }) + ); + writeFileSync( + personasPath, + JSON.stringify({ + personas: { + base: { profile: 'Admin', permissionSets: ['BasePS'] }, + support: { permissionSets: ['SupportPS'] }, + }, + }) + ); + fakeConn.query.callsFake(async (soql: string) => { + if (soql.includes('FROM Profile')) return { records: [{ Id: '00exx0000000001AAA', Name: 'Admin' }] }; + if (soql.includes('FROM PermissionSet WHERE Name IN')) { + return { + records: [ + { Id: '0PSbase000000001AAA', Name: 'BasePS' }, + { Id: '0PSsupport00001AAA', Name: 'SupportPS' }, + ], + }; + } + if (soql.includes('FROM UserLogin')) return { records: [] }; + if (soql.includes('FROM PermissionSetAssignment')) return { records: [] }; + if (soql.includes('FROM GroupMember')) return { records: [] }; + return { records: [] }; + }); + sinon.stub(UserProvision.prototype as unknown as Record, 'parse').resolves({ + flags: { + 'target-org': { getConnection: () => fakeConn }, + 'users-def': usersPath, + 'personas-def': personasPath, + 'external-id': undefined, + 'no-prompt': true, + 'dry-run': false, + 'api-version': undefined, + }, + } as never); + + const result = await UserProvision.run(['--json']); + expect(result.users[0].status).to.equal('created'); + expect(result.users[0].personas).to.deep.equal(['base', 'support']); + const psaCreate = fakeConn.sobjectMap.PermissionSetAssignment.create; + const psaArgs = (psaCreate.firstCall.args[0] as Array<{ PermissionSetId: string }>).map((r) => r.PermissionSetId); + expect(psaArgs).to.include('0PSbase000000001AAA'); + expect(psaArgs).to.include('0PSsupport00001AAA'); + }); + + it('conflict on persona profile fails that user, batch continues', async () => { + const fakeConn = createFakeConnection(); + const dir = mkdtempSync(join(tmpdir(), 'jawn-provision-test-')); + const usersPath = join(dir, 'users-conflict.json'); + const personasPath = join(dir, 'personas-conflict.json'); + writeFileSync( + usersPath, + JSON.stringify({ + users: [ + { + personas: ['a', 'b'], + LastName: 'Conflict', + Email: 'conflict@example.test', + }, + { + personas: ['a'], + LastName: 'Good', + Username: 'good@example.test', + Alias: 'good', + TimeZoneSidKey: 'America/Los_Angeles', + LocaleSidKey: 'en_US', + EmailEncodingKey: 'UTF-8', + LanguageLocaleKey: 'en_US', + }, + ], + }) + ); + writeFileSync( + personasPath, + JSON.stringify({ + personas: { + a: { profile: 'Admin' }, + b: { profile: 'Standard' }, + }, + }) + ); + fakeConn.query.callsFake(async (soql: string) => { + if (soql.includes('FROM Profile')) + return { + records: [ + { Id: '00exx0000000001AAA', Name: 'Admin' }, + { Id: '00exx0000000002AAA', Name: 'Standard' }, + ], + }; + return { records: [] }; + }); + sinon.stub(UserProvision.prototype as unknown as Record, 'parse').resolves({ + flags: { + 'target-org': { getConnection: () => fakeConn }, + 'users-def': usersPath, + 'personas-def': personasPath, + 'external-id': undefined, + 'no-prompt': true, + 'dry-run': false, + 'api-version': undefined, + }, + } as never); + + const result = await UserProvision.run(['--json']); + const conflictUser = result.users.find((u) => u.personas.includes('b')); + const goodUser = result.users.find((u) => u.personas.length === 1 && u.personas[0] === 'a'); + expect(conflictUser?.status).to.equal('failed'); + expect(conflictUser?.errors.join(' ')).to.include('conflict'); + expect(goodUser?.status).to.equal('created'); + }); + + it('Username default applied on insert when not provided', async () => { + const fakeConn = createFakeConnection('https://myorg.my.salesforce.com'); + const dir = mkdtempSync(join(tmpdir(), 'jawn-provision-test-')); + const usersPath = join(dir, 'users-default-un.json'); + const personasPath = join(dir, 'personas-default-un.json'); + writeFileSync( + usersPath, + JSON.stringify({ + users: [ + { + personas: ['default'], + FirstName: 'Jane', + LastName: 'Smith', + Email: 'jsmith@example.test', + Alias: 'jsmi', + TimeZoneSidKey: 'America/Los_Angeles', + LocaleSidKey: 'en_US', + EmailEncodingKey: 'UTF-8', + LanguageLocaleKey: 'en_US', + }, + ], + }) + ); + writeFileSync(personasPath, JSON.stringify({ personas: { default: { profile: 'Admin' } } })); + fakeConn.query.callsFake(async (soql: string) => { + if (soql.includes('FROM Profile')) return { records: [{ Id: '00exx0000000001AAA', Name: 'Admin' }] }; + if (soql.includes('FROM UserLogin')) return { records: [] }; + if (soql.includes('FROM PermissionSetAssignment')) return { records: [] }; + if (soql.includes('FROM GroupMember')) return { records: [] }; + return { records: [] }; + }); + sinon.stub(UserProvision.prototype as unknown as Record, 'parse').resolves({ + flags: { + 'target-org': { getConnection: () => fakeConn }, + 'users-def': usersPath, + 'personas-def': personasPath, + 'external-id': undefined, + 'no-prompt': true, + 'dry-run': false, + 'api-version': undefined, + }, + } as never); + + await UserProvision.run(['--json']); + const createCall = fakeConn.sobjectMap.User.create.firstCall; + const createdUser = (createCall.args[0] as Array>)[0]; + expect(createdUser.Username).to.equal('jsmith@example.test.myorg'); + }); + + it('Alias default applied on insert when not provided', async () => { + const fakeConn = createFakeConnection(); + const dir = mkdtempSync(join(tmpdir(), 'jawn-provision-test-')); + const usersPath = join(dir, 'users-default-alias.json'); + const personasPath = join(dir, 'personas-default-alias.json'); + writeFileSync( + usersPath, + JSON.stringify({ + users: [ + { + personas: ['default'], + FirstName: 'John', + LastName: 'Doe', + Username: 'jdoe@example.test', + Email: 'jdoe@example.test', + TimeZoneSidKey: 'America/Los_Angeles', + LocaleSidKey: 'en_US', + EmailEncodingKey: 'UTF-8', + LanguageLocaleKey: 'en_US', + }, + ], + }) + ); + writeFileSync(personasPath, JSON.stringify({ personas: { default: { profile: 'Admin' } } })); + fakeConn.query.callsFake(async (soql: string) => { + if (soql.includes('FROM Profile')) return { records: [{ Id: '00exx0000000001AAA', Name: 'Admin' }] }; + if (soql.includes('FROM UserLogin')) return { records: [] }; + if (soql.includes('FROM PermissionSetAssignment')) return { records: [] }; + if (soql.includes('FROM GroupMember')) return { records: [] }; + return { records: [] }; + }); + sinon.stub(UserProvision.prototype as unknown as Record, 'parse').resolves({ + flags: { + 'target-org': { getConnection: () => fakeConn }, + 'users-def': usersPath, + 'personas-def': personasPath, + 'external-id': undefined, + 'no-prompt': true, + 'dry-run': false, + 'api-version': undefined, + }, + } as never); + + await UserProvision.run(['--json']); + const createCall = fakeConn.sobjectMap.User.create.firstCall; + const createdUser = (createCall.args[0] as Array>)[0]; + expect(createdUser.Alias).to.equal('johdoe'); + }); + + it('defaults NOT applied on update (existing user)', async () => { + const fakeConn = createFakeConnection(); + const dir = mkdtempSync(join(tmpdir(), 'jawn-provision-test-')); + const usersPath = join(dir, 'users-no-defaults-update.json'); + const personasPath = join(dir, 'personas-no-defaults-update.json'); + writeFileSync( + usersPath, + JSON.stringify({ + users: [ + { + FederationIdentifier: 'EX001', + personas: ['default'], + FirstName: 'John', + LastName: 'Doe', + Email: 'jdoe@example.test', + TimeZoneSidKey: 'America/Los_Angeles', + LocaleSidKey: 'en_US', + EmailEncodingKey: 'UTF-8', + LanguageLocaleKey: 'en_US', + }, + ], + }) + ); + writeFileSync(personasPath, JSON.stringify({ personas: { default: { profile: 'Admin' } } })); + fakeConn.query.callsFake(async (soql: string) => { + if (soql.includes('FROM Profile')) return { records: [{ Id: '00exx0000000001AAA', Name: 'Admin' }] }; + if (soql.includes('SELECT Id, IsActive, FederationIdentifier FROM User')) + return { records: [{ Id: '005xx0000000001AAA', IsActive: true, FederationIdentifier: 'EX001' }] }; + if (soql.includes('FROM UserLogin')) return { records: [] }; + if (soql.includes('FROM PermissionSetAssignment')) return { records: [] }; + if (soql.includes('FROM GroupMember')) return { records: [] }; + return { records: [] }; + }); + sinon.stub(UserProvision.prototype as unknown as Record, 'parse').resolves({ + flags: { + 'target-org': { getConnection: () => fakeConn }, + 'users-def': usersPath, + 'personas-def': personasPath, + 'external-id': 'FederationIdentifier', + 'no-prompt': true, + 'dry-run': false, + 'api-version': undefined, + }, + } as never); + + await UserProvision.run(['--json']); + const updateCall = fakeConn.sobjectMap.User.update.firstCall; + const updatedUser = (updateCall.args[0] as Array>)[0]; + expect(updatedUser.Username).to.equal(undefined); + expect(updatedUser.Alias).to.equal(undefined); + }); + + it('explicit Username and Alias are never overwritten by defaults', async () => { + const fakeConn = createFakeConnection('https://myorg.my.salesforce.com'); + const dir = mkdtempSync(join(tmpdir(), 'jawn-provision-test-')); + const usersPath = join(dir, 'users-explicit.json'); + const personasPath = join(dir, 'personas-explicit.json'); + writeFileSync( + usersPath, + JSON.stringify({ + users: [ + { + personas: ['default'], + FirstName: 'Jane', + LastName: 'Doe', + Email: 'jdoe@example.test', + Username: 'explicit@custom.test', + Alias: 'custom', + TimeZoneSidKey: 'America/Los_Angeles', + LocaleSidKey: 'en_US', + EmailEncodingKey: 'UTF-8', + LanguageLocaleKey: 'en_US', + }, + ], + }) + ); + writeFileSync(personasPath, JSON.stringify({ personas: { default: { profile: 'Admin' } } })); + fakeConn.query.callsFake(async (soql: string) => { + if (soql.includes('FROM Profile')) return { records: [{ Id: '00exx0000000001AAA', Name: 'Admin' }] }; + if (soql.includes('FROM UserLogin')) return { records: [] }; + if (soql.includes('FROM PermissionSetAssignment')) return { records: [] }; + if (soql.includes('FROM GroupMember')) return { records: [] }; + return { records: [] }; + }); + sinon.stub(UserProvision.prototype as unknown as Record, 'parse').resolves({ + flags: { + 'target-org': { getConnection: () => fakeConn }, + 'users-def': usersPath, + 'personas-def': personasPath, + 'external-id': undefined, + 'no-prompt': true, + 'dry-run': false, + 'api-version': undefined, + }, + } as never); + + await UserProvision.run(['--json']); + const createCall = fakeConn.sobjectMap.User.create.firstCall; + const createdUser = (createCall.args[0] as Array>)[0]; + expect(createdUser.Username).to.equal('explicit@custom.test'); + expect(createdUser.Alias).to.equal('custom'); + }); + + it('json output includes personas array per user', async () => { + const fakeConn = createFakeConnection(); + const dir = mkdtempSync(join(tmpdir(), 'jawn-provision-test-')); + const usersPath = join(dir, 'users-json.json'); + const personasPath = join(dir, 'personas-json.json'); + writeFileSync( + usersPath, + JSON.stringify({ + users: [ + { + personas: ['base', 'support'], + Username: 'u@example.test', + LastName: 'U', + Alias: 'u', + TimeZoneSidKey: 'America/Los_Angeles', + LocaleSidKey: 'en_US', + EmailEncodingKey: 'UTF-8', + LanguageLocaleKey: 'en_US', + }, + ], + }) + ); + writeFileSync(personasPath, JSON.stringify({ personas: { base: { profile: 'Admin' }, support: {} } })); + fakeConn.query.callsFake(async (soql: string) => { + if (soql.includes('FROM Profile')) return { records: [{ Id: '00exx0000000001AAA', Name: 'Admin' }] }; + if (soql.includes('FROM UserLogin')) return { records: [] }; + if (soql.includes('FROM PermissionSetAssignment')) return { records: [] }; + if (soql.includes('FROM GroupMember')) return { records: [] }; + return { records: [] }; + }); + sinon.stub(UserProvision.prototype as unknown as Record, 'parse').resolves({ + flags: { + 'target-org': { getConnection: () => fakeConn }, + 'users-def': usersPath, + 'personas-def': personasPath, + 'external-id': undefined, + 'no-prompt': true, + 'dry-run': false, + 'api-version': undefined, + }, + } as never); + + const result = await UserProvision.run(['--json']); + expect(result.users[0].personas).to.deep.equal(['base', 'support']); + }); + + // --- Feature 4: user-level profile/role overrides --- + + it('user profile by name overrides persona profile', async () => { + const fakeConn = createFakeConnection(); + const dir = mkdtempSync(join(tmpdir(), 'jawn-provision-test-')); + const usersPath = join(dir, 'users-profile-override.json'); + const personasPath = join(dir, 'personas-profile-override.json'); + writeFileSync( + usersPath, + JSON.stringify({ + users: [ + { + personas: ['base'], + profile: 'Standard', + Username: 'u@example.test', + LastName: 'User', + Alias: 'u', + TimeZoneSidKey: 'America/Los_Angeles', + LocaleSidKey: 'en_US', + EmailEncodingKey: 'UTF-8', + LanguageLocaleKey: 'en_US', + }, + ], + }) + ); + writeFileSync(personasPath, JSON.stringify({ personas: { base: { profile: 'Admin' } } })); + fakeConn.query.callsFake(async (soql: string) => { + if (soql.includes('FROM Profile')) + return { + records: [ + { Id: '00exx0000000001AAA', Name: 'Admin' }, + { Id: '00exx0000000002AAA', Name: 'Standard' }, + ], + }; + if (soql.includes('FROM UserLogin')) return { records: [] }; + if (soql.includes('FROM PermissionSetAssignment')) return { records: [] }; + if (soql.includes('FROM GroupMember')) return { records: [] }; + return { records: [] }; + }); + sinon.stub(UserProvision.prototype as unknown as Record, 'parse').resolves({ + flags: { + 'target-org': { getConnection: () => fakeConn }, + 'users-def': usersPath, + 'personas-def': personasPath, + 'external-id': undefined, + 'no-prompt': true, + 'dry-run': false, + 'api-version': undefined, + }, + } as never); + + await UserProvision.run(['--json']); + const createCall = fakeConn.sobjectMap.User.create.firstCall; + const createdUser = (createCall.args[0] as Array>)[0]; + expect(createdUser.ProfileId).to.equal('00exx0000000002AAA'); + }); + + it('user raw ProfileId overrides persona profile (regression guard against clobber)', async () => { + const fakeConn = createFakeConnection(); + const dir = mkdtempSync(join(tmpdir(), 'jawn-provision-test-')); + const usersPath = join(dir, 'users-profileid-override.json'); + const personasPath = join(dir, 'personas-profileid-override.json'); + writeFileSync( + usersPath, + JSON.stringify({ + users: [ + { + personas: ['base'], + ProfileId: '00exx0000000002AAA', + Username: 'u@example.test', + LastName: 'User', + Alias: 'u', + TimeZoneSidKey: 'America/Los_Angeles', + LocaleSidKey: 'en_US', + EmailEncodingKey: 'UTF-8', + LanguageLocaleKey: 'en_US', + }, + ], + }) + ); + writeFileSync(personasPath, JSON.stringify({ personas: { base: { profile: 'Admin' } } })); + fakeConn.query.callsFake(async (soql: string) => { + if (soql.includes('FROM Profile')) return { records: [{ Id: '00exx0000000001AAA', Name: 'Admin' }] }; + if (soql.includes('FROM UserLogin')) return { records: [] }; + if (soql.includes('FROM PermissionSetAssignment')) return { records: [] }; + if (soql.includes('FROM GroupMember')) return { records: [] }; + return { records: [] }; + }); + sinon.stub(UserProvision.prototype as unknown as Record, 'parse').resolves({ + flags: { + 'target-org': { getConnection: () => fakeConn }, + 'users-def': usersPath, + 'personas-def': personasPath, + 'external-id': undefined, + 'no-prompt': true, + 'dry-run': false, + 'api-version': undefined, + }, + } as never); + + await UserProvision.run(['--json']); + const createCall = fakeConn.sobjectMap.User.create.firstCall; + const createdUser = (createCall.args[0] as Array>)[0]; + expect(createdUser.ProfileId).to.equal('00exx0000000002AAA'); + }); + + it('user role by name overrides persona role', async () => { + const fakeConn = createFakeConnection(); + const dir = mkdtempSync(join(tmpdir(), 'jawn-provision-test-')); + const usersPath = join(dir, 'users-role-override.json'); + const personasPath = join(dir, 'personas-role-override.json'); + writeFileSync( + usersPath, + JSON.stringify({ + users: [ + { + personas: ['base'], + role: 'SalesRep', + Username: 'u@example.test', + LastName: 'User', + Alias: 'u', + TimeZoneSidKey: 'America/Los_Angeles', + LocaleSidKey: 'en_US', + EmailEncodingKey: 'UTF-8', + LanguageLocaleKey: 'en_US', + }, + ], + }) + ); + writeFileSync(personasPath, JSON.stringify({ personas: { base: { profile: 'Admin', role: 'CEO' } } })); + fakeConn.query.callsFake(async (soql: string) => { + if (soql.includes('FROM Profile')) return { records: [{ Id: '00exx0000000001AAA', Name: 'Admin' }] }; + if (soql.includes('FROM UserRole')) + return { + records: [ + { Id: '00Exx0000000001AAA', DeveloperName: 'CEO', Name: 'CEO' }, + { Id: '00Exx0000000002AAA', DeveloperName: 'SalesRep', Name: 'Sales Rep' }, + ], + }; + if (soql.includes('FROM UserLogin')) return { records: [] }; + if (soql.includes('FROM PermissionSetAssignment')) return { records: [] }; + if (soql.includes('FROM GroupMember')) return { records: [] }; + return { records: [] }; + }); + sinon.stub(UserProvision.prototype as unknown as Record, 'parse').resolves({ + flags: { + 'target-org': { getConnection: () => fakeConn }, + 'users-def': usersPath, + 'personas-def': personasPath, + 'external-id': undefined, + 'no-prompt': true, + 'dry-run': false, + 'api-version': undefined, + }, + } as never); + + await UserProvision.run(['--json']); + const createCall = fakeConn.sobjectMap.User.create.firstCall; + const createdUser = (createCall.args[0] as Array>)[0]; + expect(createdUser.UserRoleId).to.equal('00Exx0000000002AAA'); + }); + + it('user profile suppresses persona profile conflict and resolves user value', async () => { + const fakeConn = createFakeConnection(); + const dir = mkdtempSync(join(tmpdir(), 'jawn-provision-test-')); + const usersPath = join(dir, 'users-conflict-override.json'); + const personasPath = join(dir, 'personas-conflict-override.json'); + writeFileSync( + usersPath, + JSON.stringify({ + users: [ + { + personas: ['a', 'b'], + profile: 'Custom', + Username: 'u@example.test', + LastName: 'User', + Alias: 'u', + TimeZoneSidKey: 'America/Los_Angeles', + LocaleSidKey: 'en_US', + EmailEncodingKey: 'UTF-8', + LanguageLocaleKey: 'en_US', + }, + ], + }) + ); + writeFileSync(personasPath, JSON.stringify({ personas: { a: { profile: 'Admin' }, b: { profile: 'Standard' } } })); + fakeConn.query.callsFake(async (soql: string) => { + if (soql.includes('FROM Profile')) + return { + records: [ + { Id: '00exx0000000001AAA', Name: 'Admin' }, + { Id: '00exx0000000002AAA', Name: 'Standard' }, + { Id: '00exx0000000003AAA', Name: 'Custom' }, + ], + }; + if (soql.includes('FROM UserLogin')) return { records: [] }; + if (soql.includes('FROM PermissionSetAssignment')) return { records: [] }; + if (soql.includes('FROM GroupMember')) return { records: [] }; + return { records: [] }; + }); + sinon.stub(UserProvision.prototype as unknown as Record, 'parse').resolves({ + flags: { + 'target-org': { getConnection: () => fakeConn }, + 'users-def': usersPath, + 'personas-def': personasPath, + 'external-id': undefined, + 'no-prompt': true, + 'dry-run': false, + 'api-version': undefined, + }, + } as never); + + const result = await UserProvision.run(['--json']); + expect(result.users[0].status).to.equal('created'); + const createCall = fakeConn.sobjectMap.User.create.firstCall; + const createdUser = (createCall.args[0] as Array>)[0]; + expect(createdUser.ProfileId).to.equal('00exx0000000003AAA'); + }); + + it('raw ProfileId suppresses persona profile conflict and is used as-is', async () => { + const fakeConn = createFakeConnection(); + const dir = mkdtempSync(join(tmpdir(), 'jawn-provision-test-')); + const usersPath = join(dir, 'users-conflict-rawid.json'); + const personasPath = join(dir, 'personas-conflict-rawid.json'); + writeFileSync( + usersPath, + JSON.stringify({ + users: [ + { + personas: ['a', 'b'], + ProfileId: '00exx0000000009AAA', + Username: 'u@example.test', + LastName: 'User', + Alias: 'u', + TimeZoneSidKey: 'America/Los_Angeles', + LocaleSidKey: 'en_US', + EmailEncodingKey: 'UTF-8', + LanguageLocaleKey: 'en_US', + }, + ], + }) + ); + writeFileSync(personasPath, JSON.stringify({ personas: { a: { profile: 'Admin' }, b: { profile: 'Standard' } } })); + fakeConn.query.callsFake(async (soql: string) => { + if (soql.includes('FROM Profile')) + return { + records: [ + { Id: '00exx0000000001AAA', Name: 'Admin' }, + { Id: '00exx0000000002AAA', Name: 'Standard' }, + ], + }; + if (soql.includes('FROM UserLogin')) return { records: [] }; + if (soql.includes('FROM PermissionSetAssignment')) return { records: [] }; + if (soql.includes('FROM GroupMember')) return { records: [] }; + return { records: [] }; + }); + sinon.stub(UserProvision.prototype as unknown as Record, 'parse').resolves({ + flags: { + 'target-org': { getConnection: () => fakeConn }, + 'users-def': usersPath, + 'personas-def': personasPath, + 'external-id': undefined, + 'no-prompt': true, + 'dry-run': false, + 'api-version': undefined, + }, + } as never); + + const result = await UserProvision.run(['--json']); + expect(result.users[0].status).to.equal('created'); + expect(result.users[0].errors).to.deep.equal([]); + const createCall = fakeConn.sobjectMap.User.create.firstCall; + const createdUser = (createCall.args[0] as Array>)[0]; + expect(createdUser.ProfileId).to.equal('00exx0000000009AAA'); + }); + + it('profile + ProfileId on same user produces errorUserProfileConflict per row', async () => { + const fakeConn = createFakeConnection(); + const dir = mkdtempSync(join(tmpdir(), 'jawn-provision-test-')); + const usersPath = join(dir, 'users-profile-conflict.json'); + const personasPath = join(dir, 'personas-profile-conflict.json'); + writeFileSync( + usersPath, + JSON.stringify({ + users: [ + { + personas: ['base'], + profile: 'Admin', + ProfileId: '00exx0000000001AAA', + Username: 'bad@example.test', + LastName: 'Bad', + }, + { + personas: ['base'], + Username: 'good@example.test', + LastName: 'Good', + Alias: 'good', + TimeZoneSidKey: 'America/Los_Angeles', + LocaleSidKey: 'en_US', + EmailEncodingKey: 'UTF-8', + LanguageLocaleKey: 'en_US', + }, + ], + }) + ); + writeFileSync(personasPath, JSON.stringify({ personas: { base: { profile: 'Admin' } } })); + fakeConn.query.callsFake(async (soql: string) => { + if (soql.includes('FROM Profile')) return { records: [{ Id: '00exx0000000001AAA', Name: 'Admin' }] }; + if (soql.includes('FROM UserLogin')) return { records: [] }; + if (soql.includes('FROM PermissionSetAssignment')) return { records: [] }; + if (soql.includes('FROM GroupMember')) return { records: [] }; + return { records: [] }; + }); + sinon.stub(UserProvision.prototype as unknown as Record, 'parse').resolves({ + flags: { + 'target-org': { getConnection: () => fakeConn }, + 'users-def': usersPath, + 'personas-def': personasPath, + 'external-id': undefined, + 'no-prompt': true, + 'dry-run': false, + 'api-version': undefined, + }, + } as never); + + const result = await UserProvision.run(['--json']); + const badUser = result.users.find((u) => u.key.includes('bad')); + const goodUser = result.users.find((u) => u.key.includes('good')); + expect(badUser?.status).to.equal('failed'); + expect(badUser?.errors.join(' ')).to.include('profile'); + expect(badUser?.errors.join(' ')).to.include('ProfileId'); + expect(goodUser?.status).to.equal('created'); + }); + + it('unresolved user profile name fails that user only', async () => { + const fakeConn = createFakeConnection(); + const dir = mkdtempSync(join(tmpdir(), 'jawn-provision-test-')); + const usersPath = join(dir, 'users-unresolved-profile.json'); + const personasPath = join(dir, 'personas-unresolved-profile.json'); + writeFileSync( + usersPath, + JSON.stringify({ + users: [ + { + personas: ['base'], + profile: 'DoesNotExist', + Username: 'bad@example.test', + LastName: 'Bad', + Alias: 'bad', + TimeZoneSidKey: 'America/Los_Angeles', + LocaleSidKey: 'en_US', + EmailEncodingKey: 'UTF-8', + LanguageLocaleKey: 'en_US', + }, + { + personas: ['base'], + Username: 'good@example.test', + LastName: 'Good', + Alias: 'good', + TimeZoneSidKey: 'America/Los_Angeles', + LocaleSidKey: 'en_US', + EmailEncodingKey: 'UTF-8', + LanguageLocaleKey: 'en_US', + }, + ], + }) + ); + writeFileSync(personasPath, JSON.stringify({ personas: { base: { profile: 'Admin' } } })); + fakeConn.query.callsFake(async (soql: string) => { + if (soql.includes('FROM Profile')) return { records: [{ Id: '00exx0000000001AAA', Name: 'Admin' }] }; + if (soql.includes('FROM UserLogin')) return { records: [] }; + if (soql.includes('FROM PermissionSetAssignment')) return { records: [] }; + if (soql.includes('FROM GroupMember')) return { records: [] }; + return { records: [] }; + }); + sinon.stub(UserProvision.prototype as unknown as Record, 'parse').resolves({ + flags: { + 'target-org': { getConnection: () => fakeConn }, + 'users-def': usersPath, + 'personas-def': personasPath, + 'external-id': undefined, + 'no-prompt': true, + 'dry-run': false, + 'api-version': undefined, + }, + } as never); + + const result = await UserProvision.run(['--json']); + const badUser = result.users.find((u) => u.key.includes('bad')); + const goodUser = result.users.find((u) => u.key.includes('good')); + expect(badUser?.status).to.equal('failed'); + expect(badUser?.errors.join(' ')).to.include('Profile'); + expect(goodUser?.status).to.equal('created'); + }); + + it('unresolved user profile (no persona profile) reports one error, not a duplicate ProfileId-missing', async () => { + const fakeConn = createFakeConnection(); + const dir = mkdtempSync(join(tmpdir(), 'jawn-provision-test-')); + const usersPath = join(dir, 'users-unresolved-noprofile.json'); + const personasPath = join(dir, 'personas-unresolved-noprofile.json'); + writeFileSync( + usersPath, + JSON.stringify({ + users: [ + { + personas: ['base'], + profile: 'DoesNotExist', + Username: 'bad@example.test', + LastName: 'Bad', + Alias: 'bad', + TimeZoneSidKey: 'America/Los_Angeles', + LocaleSidKey: 'en_US', + EmailEncodingKey: 'UTF-8', + LanguageLocaleKey: 'en_US', + }, + ], + }) + ); + // Persona deliberately has NO profile, so the only profile source is the (unresolved) user ref. + writeFileSync(personasPath, JSON.stringify({ personas: { base: { permissionSets: [] } } })); + fakeConn.query.callsFake(async (soql: string) => { + if (soql.includes('FROM Profile')) return { records: [] }; + if (soql.includes('FROM UserLogin')) return { records: [] }; + if (soql.includes('FROM PermissionSetAssignment')) return { records: [] }; + if (soql.includes('FROM GroupMember')) return { records: [] }; + return { records: [] }; + }); + sinon.stub(UserProvision.prototype as unknown as Record, 'parse').resolves({ + flags: { + 'target-org': { getConnection: () => fakeConn }, + 'users-def': usersPath, + 'personas-def': personasPath, + 'external-id': undefined, + 'no-prompt': true, + 'dry-run': false, + 'api-version': undefined, + }, + } as never); + + const result = await UserProvision.run(['--json']); + const badUser = result.users[0]; + expect(badUser.status).to.equal('failed'); + expect(badUser.errors.join(' ')).to.include('Profile'); + // The unresolved reference is the single root cause; ProfileId should NOT also be flagged as missing. + expect(badUser.errors.join(' ')).to.not.include('Missing required fields'); + }); + + it('passes through permission-flag user fields, with user value winning over persona', async () => { + const fakeConn = createFakeConnection(); + const dir = mkdtempSync(join(tmpdir(), 'jawn-provision-test-')); + const usersPath = join(dir, 'users-permflag.json'); + const personasPath = join(dir, 'personas-permflag.json'); + writeFileSync( + usersPath, + JSON.stringify({ + users: [ + { + personas: ['base'], + UserPermissionKnowledgeUser: true, + Username: 'kuser@example.test', + LastName: 'Knowledge', + Alias: 'kuser', + TimeZoneSidKey: 'America/Los_Angeles', + LocaleSidKey: 'en_US', + EmailEncodingKey: 'UTF-8', + LanguageLocaleKey: 'en_US', + }, + ], + }) + ); + // Persona sets the flag false via userAttributes; the user-level true must win. + writeFileSync( + personasPath, + JSON.stringify({ + personas: { base: { profile: 'Admin', userAttributes: { UserPermissionKnowledgeUser: false } } }, + }) + ); + fakeConn.query.callsFake(async (soql: string) => { + if (soql.includes('FROM Profile')) return { records: [{ Id: '00exx0000000001AAA', Name: 'Admin' }] }; + if (soql.includes('FROM UserLogin')) return { records: [] }; + if (soql.includes('FROM PermissionSetAssignment')) return { records: [] }; + if (soql.includes('FROM GroupMember')) return { records: [] }; + return { records: [] }; + }); + sinon.stub(UserProvision.prototype as unknown as Record, 'parse').resolves({ + flags: { + 'target-org': { getConnection: () => fakeConn }, + 'users-def': usersPath, + 'personas-def': personasPath, + 'external-id': undefined, + 'no-prompt': true, + 'dry-run': false, + 'api-version': undefined, + }, + } as never); + + const result = await UserProvision.run(['--json']); + expect(result.users[0].status).to.equal('created'); + const createCall = fakeConn.sobjectMap.User.create.firstCall; + const createdUser = (createCall.args[0] as Array>)[0]; + expect(createdUser.UserPermissionKnowledgeUser).to.equal(true); + }); + + it('surfaces per-user errors in human (non-json) output', async () => { + const fakeConn = createFakeConnection(); + const dir = mkdtempSync(join(tmpdir(), 'jawn-provision-test-')); + const usersPath = join(dir, 'users-human-errors.json'); + const personasPath = join(dir, 'personas-human-errors.json'); + writeFileSync( + usersPath, + JSON.stringify({ + users: [ + { + personas: ['a', 'b'], + Username: 'conflict@example.test', + LastName: 'Conflict', + Alias: 'cflt', + TimeZoneSidKey: 'America/Los_Angeles', + LocaleSidKey: 'en_US', + EmailEncodingKey: 'UTF-8', + LanguageLocaleKey: 'en_US', + }, + ], + }) + ); + writeFileSync(personasPath, JSON.stringify({ personas: { a: { profile: 'Admin' }, b: { profile: 'Standard' } } })); + fakeConn.query.callsFake(async (soql: string) => { + if (soql.includes('FROM Profile')) + return { + records: [ + { Id: '00exx0000000001AAA', Name: 'Admin' }, + { Id: '00exx0000000002AAA', Name: 'Standard' }, + ], + }; + return { records: [] }; + }); + sinon.stub(UserProvision.prototype as unknown as Record, 'parse').resolves({ + flags: { + 'target-org': { getConnection: () => fakeConn }, + 'users-def': usersPath, + 'personas-def': personasPath, + 'external-id': undefined, + 'no-prompt': true, + 'dry-run': false, + 'api-version': undefined, + }, + } as never); + + // No --json: per-user failures must be surfaced as warnings, not hidden behind the summary line. + await UserProvision.run([]); + const warned = sfCommandStubs.warn.getCalls().map((c) => String(c.args[0])); + expect(warned.some((m) => m.includes('conflict@example.test') && m.toLowerCase().includes('failed'))).to.equal( + true + ); + expect(warned.some((m) => m.includes('Personas conflict on profile'))).to.equal(true); + }); + + it('legacy persona key produces migration error per user', async () => { + const fakeConn = createFakeConnection(); + const dir = mkdtempSync(join(tmpdir(), 'jawn-provision-test-')); + const usersPath = join(dir, 'users-legacy.json'); + const personasPath = join(dir, 'personas-legacy.json'); + writeFileSync( + usersPath, + JSON.stringify({ + users: [ + { + persona: 'default', + Username: 'u@example.test', + LastName: 'U', + Alias: 'u', + TimeZoneSidKey: 'America/Los_Angeles', + LocaleSidKey: 'en_US', + EmailEncodingKey: 'UTF-8', + LanguageLocaleKey: 'en_US', + }, + ], + }) + ); + writeFileSync(personasPath, JSON.stringify({ personas: { default: { profile: 'Admin' } } })); + fakeConn.query.callsFake(async (soql: string) => { + if (soql.includes('FROM Profile')) return { records: [{ Id: '00exx0000000001AAA', Name: 'Admin' }] }; + return { records: [] }; + }); + sinon.stub(UserProvision.prototype as unknown as Record, 'parse').resolves({ + flags: { + 'target-org': { getConnection: () => fakeConn }, + 'users-def': usersPath, + 'personas-def': personasPath, + 'external-id': undefined, + 'no-prompt': true, + 'dry-run': false, + 'api-version': undefined, + }, + } as never); + + const result = await UserProvision.run(['--json']); + expect(result.users[0].status).to.equal('failed'); + expect(result.users[0].errors.join(' ')).to.include('persona'); + expect(result.users[0].errors.join(' ')).to.include('personas'); + }); }); diff --git a/test/fixtures/user-def.json b/test/fixtures/user-def.json index aea61ae..8fd85a1 100644 --- a/test/fixtures/user-def.json +++ b/test/fixtures/user-def.json @@ -1,22 +1,22 @@ { "users": [ { + "personas": ["admin"], "username": "jdoe@email.com.mySalesforceOrg", - "persona": "admin", "firstName": "John", "lastName": "Doe", "email": "jdoe@email.com" }, { "FederationIdentifier": "ABCD1234", - "persona": "default", + "personas": ["default"], "firstName": "Jane", "lastName": "Smith", "email": "jsmith@email.com" }, { "FederationIdentifier": "ABCD1235", - "persona": "default" + "personas": ["default", "admin"] } ] } diff --git a/test/userProvisioning/planner.test.ts b/test/userProvisioning/planner.test.ts index ddb81d8..08b86ce 100644 --- a/test/userProvisioning/planner.test.ts +++ b/test/userProvisioning/planner.test.ts @@ -1,8 +1,12 @@ import { expect } from 'chai'; import { + buildDefaultAlias, + buildDefaultUsername, buildFieldMap, canonicalizeFieldObject, + deriveMyDomain, isSalesforceId, + mergePersonas, mergeUserFields, missingRequiredFieldsForInsert, normalizeMode, @@ -15,10 +19,14 @@ import { describe('userProvisioning planner', () => { const fieldMap = buildFieldMap([ { name: 'Username', createable: true, updateable: true }, - { name: 'FederationIdentifier', createable: true, updateable: true }, + { name: 'FederationIdentifier', createable: true, updateable: true, externalId: true }, { name: 'FirstName', createable: true, updateable: true }, { name: 'LastName', createable: true, updateable: true }, { name: 'Email', createable: true, updateable: true }, + { name: 'Alias', createable: true, updateable: true }, + { name: 'Title', createable: true, updateable: true }, + { name: 'ProfileId', createable: true, updateable: true }, + { name: 'UserRoleId', createable: true, updateable: true }, ]); it('canonicalizes user fields case-insensitively', () => { @@ -48,12 +56,6 @@ describe('userProvisioning planner', () => { ); }); - it('rejects unknown persona on user records', () => { - expect(() => - validateAndCanonicalizeUsers([{ persona: 'missing', username: 'x' }], { admin: {} }, fieldMap) - ).to.throw('Unknown persona'); - }); - it('validates external id field', () => { expect(() => validateExternalIdField('FederationIdentifier', fieldMap)).not.to.throw(); expect(() => validateExternalIdField('Email', fieldMap)).not.to.throw(); @@ -63,18 +65,18 @@ describe('userProvisioning planner', () => { it('canonicalizes and merges users with persona defaults', () => { const users = validateAndCanonicalizeUsers( - [{ persona: 'admin', username: 'u1', firstname: 'Jane', lastname: 'User' }], + [{ personas: ['admin'], username: 'u1', firstname: 'Jane', lastname: 'User' }], { admin: { userAttributes: { LastName: 'Persona' } } }, fieldMap ); expect(users[0].fields.LastName).to.equal('User'); expect(users[0].fields.Username).to.equal('u1'); - expect(users[0].persona).to.equal('admin'); + expect(users[0].personas).to.deep.equal(['admin']); }); it('canonicalizes per-user match fields case-insensitively', () => { const users = validateAndCanonicalizeUsers( - [{ persona: 'admin', match: 'federationidentifier', FederationIdentifier: 'A1', username: 'u1' }], + [{ personas: ['admin'], match: 'federationidentifier', FederationIdentifier: 'A1', username: 'u1' }], { admin: {} }, fieldMap ); @@ -83,7 +85,11 @@ describe('userProvisioning planner', () => { }); it('records an error for unknown per-user match fields', () => { - const users = validateAndCanonicalizeUsers([{ persona: 'admin', match: 'DoesNotExist' }], { admin: {} }, fieldMap); + const users = validateAndCanonicalizeUsers( + [{ personas: ['admin'], match: 'DoesNotExist' }], + { admin: {} }, + fieldMap + ); expect(users[0].validationErrors?.[0]).to.deep.equal({ messageKey: 'errorInvalidUserMatchField', messageArgs: ['DoesNotExist'], @@ -92,7 +98,7 @@ describe('userProvisioning planner', () => { it('records an error for non-allowed per-user match fields', () => { const users = validateAndCanonicalizeUsers( - [{ persona: 'admin', match: 'LastName', LastName: 'User' }], + [{ personas: ['admin'], match: 'LastName', LastName: 'User' }], { admin: {} }, fieldMap ); @@ -104,7 +110,7 @@ describe('userProvisioning planner', () => { it('records an error when the matched field is empty', () => { const users = validateAndCanonicalizeUsers( - [{ persona: 'admin', match: 'FederationIdentifier', FederationIdentifier: '' }], + [{ personas: ['admin'], match: 'FederationIdentifier', FederationIdentifier: '' }], { admin: {} }, fieldMap ); @@ -116,7 +122,7 @@ describe('userProvisioning planner', () => { }); it('leaves matchField undefined when match is absent', () => { - const users = validateAndCanonicalizeUsers([{ persona: 'admin', username: 'u1' }], { admin: {} }, fieldMap); + const users = validateAndCanonicalizeUsers([{ personas: ['admin'], username: 'u1' }], { admin: {} }, fieldMap); expect(users[0].matchField).to.equal(undefined); }); @@ -126,14 +132,328 @@ describe('userProvisioning planner', () => { Username: 'u', LastName: 'ln', }, - {} + false ); expect(missing).to.include('Alias'); expect(missing).to.include('ProfileId'); }); + it('does not flag ProfileId missing when a profile was intended', () => { + const missing = missingRequiredFieldsForInsert({ Username: 'u', LastName: 'ln' }, true); + expect(missing).to.not.include('ProfileId'); + }); + it('treats only base62 15 or 18 char as ids', () => { expect(isSalesforceId('005000000000001')).to.equal(true); expect(isSalesforceId('Admin_Permissions')).to.equal(false); }); + + // --- errorNoPersonas / errorLegacyPersonaKey --- + + it('records errorNoPersonas when personas is missing', () => { + const users = validateAndCanonicalizeUsers([{ username: 'u1' }], { admin: {} }, fieldMap); + expect(users[0].validationErrors?.[0].messageKey).to.equal('errorNoPersonas'); + }); + + it('records errorNoPersonas when personas is empty array', () => { + const users = validateAndCanonicalizeUsers([{ personas: [], username: 'u1' }], { admin: {} }, fieldMap); + expect(users[0].validationErrors?.[0].messageKey).to.equal('errorNoPersonas'); + }); + + it('records errorLegacyPersonaKey when the old persona string key is present', () => { + const users = validateAndCanonicalizeUsers([{ persona: 'admin', username: 'u1' }], { admin: {} }, fieldMap); + expect(users[0].validationErrors?.[0].messageKey).to.equal('errorLegacyPersonaKey'); + }); + + it('records errorLegacyPersonaKey for capitalized variant Persona key (not a whole-run abort)', () => { + const users = validateAndCanonicalizeUsers([{ Persona: 'admin', username: 'u1' }], { admin: {} }, fieldMap); + expect(users[0].validationErrors?.[0].messageKey).to.equal('errorLegacyPersonaKey'); + }); + + it('records errorUnknownPersona for a missing persona name', () => { + const users = validateAndCanonicalizeUsers([{ personas: ['missing'] }], { admin: {} }, fieldMap); + expect(users[0].validationErrors?.some((e) => e.messageKey === 'errorUnknownPersona')).to.equal(true); + }); + + // --- mergePersonas: assignment list union --- + + it('unions assignment lists across personas and deduplicates', () => { + const { effective, errors } = mergePersonas( + ['a', 'b'], + { + a: { permissionSets: ['PS1', 'PS2'], publicGroups: ['G1'] }, + b: { permissionSets: ['PS2', 'PS3'], publicGroups: ['G2'] }, + }, + {}, + fieldMap + ); + expect(errors).to.have.length(0); + expect(effective.permissionSets).to.deep.equal(['PS1', 'PS2', 'PS3']); + expect(effective.publicGroups).to.deep.equal(['G1', 'G2']); + }); + + // --- mergePersonas: profile/role --- + + it('accepts agreed profile across personas', () => { + const { effective, errors } = mergePersonas( + ['a', 'b'], + { a: { profile: 'Admin' }, b: { profile: 'Admin' } }, + {}, + fieldMap + ); + expect(errors).to.have.length(0); + expect(effective.profile).to.equal('Admin'); + }); + + it('records errorPersonaConflictProfile when profiles differ', () => { + const { errors } = mergePersonas(['a', 'b'], { a: { profile: 'Admin' }, b: { profile: 'Standard' } }, {}, fieldMap); + expect(errors.some((e) => e.messageKey === 'errorPersonaConflictProfile')).to.equal(true); + }); + + it('records errorPersonaConflictRole when roles differ', () => { + const { errors } = mergePersonas(['a', 'b'], { a: { role: 'CEO' }, b: { role: 'CFO' } }, {}, fieldMap); + expect(errors.some((e) => e.messageKey === 'errorPersonaConflictRole')).to.equal(true); + }); + + // --- mergePersonas: userAttributes --- + + it('records errorPersonaConflictUserAttribute for differing attribute values', () => { + const { errors } = mergePersonas( + ['a', 'b'], + { a: { userAttributes: { Title: 'Dev' } }, b: { userAttributes: { Title: 'Admin' } } }, + {}, + fieldMap + ); + expect(errors.some((e) => e.messageKey === 'errorPersonaConflictUserAttribute')).to.equal(true); + }); + + it('suppresses userAttribute conflict when user overrides that key', () => { + const { errors } = mergePersonas( + ['a', 'b'], + { a: { userAttributes: { Title: 'Dev' } }, b: { userAttributes: { Title: 'Admin' } } }, + { Title: 'Manager' }, + fieldMap + ); + expect(errors.some((e) => e.messageKey === 'errorPersonaConflictUserAttribute')).to.equal(false); + }); + + it('detects conflict when persona attributes use different cases for the same canonical field', () => { + const { errors } = mergePersonas( + ['a', 'b'], + { a: { userAttributes: { Title: 'Dev' } }, b: { userAttributes: { title: 'Admin' } } }, + {}, + fieldMap + ); + expect(errors.some((e) => e.messageKey === 'errorPersonaConflictUserAttribute')).to.equal(true); + }); + + it('suppresses case-variant userAttribute conflict when user overrides the canonical key', () => { + const { errors } = mergePersonas( + ['a', 'b'], + { a: { userAttributes: { Title: 'Dev' } }, b: { userAttributes: { title: 'Admin' } } }, + { Title: 'Manager' }, + fieldMap + ); + expect(errors.some((e) => e.messageKey === 'errorPersonaConflictUserAttribute')).to.equal(false); + }); + + // --- mergePersonas: modes --- + + it('accepts agreed mode across personas', () => { + const { effective, errors } = mergePersonas( + ['a', 'b'], + { a: { permissionSetMode: 'sync' }, b: { permissionSetMode: 'sync' } }, + {}, + fieldMap + ); + expect(errors).to.have.length(0); + expect(effective.permissionSetMode).to.equal('sync'); + }); + + it('records errorPersonaConflictMode when modes differ', () => { + const { errors } = mergePersonas( + ['a', 'b'], + { a: { permissionSetMode: 'sync' }, b: { permissionSetMode: 'additive' } }, + {}, + fieldMap + ); + expect(errors.some((e) => e.messageKey === 'errorPersonaConflictMode')).to.equal(true); + }); + + // --- mergePersonas: profile/role override suppression --- + + it('suppresses errorPersonaConflictProfile when hasProfileOverride is true', () => { + const { errors } = mergePersonas( + ['a', 'b'], + { a: { profile: 'Admin' }, b: { profile: 'Standard' } }, + {}, + fieldMap, + true + ); + expect(errors.some((e) => e.messageKey === 'errorPersonaConflictProfile')).to.equal(false); + }); + + it('suppresses errorPersonaConflictRole when hasRoleOverride is true', () => { + const { errors } = mergePersonas(['a', 'b'], { a: { role: 'CEO' }, b: { role: 'CFO' } }, {}, fieldMap, false, true); + expect(errors.some((e) => e.messageKey === 'errorPersonaConflictRole')).to.equal(false); + }); + + // --- user-level profile/role meta keys --- + + it('extracts profileRef from user-level profile key', () => { + const users = validateAndCanonicalizeUsers( + [{ personas: ['admin'], profile: 'Custom Profile' }], + { admin: {} }, + fieldMap + ); + expect(users[0].profileRef).to.equal('Custom Profile'); + expect(users[0].validationErrors).to.equal(undefined); + }); + + it('extracts roleRef from user-level role key', () => { + const users = validateAndCanonicalizeUsers([{ personas: ['admin'], role: 'SalesRep' }], { admin: {} }, fieldMap); + expect(users[0].roleRef).to.equal('SalesRep'); + expect(users[0].validationErrors).to.equal(undefined); + }); + + it('records errorUserProfileConflict when profile and ProfileId are both set', () => { + const users = validateAndCanonicalizeUsers( + [{ personas: ['admin'], profile: 'Admin', ProfileId: '00exx0000000001AAA' }], + { admin: {} }, + fieldMap + ); + expect(users[0].validationErrors?.some((e) => e.messageKey === 'errorUserProfileConflict')).to.equal(true); + }); + + it('records errorUserRoleConflict when role and UserRoleId are both set', () => { + const users = validateAndCanonicalizeUsers( + [{ personas: ['admin'], role: 'CEO', UserRoleId: '00Exx0000000001AAA' }], + { admin: {} }, + fieldMap + ); + expect(users[0].validationErrors?.some((e) => e.messageKey === 'errorUserRoleConflict')).to.equal(true); + }); + + it('profile key suppresses persona profile conflict (via validateAndCanonicalizeUsers)', () => { + const users = validateAndCanonicalizeUsers( + [{ personas: ['a', 'b'], profile: 'Override' }], + { a: { profile: 'Admin' }, b: { profile: 'Standard' } }, + fieldMap + ); + expect(users[0].profileRef).to.equal('Override'); + expect(users[0].validationErrors?.some((e) => e.messageKey === 'errorPersonaConflictProfile') ?? false).to.equal( + false + ); + }); + + it('raw ProfileId suppresses persona profile conflict (via validateAndCanonicalizeUsers)', () => { + const users = validateAndCanonicalizeUsers( + [{ personas: ['a', 'b'], ProfileId: '00exx0000000009AAA' }], + { a: { profile: 'Admin' }, b: { profile: 'Standard' } }, + fieldMap + ); + expect(users[0].validationErrors?.some((e) => e.messageKey === 'errorPersonaConflictProfile') ?? false).to.equal( + false + ); + }); + + it('raw UserRoleId suppresses persona role conflict (via validateAndCanonicalizeUsers)', () => { + const users = validateAndCanonicalizeUsers( + [{ personas: ['a', 'b'], UserRoleId: '00Exx0000000009AAA' }], + { a: { role: 'CEO' }, b: { role: 'CFO' } }, + fieldMap + ); + expect(users[0].validationErrors?.some((e) => e.messageKey === 'errorPersonaConflictRole') ?? false).to.equal( + false + ); + }); + + it('records errorInvalidUserProfile when profile is not a string', () => { + const users = validateAndCanonicalizeUsers([{ personas: ['admin'], profile: ['Admin'] }], { admin: {} }, fieldMap); + expect(users[0].validationErrors?.some((e) => e.messageKey === 'errorInvalidUserProfile')).to.equal(true); + expect(users[0].profileRef).to.equal(undefined); + }); + + it('records errorInvalidUserRole when role is not a string', () => { + const users = validateAndCanonicalizeUsers([{ personas: ['admin'], role: 42 }], { admin: {} }, fieldMap); + expect(users[0].validationErrors?.some((e) => e.messageKey === 'errorInvalidUserRole')).to.equal(true); + expect(users[0].roleRef).to.equal(undefined); + }); + + it('treats an empty-string profile as absent (no override, no error)', () => { + const users = validateAndCanonicalizeUsers( + [{ personas: ['a', 'b'], profile: ' ' }], + { a: { profile: 'Admin' }, b: { profile: 'Standard' } }, + fieldMap + ); + expect(users[0].profileRef).to.equal(undefined); + expect(users[0].validationErrors?.some((e) => e.messageKey === 'errorInvalidUserProfile') ?? false).to.equal(false); + // With no real override, the persona profile conflict is NOT suppressed. + expect(users[0].validationErrors?.some((e) => e.messageKey === 'errorPersonaConflictProfile')).to.equal(true); + }); + + // --- buildDefaultAlias --- + + it('buildDefaultAlias: John/Doe -> johdoe', () => { + expect(buildDefaultAlias('John', 'Doe')).to.equal('johdoe'); + }); + + it('buildDefaultAlias: Jo/Anderson -> joande', () => { + expect(buildDefaultAlias('Jo', 'Anderson')).to.equal('joande'); + }); + + it('buildDefaultAlias: Al/Bo -> albo (best-effort 4)', () => { + expect(buildDefaultAlias('Al', 'Bo')).to.equal('albo'); + }); + + it('buildDefaultAlias: A/Anderson -> aander', () => { + expect(buildDefaultAlias('A', 'Anderson')).to.equal('aander'); + }); + + it('buildDefaultAlias: missing first + Anderson -> anders', () => { + expect(buildDefaultAlias(undefined, 'Anderson')).to.equal('anders'); + }); + + it('buildDefaultAlias: both missing -> undefined', () => { + expect(buildDefaultAlias(undefined, undefined)).to.equal(undefined); + }); + + it('buildDefaultAlias: strips non-alphanumeric characters', () => { + const alias = buildDefaultAlias('Anne-Marie', "O'Neil"); + expect(alias).to.match(/^[a-z0-9]+$/); + expect(alias!.length).to.be.lessThanOrEqual(8); + }); + + it('buildDefaultAlias: long names truncate at 8', () => { + const alias = buildDefaultAlias('Alexander', 'Thompson'); + expect(alias!.length).to.be.lessThanOrEqual(8); + }); + + // --- buildDefaultUsername --- + + it('buildDefaultUsername: produces email.myDomain', () => { + expect(buildDefaultUsername('user@example.com', 'myorg')).to.equal('user@example.com.myorg'); + }); + + it('buildDefaultUsername: empty email -> undefined', () => { + expect(buildDefaultUsername('', 'myorg')).to.equal(undefined); + }); + + it('buildDefaultUsername: non-string email -> undefined', () => { + expect(buildDefaultUsername(null, 'myorg')).to.equal(undefined); + }); + + // --- deriveMyDomain --- + + it('deriveMyDomain: prod My Domain -> first label', () => { + expect(deriveMyDomain('https://mycompany.my.salesforce.com')).to.equal('mycompany'); + }); + + it('deriveMyDomain: sandbox host -> name--sbx', () => { + expect(deriveMyDomain('https://mycompany--sbx.sandbox.my.salesforce.com')).to.equal('mycompany--sbx'); + }); + + it('deriveMyDomain: invalid URL -> undefined', () => { + expect(deriveMyDomain('not-a-url')).to.equal(undefined); + }); }); diff --git a/yarn.lock b/yarn.lock index db8876f..ccad58e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -66,7 +66,7 @@ "@smithy/util-utf8" "^2.0.0" tslib "^2.6.2" -"@aws-crypto/sha256-js@^5.2.0", "@aws-crypto/sha256-js@5.2.0": +"@aws-crypto/sha256-js@5.2.0", "@aws-crypto/sha256-js@^5.2.0": version "5.2.0" resolved "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz" integrity sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA== @@ -205,7 +205,7 @@ "@smithy/util-waiter" "^3.1.2" tslib "^2.6.2" -"@aws-sdk/client-sso-oidc@^3.614.0", "@aws-sdk/client-sso-oidc@3.614.0": +"@aws-sdk/client-sso-oidc@3.614.0": version "3.614.0" resolved "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.614.0.tgz" integrity sha512-BI1NWcpppbHg/28zbUg54dZeckork8BItZIcjls12vxasy+p3iEzrJVG60jcbUTTsk3Qc1tyxNfrdcVqx0y7Ww== @@ -294,7 +294,7 @@ "@smithy/util-utf8" "^3.0.0" tslib "^2.6.2" -"@aws-sdk/client-sts@^3.609.0", "@aws-sdk/client-sts@^3.614.0", "@aws-sdk/client-sts@3.614.0": +"@aws-sdk/client-sts@3.614.0": version "3.614.0" resolved "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.614.0.tgz" integrity sha512-i6QmaVA1KHHYNnI2VYQy/sc31rLm4+jSp8b/YbQpFnD0w3aXsrEEHHlxek45uSkHb4Nrj1omFBVy/xp1WVYx2Q== @@ -605,7 +605,7 @@ "@smithy/types" "^3.3.0" tslib "^2.6.2" -"@aws-sdk/types@^3.222.0", "@aws-sdk/types@3.609.0": +"@aws-sdk/types@3.609.0", "@aws-sdk/types@^3.222.0": version "3.609.0" resolved "https://registry.npmjs.org/@aws-sdk/types/-/types-3.609.0.tgz" integrity sha512-+Tqnh9w0h2LcrUsdXyT1F8mNhXz+tVYBtP19LpeEGntmvHwa2XzvLUCWpoIAIVsHp5+HdB2X9Sn0KAtmbFXc2Q== @@ -678,7 +678,7 @@ resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.19.1.tgz" integrity sha512-72a9ghR0gnESIa7jBN53U32FOVCEoztyIlKaNoU05zRhEecduGK9L9c3ww7Mp06JiR+0ls0GBPFJQwwtjn9ksg== -"@babel/core@^7.0.0", "@babel/core@^7.7.5": +"@babel/core@^7.7.5": version "7.19.1" resolved "https://registry.npmjs.org/@babel/core/-/core-7.19.1.tgz" integrity sha512-1H8VgqXme4UXCRv7/Wa1bq7RVymKOzC7znjyFM8KiEzwFqcKUKYNoQef4GhdklgNvoBXyW4gYhuBNCM5o1zImw== @@ -1211,14 +1211,6 @@ resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz" integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== -"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.9": - version "0.3.15" - resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz" - integrity sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g== - dependencies: - "@jridgewell/resolve-uri" "^3.0.3" - "@jridgewell/sourcemap-codec" "^1.4.10" - "@jridgewell/trace-mapping@0.3.9": version "0.3.9" resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz" @@ -1227,6 +1219,14 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" +"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.9": + version "0.3.15" + resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz" + integrity sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jsforce/jsforce-node@^3.2.0", "@jsforce/jsforce-node@^3.6.1": version "3.6.2" resolved "https://registry.npmjs.org/@jsforce/jsforce-node/-/jsforce-node-3.6.2.tgz" @@ -1251,7 +1251,7 @@ "@nodelib/fs.stat" "2.0.5" run-parallel "^1.1.9" -"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5": +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": version "2.0.5" resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== @@ -1264,30 +1264,6 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@oclif/core@^4", "@oclif/core@^4.0.27": - version "4.0.31" - resolved "https://registry.npmjs.org/@oclif/core/-/core-4.0.31.tgz" - integrity sha512-7oyIZv/C1TP+fPc2tSzVPYqG1zU+nel1QvJxjAWyVhud0J8B5SpKZnryedxs3nlSVPJ6K1MT31C9esupCBYgZw== - dependencies: - ansi-escapes "^4.3.2" - ansis "^3.3.2" - clean-stack "^3.0.1" - cli-spinners "^2.9.2" - debug "^4.3.7" - ejs "^3.1.10" - get-package-type "^0.1.0" - globby "^11.1.0" - indent-string "^4.0.0" - is-wsl "^2.2.0" - lilconfig "^3.1.2" - minimatch "^9.0.5" - semver "^7.6.3" - string-width "^4.2.3" - supports-color "^8" - widest-line "^3.1.0" - wordwrap "^1.0.0" - wrap-ansi "^7.0.0" - "@oclif/core@3.26.6": version "3.26.6" resolved "https://registry.npmjs.org/@oclif/core/-/core-3.26.6.tgz" @@ -1322,6 +1298,30 @@ wordwrap "^1.0.0" wrap-ansi "^7.0.0" +"@oclif/core@^4", "@oclif/core@^4.0.27": + version "4.0.31" + resolved "https://registry.npmjs.org/@oclif/core/-/core-4.0.31.tgz" + integrity sha512-7oyIZv/C1TP+fPc2tSzVPYqG1zU+nel1QvJxjAWyVhud0J8B5SpKZnryedxs3nlSVPJ6K1MT31C9esupCBYgZw== + dependencies: + ansi-escapes "^4.3.2" + ansis "^3.3.2" + clean-stack "^3.0.1" + cli-spinners "^2.9.2" + debug "^4.3.7" + ejs "^3.1.10" + get-package-type "^0.1.0" + globby "^11.1.0" + indent-string "^4.0.0" + is-wsl "^2.2.0" + lilconfig "^3.1.2" + minimatch "^9.0.5" + semver "^7.6.3" + string-width "^4.2.3" + supports-color "^8" + widest-line "^3.1.0" + wordwrap "^1.0.0" + wrap-ansi "^7.0.0" + "@oclif/plugin-command-snapshot@^5.1.9": version "5.1.9" resolved "https://registry.npmjs.org/@oclif/plugin-command-snapshot/-/plugin-command-snapshot-5.1.9.tgz" @@ -1403,31 +1403,7 @@ strip-ansi "6.0.1" ts-retry-promise "^0.8.1" -"@salesforce/core@^7.3.12": - version "7.4.0" - resolved "https://registry.npmjs.org/@salesforce/core/-/core-7.4.0.tgz" - integrity sha512-wn3fJnpG8h493qKR52Il3xgRbXg4zwbV34wonf/mTZP3n5rJeC/7S69249CGN155Os6Lds5rEUo3pGJFwgCBdQ== - dependencies: - "@jsforce/jsforce-node" "^3.2.0" - "@salesforce/kit" "^3.1.2" - "@salesforce/schemas" "^1.9.0" - "@salesforce/ts-types" "^2.0.9" - ajv "^8.15.0" - change-case "^4.1.2" - fast-levenshtein "^3.0.0" - faye "^1.4.0" - form-data "^4.0.0" - js2xmlparser "^4.0.1" - jsonwebtoken "9.0.2" - jszip "3.10.1" - pino "^8.21.0" - pino-abstract-transport "^1.2.0" - pino-pretty "^10.3.1" - proper-lockfile "^4.1.2" - semver "^7.6.2" - ts-retry-promise "^0.8.1" - -"@salesforce/core@^7.3.9": +"@salesforce/core@^7.3.12", "@salesforce/core@^7.3.9": version "7.4.0" resolved "https://registry.npmjs.org/@salesforce/core/-/core-7.4.0.tgz" integrity sha512-wn3fJnpG8h493qKR52Il3xgRbXg4zwbV34wonf/mTZP3n5rJeC/7S69249CGN155Os6Lds5rEUo3pGJFwgCBdQ== @@ -1593,14 +1569,7 @@ dependencies: "@sinonjs/commons" "^3.0.0" -"@sinonjs/fake-timers@^6.0.0": - version "6.0.1" - resolved "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz" - integrity sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA== - dependencies: - "@sinonjs/commons" "^1.7.0" - -"@sinonjs/fake-timers@^6.0.1": +"@sinonjs/fake-timers@^6.0.0", "@sinonjs/fake-timers@^6.0.1": version "6.0.1" resolved "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz" integrity sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA== @@ -1625,13 +1594,10 @@ lodash.get "^4.4.2" type-detect "^4.0.8" -"@sinonjs/text-encoding@^0.7.1": - version "0.7.2" - resolved "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz" - integrity sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ== - -"@sinonjs/text-encoding@^0.7.2": +"@sinonjs/text-encoding@^0.7.1", "@sinonjs/text-encoding@^0.7.2": version "0.7.3" + resolved "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz" + integrity sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA== "@smithy/abort-controller@^3.1.1": version "3.1.1" @@ -2228,6 +2194,11 @@ dependencies: undici-types "~6.19.8" +"@types/node@20.5.1": + version "20.5.1" + resolved "https://registry.npmjs.org/@types/node/-/node-20.5.1.tgz" + integrity sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg== + "@types/node@^18.19.32": version "18.19.34" resolved "https://registry.npmjs.org/@types/node/-/node-18.19.34.tgz" @@ -2235,11 +2206,6 @@ dependencies: undici-types "~5.26.4" -"@types/node@20.5.1": - version "20.5.1" - resolved "https://registry.npmjs.org/@types/node/-/node-20.5.1.tgz" - integrity sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg== - "@types/normalize-package-data@^2.4.0": version "2.4.4" resolved "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz" @@ -2255,7 +2221,7 @@ resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz" integrity sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA== -"@types/react@^18.3.12", "@types/react@>=18.0.0": +"@types/react@^18.3.12": version "18.3.12" resolved "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz" integrity sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw== @@ -2310,7 +2276,7 @@ semver "^7.5.4" ts-api-utils "^1.0.1" -"@typescript-eslint/parser@^6.0.0 || ^6.0.0-alpha", "@typescript-eslint/parser@^6.21.0": +"@typescript-eslint/parser@^6.21.0": version "6.21.0" resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz" integrity sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ== @@ -2358,7 +2324,7 @@ semver "^7.5.4" ts-api-utils "^1.0.1" -"@typescript-eslint/utils@^6.17.0", "@typescript-eslint/utils@6.21.0": +"@typescript-eslint/utils@6.21.0", "@typescript-eslint/utils@^6.17.0": version "6.21.0" resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz" integrity sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ== @@ -2384,6 +2350,14 @@ resolved "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== +JSONStream@^1.3.5: + version "1.3.5" + resolved "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz" + integrity sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ== + dependencies: + jsonparse "^1.2.0" + through ">=2.2.7 <3" + abort-controller@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz" @@ -2401,18 +2375,11 @@ acorn-walk@^8.1.1: resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.0.tgz" integrity sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA== -"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.4.1, acorn@^8.9.0: +acorn@^8.4.1, acorn@^8.9.0: version "8.11.2" resolved "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz" integrity sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w== -agent-base@^7.0.2: - version "7.1.0" - resolved "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz" - integrity sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg== - dependencies: - debug "^4.3.4" - agent-base@6: version "6.0.2" resolved "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz" @@ -2420,6 +2387,13 @@ agent-base@6: dependencies: debug "4" +agent-base@^7.0.2: + version "7.1.0" + resolved "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz" + integrity sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg== + dependencies: + debug "^4.3.4" + aggregate-error@^3.0.0: version "3.1.0" resolved "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz" @@ -2503,17 +2477,7 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0, ansi-styles@^4.3.0: dependencies: color-convert "^2.0.1" -ansi-styles@^6.0.0: - version "6.2.1" - resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz" - integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== - -ansi-styles@^6.1.0: - version "6.2.1" - resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz" - integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== - -ansi-styles@^6.2.1: +ansi-styles@^6.0.0, ansi-styles@^6.1.0, ansi-styles@^6.2.1: version "6.2.1" resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz" integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== @@ -2733,7 +2697,7 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" -brace-expansion@^2.0.1, brace-expansion@^2.0.2: +brace-expansion@^2.0.1: version "2.1.1" resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz" integrity sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA== @@ -2759,7 +2723,7 @@ browser-stdout@1.3.1: resolved "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz" integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== -browserslist@^4.21.3, browserslist@^4.23.0, "browserslist@>= 4.21.0": +browserslist@^4.21.3, browserslist@^4.23.0: version "4.23.0" resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz" integrity sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ== @@ -2929,15 +2893,7 @@ chalk@^2.4.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^4, chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0: - version "4.1.2" - resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" - integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - -chalk@^4.1.2: +chalk@^4, chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.2: version "4.1.2" resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -2945,12 +2901,7 @@ chalk@^4.1.2: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^5.0.0: - version "5.3.0" - resolved "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz" - integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== - -chalk@^5.3.0: +chalk@^5.0.0, chalk@^5.3.0: version "5.3.0" resolved "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz" integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== @@ -2985,7 +2936,7 @@ check-error@^1.0.3: dependencies: get-func-name "^2.0.2" -chokidar@^3.5.3, chokidar@3.5.3: +chokidar@3.5.3, chokidar@^3.5.3: version "3.5.3" resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz" integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== @@ -3109,16 +3060,16 @@ color-convert@^2.0.1: dependencies: color-name "~1.1.4" -color-name@^1.0.0, color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - -color-name@1.1.3: +color-name@1.1.3, color-name@^1.0.0: version "1.1.3" resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + color-string@^1.9.0: version "1.9.1" resolved "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz" @@ -3208,8 +3159,8 @@ conventional-commits-parser@^4.0.0: resolved "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-4.0.0.tgz" integrity sha512-WRv5j1FsVM5FISJkoYMR6tPk07fkKT0UodruX4je86V4owk451yjXAKzKAPOs9l7y59E2viHUS9eQ+dfUA9NSg== dependencies: - is-text-path "^1.0.1" JSONStream "^1.3.5" + is-text-path "^1.0.1" meow "^8.1.2" split2 "^3.2.2" @@ -3247,7 +3198,7 @@ cosmiconfig-typescript-loader@^4.0.0: resolved "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-4.4.0.tgz" integrity sha512-BabizFdC3wBHhbI4kJh0VkQP9GkBfoHPydD0COMce1nJ1kJAB3F2TmJ/I7diULBKtmEWSwEbuN/KDtgnmUUVmw== -cosmiconfig@^8.0.0, cosmiconfig@^8.3.6, cosmiconfig@>=7: +cosmiconfig@^8.0.0, cosmiconfig@^8.3.6: version "8.3.6" resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz" integrity sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA== @@ -3303,14 +3254,7 @@ dateformat@^4.6.3: resolved "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz" integrity sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA== -debug@^3.2.7: - version "3.2.7" - resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz" - integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== - dependencies: - ms "^2.1.1" - -debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.5, debug@^4.3.7, debug@4: +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.5, debug@^4.3.7: version "4.3.7" resolved "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz" integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== @@ -3324,6 +3268,13 @@ debug@4.3.4: dependencies: ms "2.1.2" +debug@^3.2.7: + version "3.2.7" + resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + dependencies: + ms "^2.1.1" + decamelize-keys@^1.1.0: version "1.1.1" resolved "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz" @@ -3406,6 +3357,11 @@ detect-newline@^4.0.0: resolved "https://registry.npmjs.org/detect-newline/-/detect-newline-4.0.1.tgz" integrity sha512-qE3Veg1YXzGHQhlA6jzebZN2qVf6NX+A7m7qlhCGG30dJixrAQhYOsJjsnBjJkCSmuOPpCk30145fr8FV0bzog== +diff@5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz" + integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== + diff@^4.0.1, diff@^4.0.2: version "4.0.2" resolved "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz" @@ -3416,11 +3372,6 @@ diff@^5.2.0: resolved "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz" integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A== -diff@5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz" - integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== - dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz" @@ -3635,6 +3586,11 @@ escape-html@^1.0.3: resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz" integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== +escape-string-regexp@4.0.0, escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz" @@ -3645,11 +3601,6 @@ escape-string-regexp@^2.0.0: resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz" integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== -escape-string-regexp@^4.0.0, escape-string-regexp@4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" - integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== - eslint-config-prettier@^9.1.0: version "9.1.0" resolved "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz" @@ -3783,7 +3734,7 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4 resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== -"eslint@^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8", "eslint@^6.0.0 || ^7.0.0 || >=8.0.0", "eslint@^7.0.0 || ^8.0.0", "eslint@^7.0.0 || ^8.0.0 || ^9.0.0", eslint@^8.56.0, eslint@>=7.0.0, eslint@>=7.7.0, eslint@>=8.56.0: +eslint@^8.56.0: version "8.57.0" resolved "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz" integrity sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ== @@ -4036,15 +3987,7 @@ find-cache-dir@^3.2.0: make-dir "^3.0.2" pkg-dir "^4.1.0" -find-up@^4.0.0, find-up@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz" - integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== - dependencies: - locate-path "^5.0.0" - path-exists "^4.0.0" - -find-up@^5.0.0: +find-up@5.0.0, find-up@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz" integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== @@ -4052,12 +3995,12 @@ find-up@^5.0.0: locate-path "^6.0.0" path-exists "^4.0.0" -find-up@5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz" - integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== +find-up@^4.0.0, find-up@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== dependencies: - locate-path "^6.0.0" + locate-path "^5.0.0" path-exists "^4.0.0" find-yarn-workspace-root@^2.0.0: @@ -4101,15 +4044,7 @@ foreground-child@^2.0.0: cross-spawn "^7.0.0" signal-exit "^3.0.2" -foreground-child@^3.1.0: - version "3.1.1" - resolved "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz" - integrity sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg== - dependencies: - cross-spawn "^7.0.0" - signal-exit "^4.0.1" - -foreground-child@^3.1.1: +foreground-child@^3.1.0, foreground-child@^3.1.1: version "3.3.1" resolved "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz" integrity sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw== @@ -4159,6 +4094,11 @@ fs.realpath@^1.0.0: resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + function-bind@^1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" @@ -4284,6 +4224,17 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" +glob@8.1.0, glob@^8.0.3: + version "8.1.0" + resolved "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz" + integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^5.0.1" + once "^1.3.0" + glob@^10.3.10: version "10.3.10" resolved "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz" @@ -4316,28 +4267,6 @@ glob@^7.0.0, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^8.0.3: - version "8.1.0" - resolved "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz" - integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^5.0.1" - once "^1.3.0" - -glob@8.1.0: - version "8.1.0" - resolved "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz" - integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^5.0.1" - once "^1.3.0" - global-dirs@^0.1.1: version "0.1.1" resolved "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz" @@ -4656,7 +4585,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3, inherits@2: +inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: version "2.0.4" resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -4940,6 +4869,11 @@ is-wsl@^2.2.0: dependencies: is-docker "^2.0.0" +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" + integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ== + isarray@^2.0.5: version "2.0.5" resolved "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz" @@ -4950,11 +4884,6 @@ isarray@~1.0.0: resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== -isarray@0.0.1: - version "0.0.1" - resolved "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" - integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ== - isexe@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" @@ -5049,15 +4978,14 @@ joycon@^3.1.1: resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@^3.13.1: - version "3.14.1" - resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz" - integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== +js-yaml@4.1.0, js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== dependencies: - argparse "^1.0.7" - esprima "^4.0.0" + argparse "^2.0.1" -js-yaml@^3.14.1: +js-yaml@^3.13.1, js-yaml@^3.14.1: version "3.14.1" resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz" integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== @@ -5065,13 +4993,6 @@ js-yaml@^3.14.1: argparse "^1.0.7" esprima "^4.0.0" -js-yaml@^4.1.0, js-yaml@4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz" - integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== - dependencies: - argparse "^2.0.1" - js2xmlparser@^4.0.1: version "4.0.2" resolved "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz" @@ -5167,14 +5088,6 @@ jsonparse@^1.2.0: resolved "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz" integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== -JSONStream@^1.3.5: - version "1.3.5" - resolved "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz" - integrity sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ== - dependencies: - jsonparse "^1.2.0" - through ">=2.2.7 <3" - jsonwebtoken@9.0.2: version "9.0.2" resolved "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz" @@ -5191,7 +5104,7 @@ jsonwebtoken@9.0.2: ms "^2.1.1" semver "^7.5.4" -jszip@^3.10.1, jszip@3.10.1: +jszip@3.10.1, jszip@^3.10.1: version "3.10.1" resolved "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz" integrity sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g== @@ -5495,11 +5408,6 @@ marked@^10.0.0: resolved "https://registry.npmjs.org/marked/-/marked-10.0.0.tgz" integrity sha512-YiGcYcWj50YrwBgNzFoYhQ1hT6GmQbFG8SksnYJX1z4BXTHSOrz1GB5/Jm2yQvMg4nN1FHP4M6r03R10KrVUiA== -marked@^4.2.12: - version "4.3.0" - resolved "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz" - integrity sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A== - marked@^4.3.0: version "4.3.0" resolved "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz" @@ -5582,6 +5490,20 @@ min-indent@^1.0.0: resolved "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz" integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== +minimatch@5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz" + integrity sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g== + dependencies: + brace-expansion "^2.0.1" + +minimatch@9.0.3: + version "9.0.3" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz" + integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== + dependencies: + brace-expansion "^2.0.1" + minimatch@^10.2.2: version "10.2.5" resolved "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz" @@ -5603,55 +5525,13 @@ minimatch@^5.0.1: dependencies: brace-expansion "^2.0.1" -minimatch@^7.1.3: - version "7.4.9" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-7.4.9.tgz" - integrity sha512-Brg/fp/iAVDOQoHxkuN5bEYhyQlZhxddI78yWsCbeEwTHXQjlNLtiJDUsp1GIptVqMI7/gkJMz4vVAc01mpoBw== - dependencies: - brace-expansion "^2.0.2" - -minimatch@^9.0.1: - version "9.0.5" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz" - integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== - dependencies: - brace-expansion "^2.0.1" - -minimatch@^9.0.3: - version "9.0.9" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz" - integrity sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg== - dependencies: - brace-expansion "^2.0.2" - -minimatch@^9.0.4: - version "9.0.5" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz" - integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== - dependencies: - brace-expansion "^2.0.1" - -minimatch@^9.0.5: +minimatch@^9.0.1, minimatch@^9.0.3, minimatch@^9.0.4, minimatch@^9.0.5: version "9.0.5" resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz" integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== dependencies: brace-expansion "^2.0.1" -minimatch@5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz" - integrity sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g== - dependencies: - brace-expansion "^2.0.1" - -minimatch@9.0.3: - version "9.0.3" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz" - integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== - dependencies: - brace-expansion "^2.0.1" - minimist-options@4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz" @@ -5702,16 +5582,16 @@ mri@^1.2.0: resolved "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz" integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA== -ms@^2.1.1, ms@^2.1.3, ms@2.1.3: - version "2.1.3" - resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - ms@2.1.2: version "2.1.2" resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +ms@2.1.3, ms@^2.1.1, ms@^2.1.3: + version "2.1.3" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + multistream@^3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/multistream/-/multistream-3.1.0.tgz" @@ -6303,7 +6183,7 @@ prelude-ls@^1.2.1: resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -prettier@^2.0.0, prettier@^2.8.8: +prettier@^2.8.8: version "2.8.8" resolved "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz" integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== @@ -6415,7 +6295,7 @@ react-reconciler@^0.29.0: loose-envify "^1.1.0" scheduler "^0.23.2" -react@^18.3.1, react@>=18.0.0: +react@^18.3.1: version "18.3.1" resolved "https://registry.npmjs.org/react/-/react-18.3.1.tgz" integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ== @@ -6441,7 +6321,7 @@ read-pkg@^5.2.0: parse-json "^5.0.0" type-fest "^0.6.0" -readable-stream@^3.0.0, readable-stream@^3.4.0, readable-stream@3: +readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.4.0: version "3.6.2" resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz" integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== @@ -6561,17 +6441,17 @@ resolve-alpn@^1.2.0: resolved "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz" integrity sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g== +resolve-from@5.0.0, resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz" integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== -resolve-from@^5.0.0, resolve-from@5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz" - integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== - -resolve-global@^1.0.0, resolve-global@1.0.0: +resolve-global@1.0.0, resolve-global@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/resolve-global/-/resolve-global-1.0.0.tgz" integrity sha512-zFa12V4OLtT5XUX/Q4VLvTfBf+Ok0SPc1FNGM/z9ctUdiU618qwKpWnd0CHs3+RqROfyEg/DhuHbMWYqcgljEw== @@ -6602,16 +6482,16 @@ restore-cursor@^4.0.0: onetime "^5.1.0" signal-exit "^3.0.2" -retry@^0.12.0: - version "0.12.0" - resolved "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz" - integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow== - retry@0.13.1: version "0.13.1" resolved "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz" integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== +retry@^0.12.0: + version "0.12.0" + resolved "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz" + integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow== + reusify@^1.0.4: version "1.0.4" resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz" @@ -6641,7 +6521,7 @@ safe-array-concat@^1.0.1: has-symbols "^1.0.3" isarray "^2.0.5" -safe-buffer@*, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@>=5.1.0, safe-buffer@~5.2.0: +safe-buffer@*, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -6682,26 +6562,6 @@ secure-json-parse@^2.4.0: resolved "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz" integrity sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw== -semver@^6.0.0: - version "6.3.1" - resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" - integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== - -semver@^6.3.0: - version "6.3.1" - resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" - integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== - -semver@^6.3.1: - version "6.3.1" - resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" - integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== - -semver@^7.0.0, semver@^7.3.4, semver@^7.3.5, semver@^7.5.3, semver@^7.5.4, semver@^7.6.0, semver@^7.6.2, semver@^7.6.3: - version "7.6.3" - resolved "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz" - integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== - "semver@2 || 3 || 4 || 5": version "5.7.2" resolved "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz" @@ -6714,6 +6574,16 @@ semver@7.5.4: dependencies: lru-cache "^6.0.0" +semver@^6.0.0, semver@^6.3.0, semver@^6.3.1: + version "6.3.1" + resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +semver@^7.0.0, semver@^7.3.4, semver@^7.3.5, semver@^7.5.3, semver@^7.5.4, semver@^7.6.0, semver@^7.6.2, semver@^7.6.3: + version "7.6.3" + resolved "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== + sentence-case@^3.0.4: version "3.0.4" resolved "https://registry.npmjs.org/sentence-case/-/sentence-case-3.0.4.tgz" @@ -6790,7 +6660,7 @@ shelljs@^0.8.4, shelljs@^0.8.5: interpret "^1.0.0" rechoir "^0.6.2" -shiki@^0.14.1, shiki@^0.14.7: +shiki@^0.14.7: version "0.14.7" resolved "https://registry.npmjs.org/shiki/-/shiki-0.14.7.tgz" integrity sha512-dNPAPrxSc87ua2sKJ3H5dQ/6ZaY8RNnaAqK+t0eG7p0Soi2ydiqbGOTaZCqaYvA/uZYfS1LJnemt3Q+mSfcPCg== @@ -6814,12 +6684,7 @@ signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== -signal-exit@^4.0.1: - version "4.1.0" - resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz" - integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== - -signal-exit@^4.1.0: +signal-exit@^4.0.1, signal-exit@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz" integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== @@ -6831,18 +6696,6 @@ simple-swizzle@^0.2.2: dependencies: is-arrayish "^0.3.1" -sinon@^17.0.2: - version "17.0.2" - resolved "https://registry.npmjs.org/sinon/-/sinon-17.0.2.tgz" - integrity sha512-uihLiaB9FhzesElPDFZA7hDcNABzsVHwr3YfmM9sBllVwab3l0ltGlRV1XhpNfIacNDLGD1QRZNLs5nU5+hTuA== - dependencies: - "@sinonjs/commons" "^3.0.1" - "@sinonjs/fake-timers" "^11.2.2" - "@sinonjs/samsam" "^8.0.0" - diff "^5.2.0" - nise "^5.1.9" - supports-color "^7" - sinon@10.0.0: version "10.0.0" resolved "https://registry.npmjs.org/sinon/-/sinon-10.0.0.tgz" @@ -6855,6 +6708,18 @@ sinon@10.0.0: nise "^4.1.0" supports-color "^7.1.0" +sinon@^17.0.2: + version "17.0.2" + resolved "https://registry.npmjs.org/sinon/-/sinon-17.0.2.tgz" + integrity sha512-uihLiaB9FhzesElPDFZA7hDcNABzsVHwr3YfmM9sBllVwab3l0ltGlRV1XhpNfIacNDLGD1QRZNLs5nU5+hTuA== + dependencies: + "@sinonjs/commons" "^3.0.1" + "@sinonjs/fake-timers" "^11.2.2" + "@sinonjs/samsam" "^8.0.0" + diff "^5.2.0" + nise "^5.1.9" + supports-color "^7" + slash@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" @@ -7024,20 +6889,6 @@ stack-utils@^2.0.6: dependencies: escape-string-regexp "^2.0.0" -string_decoder@^1.1.1, string_decoder@^1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - "string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" @@ -7101,6 +6952,20 @@ string.prototype.trimstart@^1.0.7: define-properties "^1.2.0" es-abstract "^1.22.1" +string_decoder@^1.1.1, string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +string_decoder@^1.3.0: + version "1.3.0" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + "strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" @@ -7108,21 +6973,14 @@ string.prototype.trimstart@^1.0.7: dependencies: ansi-regex "^5.0.1" -strip-ansi@^6.0.0, strip-ansi@^6.0.1, strip-ansi@6.0.1: +strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== dependencies: ansi-regex "^5.0.1" -strip-ansi@^7.0.1: - version "7.1.0" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz" - integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== - dependencies: - ansi-regex "^6.0.1" - -strip-ansi@^7.1.0: +strip-ansi@^7.0.1, strip-ansi@^7.1.0: version "7.1.0" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz" integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== @@ -7151,7 +7009,7 @@ strip-indent@^3.0.0: dependencies: min-indent "^1.0.0" -strip-json-comments@^3.1.1, strip-json-comments@3.1.1: +strip-json-comments@3.1.1, strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== @@ -7161,6 +7019,13 @@ strnum@^1.0.5: resolved "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz" integrity sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA== +supports-color@8.1.1, supports-color@^8, supports-color@^8.1.1: + version "8.1.1" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + supports-color@^5.3.0: version "5.5.0" resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz" @@ -7175,27 +7040,6 @@ supports-color@^7, supports-color@^7.0.0, supports-color@^7.1.0: dependencies: has-flag "^4.0.0" -supports-color@^8: - version "8.1.1" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz" - integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== - dependencies: - has-flag "^4.0.0" - -supports-color@^8.1.1: - version "8.1.1" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz" - integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== - dependencies: - has-flag "^4.0.0" - -supports-color@8.1.1: - version "8.1.1" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz" - integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== - dependencies: - has-flag "^4.0.0" - supports-hyperlinks@^2.2.0: version "2.3.0" resolved "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz" @@ -7259,11 +7103,6 @@ thread-stream@^3.0.0: dependencies: real-require "^0.2.0" -"through@>=2.2.7 <3": - version "2.3.8" - resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz" - integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== - through2@^4.0.0: version "4.0.2" resolved "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz" @@ -7271,6 +7110,11 @@ through2@^4.0.0: dependencies: readable-stream "3" +"through@>=2.2.7 <3": + version "2.3.8" + resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz" + integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== + tiny-jsonc@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/tiny-jsonc/-/tiny-jsonc-1.0.1.tgz" @@ -7326,7 +7170,7 @@ ts-json-schema-generator@^1.5.1: safe-stable-stringify "^2.4.3" typescript "~5.4.2" -ts-node@^10.8.1, ts-node@^10.9.2, ts-node@>=10: +ts-node@^10.8.1, ts-node@^10.9.2: version "10.9.2" resolved "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz" integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== @@ -7379,7 +7223,7 @@ type-check@^0.4.0, type-check@~0.4.0: dependencies: prelude-ls "^1.2.1" -type-detect@^4.0.0, type-detect@^4.0.8, type-detect@4.0.8: +type-detect@4.0.8, type-detect@^4.0.0, type-detect@^4.0.8: version "4.0.8" resolved "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== @@ -7480,17 +7324,7 @@ typedoc@^0.25.12: minimatch "^9.0.3" shiki "^0.14.7" -"typedoc@0.22.x || 0.23.x": - version "0.23.28" - resolved "https://registry.npmjs.org/typedoc/-/typedoc-0.23.28.tgz" - integrity sha512-9x1+hZWTHEQcGoP7qFmlo4unUoVJLB0H/8vfO/7wqTnZxg4kPuji9y3uRzEu0ZKez63OJAUmiGhUrtukC6Uj3w== - dependencies: - lunr "^2.3.9" - marked "^4.2.12" - minimatch "^7.1.3" - shiki "^0.14.1" - -"typescript@^4.6.4 || ^5.2.2", typescript@^5.4.3, typescript@^5.4.5, typescript@>=2.7, typescript@>=4, typescript@>=4.2.0, typescript@>=4.9.5, typescript@~5.4.2, "typescript@4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x", "typescript@4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x": +"typescript@^4.6.4 || ^5.2.2", typescript@^5.4.3, typescript@^5.4.5, typescript@~5.4.2: version "5.4.5" resolved "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz" integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== @@ -7820,6 +7654,11 @@ yallist@^4.0.0: resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== +yargs-parser@20.2.4, yargs-parser@^20.2.2: + version "20.2.4" + resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz" + integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== + yargs-parser@^18.1.2: version "18.1.3" resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz" @@ -7828,11 +7667,6 @@ yargs-parser@^18.1.2: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^20.2.2: - version "20.2.9" - resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz" - integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== - yargs-parser@^20.2.3: version "20.2.9" resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz" @@ -7843,11 +7677,6 @@ yargs-parser@^21.1.1: resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz" integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== -yargs-parser@20.2.4: - version "20.2.4" - resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz" - integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== - yargs-unparser@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz" @@ -7858,6 +7687,19 @@ yargs-unparser@2.0.0: flat "^5.0.2" is-plain-obj "^2.1.0" +yargs@16.2.0: + version "16.2.0" + resolved "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + yargs@^15.0.2: version "15.4.1" resolved "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz" @@ -7888,19 +7730,6 @@ yargs@^17.0.0, yargs@^17.7.2: y18n "^5.0.5" yargs-parser "^21.1.1" -yargs@16.2.0: - version "16.2.0" - resolved "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz" - integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== - dependencies: - cliui "^7.0.2" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.0" - y18n "^5.0.5" - yargs-parser "^20.2.2" - yn@3.1.1: version "3.1.1" resolved "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz" From df9a09b2f0291f33718c2e9ea470341a7ae50f83 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2026 02:37:48 +0000 Subject: [PATCH 2/2] chore(dev-deps): bump typescript from 5.4.5 to 5.9.3 Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.4.5 to 5.9.3. - [Release notes](https://github.com/microsoft/TypeScript/releases) - [Commits](https://github.com/microsoft/TypeScript/compare/v5.4.5...v5.9.3) --- updated-dependencies: - dependency-name: typescript dependency-version: 5.9.3 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 21a7d13..20b06f7 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "eslint-plugin-sf-plugin": "^1.18.6", "oclif": "^4.14.0", "ts-node": "^10.9.2", - "typescript": "^5.4.5" + "typescript": "^5.9.3" }, "engines": { "node": ">=18.0.0" diff --git a/yarn.lock b/yarn.lock index ccad58e..d65f959 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7324,7 +7324,12 @@ typedoc@^0.25.12: minimatch "^9.0.3" shiki "^0.14.7" -"typescript@^4.6.4 || ^5.2.2", typescript@^5.4.3, typescript@^5.4.5, typescript@~5.4.2: +"typescript@^4.6.4 || ^5.2.2", typescript@^5.4.3, typescript@^5.9.3: + version "5.9.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f" + integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== + +typescript@~5.4.2: version "5.4.5" resolved "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz" integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==