Skip to content

feat!: OAuth client credentials and the v1 API (1.0.0) - #48

Draft
royklo wants to merge 27 commits into
mainfrom
feature/oauth-auth
Draft

royklo wants to merge 27 commits into
mainfrom
feature/oauth-auth

Conversation

@royklo

@royklo royklo commented Nov 4, 2026 •

Copy link
Copy Markdown
Owner

What's New

inforcer now has a versioned API (v1) with API clients that log in with a client ID and secret. 1.0.0 supports both logins: an API key keeps using the legacy beta API, and client credentials use v1. Every cmdlet works with either, and v1 responses are mapped to the module's usual property names, so most scripts run unchanged.

Highlights

  • Connect-Inforcer -ClientId <id> -ClientSecret <secret> -Region uk, with automatic token renewal and 429 retries that honour Retry-After.
  • -TenantId takes a Microsoft tenant ID (GUID) or a tenant name with both logins; numeric IDs keep working with an API key.
  • Invoke-InforcerAssessment shows inforcer's own compliance score with client credentials. Compare-InforcerEnvironments can compare across logins, and it collects both tenants at the same time.
  • v1 returns Intune policies (inforcer fixed ENG-6967, verified live: v1 = beta, 244 policies). Policy types inforcer could not read are named in a warning (failedPolicyTypes).

Fixes that API-key users get too

  • Piping several tenants now handles every tenant. Before, only the last one was processed.
  • Get-InforcerAlignmentDetails -Tag with a tag no tenant has returns nothing and warns; it used to return every row.
  • The settings catalog updates again, and Get-InforcerSupportedEventType returns the live list.
  • Correct Microsoft Learn names for 11 Conditional Access apps. The export fills "Profile type" again.
  • Bulk report download through Save-InforcerReportOutput works.
  • Security:
    • Downloads never overwrite files.
    • Exports neutralise formulas, and HTML reports escape names.
    • Credentials never follow a redirect.
    • The API key no longer ends up in $Error.

The full list and the migration steps are in CHANGELOG.md under 1.0.0.

