Skip to content
C.W.K.
Stream
Lesson 03 of 05 · published

The Permission That Would Have Vanished

~13 min · migration, silent-failure, capability, review

Level 0Loose Parts
0 XP0/36 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

One Attribute, One Feature

An embedded frame declares which capabilities it grants to the content inside it. The shared panel's policy allowed clipboard writing and nothing else — correct for every consumer it had.

The app being converted allowed clipboard writing and microphone access, because its embedded surface supports voice input. Adopting the shared component as written would have replaced that declaration with the shared one, and voice input would have stopped working.

Nothing about that failure announces itself. The frame renders. The panel opens. The conversation works. Text input is unaffected. One button stops doing anything, and the reason lives in an attribute nobody looks at during a component migration, because the migration is about the component.

Why the Reviews Would Have Passed

Consider what everyone would have checked. Does the build pass — yes. Do the tests pass — yes, because no test asserted on the frame's permission attribute. Does the panel look right — yes. Does the conversation still work — yes. Every question anybody would have thought to ask returns a clean answer, because every one of them is about whether the component works, and the component works.

The question that finds it is a different shape: what capabilities did the thing being replaced have that the replacement does not? That question is not asked by default, because a merge is framed as making two similar things one, and framing it that way directs attention to similarity.

Before replacing a component, diff its CAPABILITY DECLARATIONS against the replacement's, not just its behavior. Permission attributes, granted scopes, allowed origins, enabled feature flags, requested entitlements — these are silent when narrowed. Nothing errors, nothing warns; a capability simply stops being available, and the feature that depended on it fails in a way that looks unrelated to the change that caused it.

The Fix, and the Rule It Left Behind

The shared component grew a permission prop so the app could pass its wider declaration, defaulting to the previous value so the other consumers were untouched. Small change, correct outcome.

What it left behind is a checklist item that generalizes past this family. Whenever a shared component is adopted in place of a bespoke one, enumerate the declarations the bespoke version carried — every attribute that grants, permits, or enables something — and confirm each survives. A later conversion in the same family did exactly this check, found its frame already carried precisely the shared default, and proceeded knowing rather than hoping.

Code

The narrowing that reports nothing, and the check that catches it·typescript
// BEFORE - the app's own panel
<iframe
  src={embedUrl}
  allow="clipboard-write; microphone"   // <- voice input works
/>

// THE SHARED COMPONENT, as written
function SharedPanel({ embedUrl }: Props) {
  return <iframe src={embedUrl} allow="clipboard-write" />;
  //                                   ^^^^^^^^^^^^^^^^
  // Correct for every consumer it had. Adopting as-is silently
  // removes microphone: the frame renders, the panel opens, the
  // conversation works, and one button stops doing anything.
}

// AFTER - a prop, defaulting to the previous value so the other
// consumers are untouched.
function SharedPanel({ embedUrl,
                       iframeAllow = "clipboard-write" }: Props) {
  return <iframe src={embedUrl} allow={iframeAllow} />;
}


// THE PRE-CONVERSION CHECK, which is not a code review question:
//
//   For the component being REPLACED, list every declaration that
//   grants, permits, or enables something:
//
//     iframe allow=          permissions granted to embedded content
//     sandbox=               restrictions LIFTED from it
//     CSP directives         what the page may load or connect to
//     requested scopes       what a token is allowed to do
//     feature flags on       what code paths are reachable
//     entitlements           what the OS lets the process do
//
//   Then assert each one survives. Narrowing any of these is SILENT:
//   no error, no warning, just a capability that is no longer there.

test("conversion preserves every granted capability", () => {
  const before = new Set("clipboard-write; microphone".split("; "));
  const after = new Set(renderPanel().allow.split("; "));
  for (const cap of before) expect(after).toContain(cap);
});

External links

Exercise

Pick a component or service in your system that was replaced by a shared or third-party one in the last year. List every capability declaration the original carried — permissions, scopes, flags, entitlements, allowed origins — and check each against the replacement. Report anything that narrowed, whether or not anyone has noticed.
Hint
Version control is the fastest route: diff the replacement commit and read only the deleted lines, ignoring everything added. Capability declarations are short, distinctive, and almost always deleted wholesale rather than edited, so they stand out immediately in a deletions-only view.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.