Add personal/per-extension DID assignment ("their own numbers")

Multiple DIDs can now share one trunk/account, each assigned to a specific
extension - additive to the existing shared trunk DID/ring-group, which
keeps working unchanged for everyone regardless of what's assigned here.

- New pstn-personal-dids.conf (DID -> owner extension), read live by the
  dialplan for inbound routing: a call to a personal DID routes straight
  to its owner, checked against the owner's own tier/approved-numbers, no
  ring-group fallback since it's that extension's own line.
- New personal_did= field per extension in pstn-permissions.conf: the
  outbound Caller-ID override, used by pstn_check_busy (the shared exit
  point for both domestic and international dialing) instead of the
  shared trunk DID when the calling extension has one assigned.
- Both files kept in sync automatically by one write path - CLI prompt at
  install/update, or a new "Personal numbers" card in the Security
  Dashboard's PSTN Trunk tab - rather than requiring hand-editing both
  consistently. Reassigning a DID or giving an extension a new one cleanly
  drops the stale side of the old mapping.
- Assigning a DID to an internal-tier extension is accepted but warned
  about (won't ring anyone until also granted full/restricted tier),
  matching this repo's permissive-with-warnings style rather than
  blocking the action outright.

Also fixed a real bug found while building this: security-dashboard.sh's
write_permission() did cp.remove_section(ext) whenever tier was set to
"internal", silently discarding any messaging=yes or personal_did=
already set on that extension. Now removes only the tier/allowed_numbers
keys, dropping the section only once nothing else remains in it.

Separately, de-anchored pstn-trunk.sh's install prompts from VoIP.ms as
the implicit default (provider name default, server-hostname example
wording, rate-prompt wording) now that Anveo Direct is an equally
confirmed, tested provider option.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ho9mZgAkVpdz7S5wJkg8Nf
This commit is contained in:
Claude
2026-07-23 05:03:13 +00:00
parent 616dcdcfbb
commit fcb01a4d93
3 changed files with 442 additions and 52 deletions
+29
View File
@@ -475,3 +475,32 @@ generator output. Fixed by quoting every value in that heredoc.
`README-pstn-trunk.md` for the full honest breakdown of what's actually
hard (the provider's own $0-balance block) vs. estimate-based (this
kill-switch).
15. ~~Multiple DIDs, one per extension ("personal numbers")~~ Done —
additive to the existing shared trunk DID/ring-group, not a
replacement. Anveo Direct's DID pricing (~$0.15/mo + $0.25 setup on the
Per Minute plan) makes "everyone gets their own number" genuinely cheap
at personal-use volumes, and multiple DIDs sharing one trunk/account is
exactly what that plan is built for (10 dedicated incoming channels
bundled in). New `pstn-personal-dids.conf` (DID -> owner extension,
read live by the dialplan for inbound routing) plus a `personal_did=`
field per extension in `pstn-permissions.conf` (the outbound Caller-ID
override) — both kept in sync automatically by a single write path
(CLI prompt at install/update, or the Security Dashboard's "PSTN
Trunk" tab), never requiring the admin to hand-edit both files
consistently. Inbound: a call to a personal DID routes straight to its
owner, checked against the *owner's own* tier/approved-numbers — no
ring-group fallback, since a personal DID isn't the shared line.
Outbound: `pstn_check_busy` (the one shared exit point for both
domestic and international dialing) looks up the calling extension's
`personal_did` and uses it as `CALLERID(num)` instead of the shared
trunk DID when one is assigned. A personal DID assigned to an
internal-tier extension is accepted but silently never rings anyone
until that extension is also granted full/restricted tier — both the
CLI and the dashboard warn about this at assignment time rather than
blocking it, matching this repo's general permissive-with-warnings
style. Caught and fixed a real bug while building this: the
dashboard's `write_permission()` did `cp.remove_section(ext)` when
tier was set to "internal", which silently discarded any
`messaging=yes` or `personal_did=` already on that extension — fixed
to remove only the tier/allowed_numbers keys, dropping the section
only once nothing else is left in it.