Verification

  • Pester: 828 passed; 1 failure that also fails on main (SettingsCatalog integration test against the live catalog data, unrelated).
  • Live matrix: every cmdlet and option was run with an API key and with client credentials on the same tenants (109 cases) and compared output by output. That includes one real assessment and report run per variant. It was rerun after the review fixes. (That run predates inforcer's Intune fix; Export -Tag on v1 failed then because the tag sits on an Intune policy.)
  • API-key output was compared with 0.8.0 on the same account; the only differences are the fixes listed in the changelog.
  • Reviews: consistency contract, security, performance, docs accuracy, static-to-dynamic, and API feedback. All high and medium findings are fixed.
  • 2026-11-05: inforcer's v1 fixes (ENG-6987 to 6992) verified live and taken in: the full v1 tenant list (no per-tenant calls), the assessment score, one policy type name. Red/blue/green review of that change; every new behaviour survives mutation testing.

Full Changelog: v0.8.0...feature/oauth-auth

🤖 Generated with Claude Code

royklo and others added 23 commits November 3, 2026 20:54
…PI keys

Connect-Inforcer gains a ClientCredentials parameter set (-ClientId/-ClientSecret).
The API version follows the login: API key -> /beta, client credentials -> /v1.
v1 responses are mapped to the module's existing field names at the transport
boundary, so API-key users see no change apart from a legacy-API warning.

- Get-InforcerAuthHeader is the single place auth headers are built; v1 tokens
  are requested and renewed automatically
- Invoke-InforcerApiRequest: version-neutral endpoints, v1 renames, problem+json
  errors, 429 Retry-After handling, 401 token renewal, collection unwrap
- New Invoke-InforcerPagedRequest, Get-InforcerTenantList (session-bound cache)
- Resolve-InforcerTenantId accepts Microsoft tenant IDs and names on v1
- Test-InforcerConnection no longer writes part of the API key to Verbose

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… audit events

- Paging goes through Invoke-InforcerPagedRequest in both modes
- Get-InforcerGroup -Group attaches v1 members from the paged members endpoint
- Get-InforcerTenantPolicies warns when v1 returns no Intune policies
- v1 audit search without -EventType no longer sends beta-only event types
- -TenantId accepts the MsTenantId alias for pipeline binding
- fix: Get-InforcerUser -OutputType JsonObject no longer throws
- fix: Get-InforcerSupportedEventType returns the live list instead of the
  built-in fallback (a ?. expression parsed as a variable named $n?)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- All report and assessment requests, including the assessment runspace, the
  output download, the run probe and the TAB completer, take their auth header
  from Get-InforcerAuthHeader; long polls fetch a fresh header per call
- v1 report runs post msTenantIds; outputs map msTenantId to tenantId
- Permission and scope hints name v1 API client permissions on a v1 login
- Invoke-InforcerAssessment -MultiTenant finds tenants on v1
- -TenantId accepts the MsTenantId alias
- fix: the assessment-ID completer returned nothing when -ReportType was unbound

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eference Stop

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ThrowTerminatingError is silenced when the caller passes -ErrorAction
SilentlyContinue, so cmdlets wrapping Resolve-InforcerTenantId in try/catch
returned nothing instead of their InvalidTenantId error. A plain throw of the
ErrorRecord keeps the ErrorId and is always catchable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Get-InforcerTenant -Detailed fetches per-tenant details on v1 (no-op on
  beta, whose list is already rich); -TenantId returns the detailed tenant
- Get-InforcerBaseline and Get-InforcerAlignmentDetails work on v1, including
  -Tag, baseline members and the scores table
- Tenant lookups reuse the cached tenant list instead of refetching
- Format views show the Microsoft tenant ID when there is no client tenant ID
- -TenantId accepts the MsTenantId alias for pipeline binding

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The caught resolver error also lands in -ErrorVariable, as it already did for
unknown tenant names, so the cmdlet's own record is not always first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It emitted the session and then the status object, so the documented
$s = Connect-Inforcer -PassThru; Compare-InforcerEnvironments -SourceSession $s
failed with 'Cannot convert System.Object[] to Hashtable'.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…forcerEnvironments

- Documentation data loads the detailed tenant on v1 and never falls back to
  another tenant when the record is missing
- Baseline filtering on v1 joins on policy ids (v1 ids are Microsoft 365 ids)
- Compare resolves each baseline owner in its own session, so mixed API-key and
  client-credentials sessions work
- On v1 the HTML, Markdown, Excel and comparison reports carry a note when the
  API returned no Intune policies
- Export -TenantId accepts the MsTenantId alias

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every caller now passes version-neutral endpoints.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…m API keys

- README: quick start for both logins, legacy notice, migration guide
- CMDLET-REFERENCE: new Connect parameter set, -Detailed, MsTenantId alias,
  v1 endpoints and API client permissions per cmdlet, Intune-gap notes
- API-REFERENCE: authentication, v1 envelope, errors, paging, rate limits,
  every v1 path with its beta equivalent, permission tables for both models
- docs/openapi-snapshot-v1.json: production v1 spec snapshot

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ixed it

Verified live: every dynamic group carries its rule on the list.
Removed the help, view and reference text that said it was always null.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- API-key logins no longer retry on 429; only v1 honours Retry-After (0.8.0 behaviour)
- cmdlets read their own error record, so network failures show their reason again
- failed requests are stripped of auth headers and body, so no token, key or
  secret remains in $Error, -ErrorVariable or Get-Error
- user and group lists stream again; Select-Object -First stops paging
- a tenant missing from the cached list triggers one refresh
- a failed v1 members call warns instead of returning an empty member list
- Connect-Inforcer -BaseUrl must use https
- help, docs and tests use synthetic ids; personal-data note added
- dead fallbacks and stale comments removed

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Get-InforcerReportRun -IncludeOutputs / -Wait embedded outputs without a
runId, so the documented bulk-download pipeline failed with "RunId missing"
in both login modes (also on 0.8.0). Outputs now carry runId, set where the
outputs endpoint is read. README named the internal error id for numeric
tenant ids on v1; callers see InvalidTenantId.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Get-InforcerTenantPolicies, Get-InforcerBaseline, Get-InforcerAlignmentDetails
and Export-InforcerTenantDocumentation bind -TenantId from the pipeline but had
no process block, so `Get-InforcerTenant | <cmdlet>` only handled the last
tenant (also on 0.8.0). Invoke-InforcerAssessment collects piped tenants and
runs them as one multi-tenant assessment, the same as an -TenantId array.
A consistency test now fails for any cmdlet that takes pipeline input
without a process block.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Eleven app ids in the -FetchGraphData name map were labelled with the wrong
or an outdated name, e.g. 01cb2876 is Device Registration Service (not Global
Secure Access), de8bc8b5 is Graph Explorer (not Microsoft Graph Command Line
Tools), d4ebce55 is Microsoft Intune Enrollment and 4765445b is OfficeHome.
Names now match Microsoft Learn; the two ids Learn does not list are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Report downloads reduce the server's file name to a safe leaf, never
  overwrite an existing file (name (n).ext) and -FileName writes directly.
- HTML reports encode tenant- and API-supplied strings for their context:
  the assessment matrix no longer builds inline handlers from tenant names,
  the documentation search highlight builds text nodes, the comparison
  category filter builds DOM elements.
- Excel export writes '=' values as text and no automatic hyperlinks;
  Markdown cells and headings encode & < >.
- Requests that carry the API key, token or client secret never follow a
  redirect; a 3xx fails with a clear message.
- The settings catalog freshness check decoded the release asset as bytes,
  so the cache never refreshed; it now decodes UTF-8, downloads when the
  remote date is unknown, and validates size and shape before replacing it.
- Saved downloads are marked as internet files on Windows; -Open no longer
  launches .xls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Get-InforcerBaseline and Get-InforcerAlignmentDetails collect piped
  tenants and fetch baselines, scores and tenant details once per call;
  rows no longer repeat.
- Results from piped tenants carry TenantId and -OutputType JsonObject
  returns one array; calls without pipeline input are unchanged.
- v1 Role, Tenant and the alignment table return a JSON array for one item.
- Baselines are cached per session (Get-InforcerBaselineList); a v1
  -TenantId GUID lookup makes only the detail call.
- Beta audit search sends one request instead of one per 10 event types
  (verified live: identical results).
- The assessment run renews a v1 token once on 401, honours Retry-After on
  v1 429 and never follows a redirect; CSV export neutralises formulas.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… type

- Export builds Graph maps per tenant (no carry-over between piped tenants
  or calls), stops before overwriting one -OutputPath file with several
  tenants, and opens each HTML file once.
- Graph sign-in requests Directory.Read.All, Policy.Read.All and
  DeviceManagementConfiguration.Read.All; 401/403 are not retried.
- Conditional Access app names come from Graph (static map only as
  fallback), roles from roleDefinitions (custom roles too), groups via
  getByIds; Entra/SharePoint/M365 settings use inforcer's own names.
- Compare fetches source and destination in parallel.
- Beta no longer sends the policy type name; Export fills Profile type
  from policyTypeId (Get-InforcerPolicyTypeName, names taken from v1
  responses) until inforcer restores the field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixes 30 verified documentation errors (examples that did not bind,
-OutputPath being a file for assessments, Test-InforcerConnection needing a
baselines permission, swapped beta/v1 endpoints, wrong error ids and
response shapes) and documents the behaviour from the security, pipeline
and Graph fixes: piped TenantId, download rules, redirect refusal, catalog
refresh, Graph scopes and parallel Compare.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… audit event fix

-TenantId returns a fresh record per call, as in 0.8.0, so the cached-list note was wrong.
The single audit search also returns event types the per-type search missed (apiKeyCreate, apiClientCreate).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings November 4, 2026 13:57
@royklo
royklo marked this pull request as draft November 4, 2026 13:59

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved security and correctness issues can expose client secrets in error records, trigger account-wide assessments from empty pipelines, and select groups from incomplete search results.

6 open issues
What changed in this PR

Introduces the 1.0.0 dual-login model: OAuth client credentials use the v1 API, while API keys retain legacy beta access. Shared helpers normalize responses and authentication so existing cmdlets can support both APIs.

Changes:

  • Adds v1 authentication, response mapping, tenant resolution, and shared pagination.
  • Improves multi-tenant pipelines, report downloads, and parallel environment comparisons.
  • Hardens credential handling and exports, refreshes settings-catalog handling, and documents migration.
File Description
Tests/​GraphResolution.Tests.ps1 Replaces named-location fixture GUIDs.
Tests/​DocModel.Tests.ps1 Standardizes GUID comparison fixtures.
Tests/​Consistency.Tests.ps1 Updates authentication, pipeline, and download assertions.
module/​Public/​Test-InforcerConnection.ps1 Uses shared authenticated requests for connection checks.
module/​Public/​Save-InforcerReportOutput.ps1 Supports both APIs and safe filename overrides.
module/​Public/​Invoke-InforcerAssessment.ps1 Collects piped tenants and protects CSV values.
module/​Public/​Get-InforcerUser.ps1 Adds shared pagination and tenant-labelled pipeline output.
module/​Public/​Get-InforcerTenantPolicies.ps1 Supports v1 policies and warns about missing Intune data.
module/​Public/​Get-InforcerTenant.ps1 Adds v1 tenant details and session-cached lookups.
module/​Public/​Get-InforcerSupportedEventType.ps1 Corrects live event-type parsing.
module/​Public/​Get-InforcerSecureScore.ps1 Supports both APIs and combined pipeline JSON.
module/​Public/​Get-InforcerRole.ps1 Adds v1 mapping and tenant-labelled results.
module/​Public/​Get-InforcerReportType.ps1 Uses version-neutral report catalog requests.
module/​Public/​Get-InforcerReportRun.ps1 Maps v1 report runs to existing property names.
module/​Public/​Get-InforcerGroup.ps1 Adds shared pagination and separate v1 member retrieval.
module/​Public/​Get-InforcerBaseline.ps1 Fetches once and labels results for piped tenants.
module/​Public/​Get-InforcerAuditEvent.ps1 Consolidates searches through shared pagination.
module/​Public/​Get-InforcerAssessment.ps1 Uses version-neutral assessment requests.
module/​Public/​Disconnect-Inforcer.ps1 Recognizes either login type when disconnecting.
module/​Public/​Compare-InforcerEnvironments.ps1 Resolves baseline owners per session and propagates notes.
module/​Private/​Test-InforcerSession.ps1 Validates API-key or client-credential sessions.
module/​Private/​Test-InforcerReportRunTerminal.ps1 Supports v1 polling, token renewal, and output run IDs.
module/​Private/​Select-InforcerBaselinePolicies.ps1 Supports v1 tenant and policy identifiers.
module/​Private/​Resolve-InforcerTenantId.ps1 Resolves identifiers according to the active API.
module/​Private/​Resolve-InforcerReportTypeSchema.ps1 Uses mapped catalog requests and broader credential redaction.
module/​Private/​Resolve-InforcerReportOutputFileName.ps1 Prevents downloaded filenames from becoming hidden dotfiles.
module/​Private/​Resolve-InforcerGroupId.ps1 Searches all group pages during name resolution.
module/​Private/​Resolve-InforcerBaselineId.ps1 Uses version-neutral baseline lookup.
module/​Private/​Resolve-InforcerAssessmentId.ps1 Uses version-neutral assessment lookup.
module/​Private/​Protect-InforcerApiKeyInText.ps1 Extends redaction to secrets and bearer tokens.
module/​Private/​Invoke-InforcerRawDownload.ps1 Adds dual authentication and non-overwriting downloads.
module/​Private/​Invoke-InforcerPagedRequest.ps1 Centralizes pagination for both APIs.
module/​Private/​Invoke-InforcerGraphRequest.ps1 Refines retries and permission warnings.
module/​Private/​Invoke-InforcerAssessmentRun.ps1 Adds v1 retries and redirect-safe background requests.
module/​Private/​Get-InforcerTenantList.ps1 Adds a session-specific tenant cache.
module/​Private/​Get-InforcerSettingsCatalogPath.ps1 Fixes refresh handling and validates downloaded catalogs.
module/​Private/​Get-InforcerPolicyTypeName.ps1 Supplies fallback profile-type names.
module/​Private/​Get-InforcerDocData.ps1 Collects dual-API data and records coverage gaps.
module/​Private/​Get-InforcerComparisonData.ps1 Collects destination data concurrently with the source.
module/​Private/​Get-InforcerBaseUrl.ps1 Centralizes versioned URLs and endpoint renames.
module/​Private/​Get-InforcerBaselineList.ps1 Adds a session-specific baseline cache.
module/​Private/​Get-InforcerAuthHeader.ps1 Centralizes authentication and OAuth token renewal.
module/​Private/​Export-InforcerDocExcel.ps1 Neutralizes formulas and adds report notes.
module/​Private/​ConvertTo-InforcerMarkdown.ps1 Encodes HTML-sensitive text and includes notes.
module/​Private/​ConvertTo-InforcerHtml.ps1 Safely highlights search text and displays notes.
module/​Private/​ConvertTo-InforcerDocModel.ps1 Improves policy labels, profile types, and tenant fallbacks.
module/​Private/​ConvertTo-InforcerComparisonHtml.ps1 Safely constructs filters and displays coverage notes.
module/​Private/​ConvertTo-InforcerAssessmentMatrixHtml.ps1 Hardens embedded data and tenant controls.
module/​Private/​ConvertTo-InforcerAssessmentHtml.ps1 Escapes status attribute values.
module/​Private/​ConvertFrom-InforcerV1Response.ps1 Maps v1 objects to canonical property names.
module/​Private/​Connect-InforcerGraph.ps1 Expands default Graph read scopes.
module/​InforcerCommunity.psm1 Warns when unloading either authenticated session type.
module/​InforcerCommunity.psd1 Sets version 1.0.0 and updates release notes.
module/​InforcerCommunity.Format.ps1xml Adapts displayed fields to API-specific properties.
CONTRIBUTING.md Documents dual-API implementation and testing conventions.
CHANGELOG.md Records release changes, limitations, and migration steps.

Review effort: Balanced

…olicy type name

inforcer released fixes for the v1 API (ENG-6987..6992), verified live on UK 2026-11-05.

- The v1 tenant list now holds every field, so Get-InforcerTenant -Detailed (never released)
  is removed, and -Tag and -TenantId in Get-InforcerAlignmentDetails read tags and aligned
  tenants from the list: no per-tenant or /baselines call. v1 and beta share one path.
- Invoke-InforcerAssessment shows inforcer's own score (v1 summary.score) when it is a number
  from 0 to 100, else it computes Pass / (Pass + Fail); the matrix report escapes the score.
- Profile type names follow the names v1 now uses on every route (8 updated, 5 added).
- A -Tag or piped tenant that matches nothing returns no rows instead of every row; -Tag
  warns only when no tenant has the tag (also a fix for API-key users).
- Docs: v1 audit events carry relId/relType and list the apiClient* types; refreshed the v1
  OpenAPI snapshot.

Reviewed red/blue/green; mutations of every new behaviour are caught by the tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings November 5, 2026 18:02

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread module/Public/Get-InforcerBaseline.ps1 Resolved
Comment thread module/Public/Get-InforcerReportRun.ps1 Resolved
- Group name lookup no longer picks a group when a later search page failed.
- Report-run polling waits out a v1 429 (Retry-After, at most 3 times) instead of stopping.
- An empty pipeline into Invoke-InforcerAssessment runs nothing, also with -MultiTenant.
- Compare fetches the destination again in the foreground when the background fetch reported
  an error, so the error follows the caller's -ErrorAction.
- Baseline scoping counts one entry per baseline policy and only lists a policy as missing
  when the tenant has it under neither its name nor any of its ids.
- v1 JSON stays an array: Get-InforcerBaseline -TenantId, Get-InforcerReportRun (also [] for
  no runs; a failed list request still returns nothing) and Invoke-InforcerReport -NoWait.

Each fix has a test that fails without it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings November 5, 2026 18:20

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Successful empty v1 tenant responses bypass JSON serialization, causing -OutputType JsonObject to return $null instead of [] and violating the documented list-array contract.

No open issues 🎉

1 previously missed issue

In code that hasn't changed since last review

Medium severity Return an empty JSON array for successful v1 tenant lists

module/​Public/​Get-InforcerTenant.ps1:115

A successful v1 response with no visible tenants exits here before JSON serialization, so Get-InforcerTenant -OutputType JsonObject returns $null rather than []. This breaks the documented v1 list-array contract for clients whose tenant filters match nothing. Let successful empty v1 lists reach the JSON branch, while preserving beta behavior and returning nothing when the list request fails.

10 resolved since last review

Review effort: Balanced

Get-InforcerTenant and the Get-InforcerAlignmentDetails table returned nothing for a successful
empty v1 response, breaking the v1 list-array contract. A failed request still returns nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings November 5, 2026 18:29

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Human assessment is needed because the published review omits relevant concerns about Graph scope authorization and token-refresh behavior during retries.

No open issues 🎉

Review effort: Balanced

The v1 policies endpoint now returns Intune and Defender settings-catalog policies (verified on
UK 2026-11-05: v1 = beta, 244 policies, 81 Intune). Removed the zero-Intune warning in
Get-InforcerTenantPolicies and Get-InforcerDocData, and the report note it fed (DocModel Notes in
HTML, Markdown, Excel and the Compare report), which nothing else used. Skipped types are still
named through the failedPolicyTypes warning. Docs no longer tell users to keep an API key for Intune.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings November 5, 2026 19:38

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Human assessment is needed to weigh moderate-severity risks in missing-policy detection, OAuth retry handling, and Graph scope-tag permissions.

No open issues 🎉

Review effort: Balanced

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants