Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .squad/agents/apimexpert/history.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,48 @@ the SDK surface, reference docs, or ad-hoc observation.
- Classic Developer/Premium SKU only.
- Docs: <https://learn.microsoft.com/rest/api/apimanagement/policy-restriction> · <https://learn.microsoft.com/azure/templates/microsoft.apimanagement/service/policyrestrictions>

### 2026-06-02: Named Value Token Canonicalization — Broadened Scope

**Problem:** Initial fix only canonicalized `{{namedValue}}` tokens in Logger credentials. Backend credentials also contain these tokens and fail validation when override casing differs from artifact names.

**APIM resources supporting named value tokens:**
1. **Logger** — `properties.credentials.*` (instrumentationKey, connectionString, etc.)
2. **Backend** — `properties.credentials.authorization.parameter`, `properties.credentials.header.*`, `properties.credentials.query.*`
3. Policies/PolicyFragments use `{{tokens}}` in XML, not JSON payloads (handled separately)

**Solution:** Generalized normalization to traverse entire JSON payload recursively, not just Logger credentials. Renamed function: `normalizeLoggerCredentialNamedValueReferences` → `normalizeNamedValueReferences`. Applied to all resource types before PUT.

**Rationale:**
- Backend credentials affected today, not hypothetical
- Future-proof — new resource types with token support work automatically
- Smallest correct change — recursive traversal already existed, just removed Logger-specific conditional
- Preserves opaque JSON (Constitution §VII) — canonicalization safe for unknown properties

**Evidence:**
- Azure REST API spec: Backend credentials support `{{namedValue}}` references ([azure-rest-api-specs](https://github.com/Azure/azure-rest-api-specs/tree/main/specification/apimanagement))
- APIM documentation: Named values in Backend authorization, headers, query strings
- User feedback: "name-value pairs maybe used for other resources"

**Tests:** Added Backend credential canonicalization test. All 910 tests pass.

**Decision doc:** `.squad/decisions/inbox/apimexpert-named-value-scope.md`


### 2026-06-02: Named Value Normalization Generalization — Complete

**Recommendation adopted:** TestEngineer's audit identified Backend resources as affected by the original Logger-only normalization. Generalized approach implemented as recommended.

**Changes:**
- Renamed: `normalizeLoggerCredentialNamedValueReferences()` → `normalizeNamedValueReferences()`
- Scope: Now applies to all resource types (not just Logger)
- Logic: Recursive JSON traversal, entire payload canonicalization
- New test: Backend credential canonicalization (passing)

**Verification:**
- All 910 unit tests pass
- Backend test covers credentials.header, credentials.query, credentials.authorization.parameter
- Recursive approach handles unknown properties (Constitution §VII)

**Decision merged:** Inbox notes moved to `.squad/decisions/decisions.md` (2026-06-02 entries).

**Future-proofing:** Any new APIM resource type with `{{token}}` references will be automatically normalized without additional code changes.
50 changes: 50 additions & 0 deletions .squad/agents/testengineer/history.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,3 +172,53 @@ Gotchas for future PowerShell work:
- `$x = if ($cond) { [List[T]]::new() }` assigns `$null` — PowerShell enumerates the empty list. Use `$x = $null; if ($cond) { $x = ... }`.
- `ProcessStartInfo.StandardOutputEncoding/StandardErrorEncoding` default to OEM on Windows; force UTF-8 or `az --debug` output mangles.

### 2026-05-19: Named Value Token Normalization Coverage Audit

**Context:** User raised concern that Logger credential token normalization (commit 4cb0fa3) might not cover other resources that also use `{{namedValue}}` tokens.

**Audit findings:**
1. **Current implementation**: Only Logger resources have token normalization (lines 118-124 in resource-publisher.ts)
2. **Current test coverage**: One test for Logger credential normalization (line 174-223 in resource-publisher.test.ts)
3. **Gap identified**: Backend resources can also use `{{namedValue}}` tokens in `credentials.header` and `credentials.query` properties

**Backend credentials structure from APIM REST API:**
```json
{
"credentials": {
"header": {
"Authorization": ["{{bearer-token}}"],
"x-api-key": ["{{api-key-namedvalue}}"]
},
"query": { ... },
"certificate": [ ... ]
}
}
```

**Action taken:**
- Added skipped test `should canonicalize backend credential named value references from overrides` to document the gap
- Test currently fails: expects `{{Backend-Auth-Token}}` (canonical casing) but receives `{{backend-auth-token}}` (from override)
- Test will pass once implementation is extended to Backend resources
- Created decision inbox note at `.squad/decisions/inbox/testengineer-named-value-coverage.md` recommending P2 extension

**Recommendation:** Generalize `normalizeLoggerCredentialNamedValueReferences()` to `normalizeCredentialNamedValueReferences()` and apply to both Logger and Backend resource types. The existing recursive logic already handles nested objects/arrays, so minimal code change required.

**Other resource types reviewed:**
- **Diagnostic**: References loggers by ARM ID, not named value tokens
- **Policies (XML)**: Already handle `{{namedValue}}` tokens, but as opaque XML content, not JSON properties
- **ApiOperation, PolicyFragment**: May use named values in future, but not documented in current APIM REST API

**Pattern reinforced:** When implementing normalization/canonicalization logic, audit all resource types that share similar property patterns. Skipped tests document known gaps and serve as regression coverage when gaps are closed.

**Result:** 34 tests passing, 1 skipped test documenting Backend normalization gap. Decision note created for team review.


### 2026-06-02: Named Value Coverage Audit — Merged to Decisions

**Team decision:** ApimExpert implemented the generalized approach, renaming `normalizeLoggerCredentialNamedValueReferences` → `normalizeNamedValueReferences` and applying it to all resource types (not just Logger). Recursive traversal now handles Backend credentials, future resource types, and preserves Constitution §VII (opaque JSON).

**Test status:** Skipped test `should canonicalize backend credential named value references from overrides` now passes. All 910 tests passing.

**Decision merged:** Inbox note moved to `.squad/decisions/decisions.md` (2026-06-02 entry).

**Impact:** Backend resources with `{{namedValue}}` tokens in credentials now work correctly regardless of casing mismatch between override and artifact names.
36 changes: 36 additions & 0 deletions .squad/decisions/decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,39 @@ All new `.ts` files in `src/` and `tests/` must include:
- [Microsoft Open Source Program](https://opensource.microsoft.com/program)

---

### 2026-06-02: Named Value Token Canonicalization — Broadened Scope
**By:** ApimExpert
**Status:** Implemented

The initial fix for named value token canonicalization only applied to Logger resources. Backend resources also contain `{{namedValue}}` tokens in `credentials.header`, `credentials.query`, and `credentials.authorization.parameter`, and require the same casing-sensitive normalization.

**Decision:** Generalize the normalization logic to canonicalize ALL `{{token}}` references across ANY resource type's entire JSON payload.
- Renamed function: `normalizeLoggerCredentialNamedValueReferences` → `normalizeNamedValueReferences`
- Removed Logger-specific resource type check; now applies to all resources
- Recursive traversal handles unknown properties (Constitution §VII)
- Implementation: `src/services/resource-publisher.ts`
- Tests: Backend credential test added + all 910 tests pass

**Rationale:** Backend credentials are affected today, not hypothetical. Future-proof for any new APIM resource types that support named value tokens.

**Evidence:** Azure APIM REST API spec shows Backend credentials support `{{namedValue}}` references in authorization parameters, headers, and query strings. User report confirmed: "Named value pairs maybe used for other resources!"

---

### 2026-06-02: Extend Named Value Token Normalization to Backend Resources
**By:** TestEngineer
**Status:** Audit Complete

Audit identified that named value token normalization only covered Logger resources, leaving Backend credentials vulnerable to APIM validation failures when override casing differs from artifact naming.

**Findings:**
- Backend resources contain `{{namedValue}}` tokens in `credentials.header`, `credentials.query`, and `credentials.authorization.parameter`
- Example: override supplies `{{Bearer-Token}}` but artifact is named `bearer-token` → APIM rejects PUT
- Skipped test added: `should canonicalize backend credential named value references from overrides` (test code in resource-publisher.test.ts)

**Recommendation:** Extend normalization logic to Backend resources. Generalized approach (apply to all resource types) preferred over Backend-specific implementation.

**Priority:** P2 (Medium) — Valid production use case but workaround exists (manual casing in overrides).

---
14 changes: 13 additions & 1 deletion src/services/extract-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,7 @@ async function resolveAndExtractTransitive(

// Build maps for transitive resolution
const apiJsonMap = new Map<string, Record<string, unknown>>();
const namedValueJsonMap = new Map<string, Record<string, unknown>>();
for (const apiResult of result.apiResults) {
// Find the API's JSON from type results
const apiTypeResults = result.typeResults.filter((r) => r.type === ResourceType.Api);
Expand All @@ -485,10 +486,21 @@ async function resolveAndExtractTransitive(
}
}

const namedValueTypeResults = result.typeResults.filter((r) => r.type === ResourceType.NamedValue);
for (const ntr of namedValueTypeResults) {
for (const extracted of ntr.extracted) {
if (extracted.status === 'success') {
const name = getNamePart(extracted.descriptor.nameParts, 0);
namedValueJsonMap.set(name, extracted.json);
}
}
}

// Find transitive dependencies
const transitiveDeps = findTransitiveDependencies(
result.collectedPolicies,
apiJsonMap
apiJsonMap,
namedValueJsonMap
);

// Filter out already-extracted resources
Expand Down
97 changes: 97 additions & 0 deletions src/services/resource-publisher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,12 @@ export async function publishResource(
json = normalizeApiOperationTextFields(json);
}

// APIM resource payloads can reference NamedValues using {{name}} syntax.
// Logger credentials, Backend credentials, and other properties support this.
Comment thread
EMaher marked this conversation as resolved.
// Canonicalize all {{token}} casing to actual NamedValue artifact names so
// APIM can resolve references at publish time (APIM validation is case-sensitive).
json = await normalizeNamedValueReferences(json, store, config.sourceDir);

// For KeyVault-backed NamedValues:
// 1. Strip properties.value — APIM must not receive both keyVault and value
// in the same PUT body, as it causes indefinite provisioning or rejection.
Expand Down Expand Up @@ -551,3 +557,94 @@ function isAutoGeneratedProductSubscription(

return scope.includes('/products/');
}

/**
* Normalize all named value token references across the entire resource payload.
*
* APIM resolves named value references using {{namedValueName}} tokens in various
* resource properties (Logger credentials, Backend credentials, etc.).
* If an override supplies a token with different casing, APIM may reject the PUT
* with a validation error (APIM validation is case-sensitive).
*
* This function recursively traverses the entire JSON payload and rewrites all
* {{token}} references to match the canonical NamedValue artifact names on disk.
*/
async function normalizeNamedValueReferences(
Comment thread
EMaher marked this conversation as resolved.
json: Record<string, unknown>,
store: IArtifactStore,
sourceDir: string
): Promise<Record<string, unknown>> {
const resources = await store.listResources(sourceDir);
const namedValueNames = new Map<string, string>();
for (const descriptor of resources) {
if (descriptor.type === ResourceType.NamedValue) {
const name = getNamePart(descriptor.nameParts, 0);
namedValueNames.set(name.toLowerCase(), name);
}
}

if (namedValueNames.size === 0) {
return json;
}

const normalizeTokenString = (value: string): string =>
value.replace(/\{\{\s*([^}]+?)\s*\}\}/g, (match, tokenName: string) => {
const canonicalName = namedValueNames.get(tokenName.toLowerCase());
return canonicalName ? `{{${canonicalName}}}` : match;
});

const normalizedRoot: Record<string, unknown> = {};
const stack: Array<{ source: unknown; assign: (value: unknown) => void }> = [];

for (const [key, value] of Object.entries(json)) {
stack.push({
source: value,
assign: (normalizedValue: unknown) => {
normalizedRoot[key] = normalizedValue;
},
});
}

while (stack.length > 0) {
const frame = stack.pop()!;
const { source, assign } = frame;

if (typeof source === 'string') {
assign(normalizeTokenString(source));
continue;
}

if (Array.isArray(source)) {
const out: unknown[] = new Array(source.length);
assign(out);
// Push in reverse so LIFO processing assigns array elements in natural order.
for (let i = source.length - 1; i >= 0; i--) {
stack.push({
source: source[i],
assign: (normalizedValue: unknown) => {
out[i] = normalizedValue;
},
});
}
continue;
}

if (source && typeof source === 'object') {
const out: Record<string, unknown> = {};
assign(out);
for (const [key, child] of Object.entries(source as Record<string, unknown>)) {
stack.push({
source: child,
assign: (normalizedValue: unknown) => {
out[key] = normalizedValue;
},
});
}
continue;
}

assign(source);
}

return normalizedRoot;
}
Loading
Loading