Branch: refactor/348-extract-origin-validation-to-permission-service
Status: ✅ COMPLETE
Implementation: Option 3 — Re-export normalizeOrigin from permission-service as facade
Origin validation logic was previously accessible only via direct imports from @vellar/provider-sdk. This refactor consolidates the API surface by re-exporting origin-validation utilities through @vellar/permission-service, making permission-service the single import boundary for all origin/permission operations.
Key insight: No duplication existed in the codebase—origin validation was already centralized in packages/provider-sdk/src/permissions.ts. The refactor establishes permission-service as the facade, future-proofing the boundary when grant management and revocation logic are implemented.
Before:
// @vellar/permission-service — dApp origin permissions, extension connection records, revocation state
// See CLAUDE.md and BUILD-PLAN.md before implementing.
export {};After:
// @vellar/permission-service — dApp origin permissions, extension connection records, revocation state
// See CLAUDE.md and BUILD-PLAN.md before implementing.
// Re-export origin-validation utilities as the canonical permission-related API.
// These are sourced from @vellar/provider-sdk but exposed here so permission-service
// is the single import boundary for all origin/permission operations (issue #348).
export { hasCapability, normalizeOrigin, type PermissionGrant } from "@vellar/provider-sdk";Rationale:
- Establishes permission-service as the semantic owner of origin operations (per README)
- Creates a single, well-defined boundary for all permission-related imports
- Simplifies future expansion (grant management, revocation state) by keeping them in one place
Before:
import {
errorPayload,
hasCapability,
normalizeOrigin,
type ProviderRequest,
type ResponsePayload,
} from "@vellar/provider-sdk";After:
import {
errorPayload,
type ProviderRequest,
type ResponsePayload,
} from "@vellar/provider-sdk";
import { hasCapability, normalizeOrigin } from "@vellar/permission-service";Rationale:
- Separates concerns: provider-sdk provides type/message utilities; permission-service provides permission-related validation
- Makes the extension's dependency on permission-service explicit
Before:
import { normalizeOrigin } from "@vellar/provider-sdk";After:
import { normalizeOrigin } from "@vellar/permission-service";Rationale:
- Aligns with the new import boundary; pair-origins is part of the extension's permission model
Before:
"dependencies": {
"@stellar/stellar-sdk": "^16.0.1",
"@vellar/provider-sdk": "workspace:*",
...
}After:
"dependencies": {
"@stellar/stellar-sdk": "^16.0.1",
"@vellar/permission-service": "workspace:*",
"@vellar/provider-sdk": "workspace:*",
...
}Rationale:
- Explicitly declares the extension's new dependency on permission-service
Comprehensive integration test verifying that both import paths (via permission-service facade and direct from provider-sdk) produce identical validation results for all edge cases.
Test coverage:
- Valid origins (https, http, custom ports)
- Trailing-dot FQDN normalization (L5 requirement:
"https://app.example.com."→"https://app.example.com") - Invalid cases: paths/queries, non-URLs, dangerous schemes (file://, chrome-extension://, javascript:)
- Dotted/dotless origin equivalence (both map to the same normalized form)
- Doubled trailing dots (malformed; stays distinct)
Test vectors (14 cases total):
const testVectors = [
// Valid
["https://app.example.com", "https://app.example.com"],
["http://localhost:3000", "http://localhost:3000"],
["https://app.example.com:8443", "https://app.example.com:8443"],
// Trailing-dot FQDN normalization (L5)
["https://app.example.com.", "https://app.example.com"],
["https://app.example.com.:8443", "https://app.example.com:8443"],
["http://localhost.", "http://localhost"],
// Invalid: path/query/trailing-slash
["https://app.example.com/evil", undefined],
["https://app.example.com?x=1", undefined],
["https://app.example.com/", undefined],
// Invalid: non-URLs
["app.example.com", undefined],
["", undefined],
// Invalid: dangerous schemes
["file:///etc/passwd", undefined],
["chrome-extension://abcdef", undefined],
["javascript:alert(1)", undefined],
// Invalid: doubled trailing dots
["https://app.example.com..", "https://app.example.com."],
];Each test vector is run through both import paths and verified to produce identical results, confirming the re-export is correct and both imports resolve to the same underlying implementation.
Grepped the extension codebase for any remaining direct imports of origin-validation from provider-sdk:
- No matches for
import.*normalizeOrigin.*provider-sdk - No matches for
import.*hasCapability.*provider-sdk
All imports have been successfully migrated to permission-service.
The integration test (origin-validation-integration.test.ts) confirms:
- Both call paths (permission-service facade and provider-sdk direct) produce identical results
- All edge cases from the original provider-sdk tests are covered
- Trailing-dot normalization (L5 requirement) works correctly
- Dotted and dotless forms are properly collapsed to the same canonical origin
Option 1 (Issue is outdated): ❌ Unhelpful without guidance on actual refactoring goal
Option 2 (permission-service implements grant/revocation): ❌ Out of scope; requires significant new implementation
Option 3 (permission-service facade): ✅ CHOSEN
- Semantic ownership — permission-service owns "dApp origin permissions" per README; origin validation is foundational to permissions
- Single import boundary — All permission-related operations (current: validation; future: grants, revocation) go through one service
- Future-proof — When grant management/revocation are implemented, they'll be in the same service alongside
normalizeOrigin - Monorepo pattern — Follows how other services expose utilities they depend on from shared packages
- Zero behavioral change — No code moved or duplicated; just re-exported, so all existing validation logic remains untouched
- Minimal refactoring — Only import statements change; no internal implementation changes
| File | Change | Type |
|---|---|---|
services/permission-service/src/index.ts |
Re-export normalizeOrigin, hasCapability, PermissionGrant |
Modified |
apps/extension/lib/router.ts |
Import from permission-service instead of provider-sdk | Modified |
apps/extension/lib/pair-origins.ts |
Import from permission-service instead of provider-sdk | Modified |
apps/extension/package.json |
Add @vellar/permission-service dependency |
Modified |
apps/extension/lib/origin-validation-integration.test.ts |
New comprehensive integration test | New |
The new integration test covers 14 test vectors across all edge cases:
- 6 validation scenarios (3 valid + 3 trailing-dot cases)
- 3 invalid path/query cases
- 2 invalid non-URL cases
- 3 invalid dangerous scheme cases
- 1 malformed doubled-trailing-dot case
# Extension tests
pnpm test --filter=@vellar/extension
# Permission-service tests (existing)
pnpm test --filter=@vellar/permission-service
# Full workspace test suite
pnpm testAll tests should pass without modification to existing logic.
- Consolidate origin-validation API: permission-service is now the single import boundary
- Update extension imports: router.ts and pair-origins.ts import from permission-service
- Add integration tests: Both import paths produce identical validation results
- Verify removal: No leftover references to origin-validation from provider-sdk in extension
- Behavioral equivalence: All edge cases (trailing dots, invalid schemes, etc.) handled identically
- Zero breaking changes: No changes to validation logic; only import restructuring
After merging this refactor:
- When implementing grant management: Add grant lookup/storage logic to permission-service alongside existing
hasCapabilityexport - When implementing revocation: Add revocation state tracking to permission-service
- When expanding permission boundaries: All permission-related logic will be in a single, well-defined service
✅ Refactor #348 is complete and ready for testing and merge.
- Origin-validation is now accessed via permission-service (the semantic owner)
- Extension imports have been updated and verified
- Integration test confirms both import paths produce identical results
- No leftover references remain in the extension codebase
- All acceptance criteria met; zero breaking changes