CLI: positional rp-id/rp-name; init adds domains to an existing database

- 'paskia init [rp-id] [rp-name]' and 'paskia migrate [rp-id]' are now
  positional; comma separation and the --rp-id/--rp-name flags are gone.
- With an existing paskia.kantadb, init adds the rp-id as a new domain
  (seeding its OIDC provider) or updates an existing domain's rp-name.
- Origin allow-list semantics clarified: the bare '*' entry allows
  anything within the rp-id domain on any scheme and port (also the
  empty-list default and its display in the admin UI, replacing the
  synthetic '*.rp-id' row); '*.x' wildcards are https-only; exact entries
  match scheme, host and port. Legacy '*.rp-id' wildcards migrate to '*'
  to preserve their any-scheme meaning.
This commit is contained in:
2026-09-07 03:07:38 +00:00
parent 476ce996ad
commit 7ff8869e1d
16 changed files with 293 additions and 129 deletions
+12 -9
View File
@@ -113,6 +113,7 @@ function isWellFormedDomain(value) {
function originHostname(origin) {
if (!origin.trim()) return null
if (origin.trim() === '*') return '*'
if (origin.trim().startsWith('*.')) {
const base = origin.trim().slice(2).replace(/\.+$/, '')
return isWellFormedDomain(base) ? base : null
@@ -126,6 +127,7 @@ function originHostname(origin) {
}
function isWithinDomain(origin, rpId) {
if (origin.trim() === '*') return true
const hostname = originHostname(origin)
if (!hostname) return false
return hostname === rpId || hostname.endsWith('.' + rpId)
@@ -167,7 +169,7 @@ function validateOrigin(i) {
d.originValidation[i] = 'invalid'
return
}
if (value.trim().startsWith('*.')) {
if (value.trim() === '*' || value.trim().startsWith('*.')) {
d.originValidation[i] = null // wildcards have no concrete site to probe
return
}
@@ -203,15 +205,15 @@ async function testWellKnown() {
}
watch(() => relatedEntries.value.map(asHttpsOrigin).join('|'), testWellKnown, { immediate: true })
// Seed the default wildcard entry for a new domain once its rp-id is known,
// so the list always shows what is allowed ('*.example.com' = the domain and
// all its subdomains). Removing the last in-domain entry is blocked in the
// row menu, so the list never becomes empty afterwards.
// Seed the default '*' entry for a new domain once its rp-id is known,
// so the list always shows what is allowed ('*' = the domain and all its
// subdomains, any scheme/port). Removing the last in-domain entry is
// blocked in the row menu, so the list never becomes empty afterwards.
watch(dialogRpId, rp => {
const d = props.dialog?.data
if (props.dialog?.type !== 'domain-edit' || !d?.isNew) return
if (!d.origins.length && isWellFormedDomain(rp)) {
d.origins.push('*.' + rp)
d.origins.push('*')
d.originValidation.push(null)
}
})
@@ -236,9 +238,10 @@ function setAuthHost(i) {
const d = props.dialog?.data
if (!d) return
let key = entryKey(d.origins[i])
if (key.startsWith('*.')) {
if (key === '*' || key.startsWith('*.')) {
// A wildcard cannot be the auth host — create a concrete auth.<base> entry
key = 'auth.' + key.slice(2)
const base = key === '*' ? dialogRpId.value : key.slice(2)
key = 'auth.' + base
if (!d.origins.some(o => entryKey(o) === key)) {
d.origins.push(key)
d.originValidation.push(null)
@@ -387,7 +390,7 @@ function onRemoveOrigin(i) {
<p v-else-if="dialog.data.originValidation.some(v => v === 'mismatch')" class="small muted">Some sites are reachable but do not serve this domain.</p>
</div>
<p class="small muted">
Only the listed sites may sign in with this domain's passkeys — <strong>*.{{ dialog.data.rp_id }}</strong> means the domain and all its subdomains.
Only the listed sites may sign in with this domain's passkeys — <strong>*</strong> means the domain and all its subdomains on any scheme and port; <strong>*.{{ dialog.data.rp_id }}</strong> restricts that to https.
Entries on other domain names become related origins (WebAuthn ROR). The 🔑 site hosts the account and admin interface (set via ⋮).
</p>