Full audit of every phone-number comparison/storage point across
pstn-trunk.sh, security-dashboard.sh, asterisk.sh, asterisk-digital-ocean.sh,
and the vendored easy-asterisk base script, prompted by the inbound
Caller-ID normalization fix — the same digit-count mismatch was also
possible on the admin-input side, just silent instead of loud:
- security-dashboard.sh's write_permission(): a 10-digit whitelist entry
was silently DROPPED (NUMBER_RE required exactly 11 digits), with no
warning unless every entry in the field was invalid — a mixed
10-digit + 11-digit list saved "successfully" while quietly losing the
10-digit one. Now normalizes any bare 10-digit token to 11-digit instead
of discarding it (_normalize_nanp_number).
- write_personal_did(): required exactly 10 digits, rejecting an
11-digit entry outright with a clear (but avoidable) error. Now accepts
either and normalizes to the canonical 10-digit storage form
(_normalize_personal_did_input).
- pstn-trunk.sh's TRUNK_DID install prompt: same fix, strips a leading
"1" instead of aborting the install over it.
Everything else checked out clean: outbound dialed-number matching
already normalizes via the existing _NXXNXXXXXX pattern (adds "1" before
any tier check), the area-code/country-code international gates aren't
phone numbers so digit-count doesn't apply, and asterisk.sh/
asterisk-digital-ocean.sh/the vendored base script have no phone-number
comparison logic at all — this class of bug only lives in the PSTN
permission/whitelist layer this project added on top.
allowed_numbers is always stored 11-digit (dashboard's NUMBER_RE requires
exactly 11 digits), but nothing normalized the inbound CALLERID(num) to
match. Confirmed live: Anveo delivered a bare 10-digit caller ID
(5557654321) for a call to a group-owned personal DID whose members'
allowed_numbers were correctly stored as 15557654321 — every restricted-
tier REGEX comparison failed on a plain digit-count mismatch, not because
the number was actually unauthorized. Outbound already guards against the
mirror-image case (_NXXNXXXXXX prepends "1" before any tier check); this
adds the equivalent for inbound via a new PSTN_CALLERID_NORM dialplan
variable, used everywhere CALLERID(num) previously fed a restricted-tier
comparison: the shared ring-group's per-member block, the single-owner
personal DID check, and the group-owned personal DID's ring script
invocation (which also gets a defensive normalize of its own, since it's
useful to invoke by hand for testing).
Verified by regenerating the actual group-ring script and running it
against the exact config dump and caller ID from the live failing call —
now correctly resolves all three group members instead of an empty list.
install_pstn-trunk's "update" branch unconditionally restarted the
asterisk container, then immediately called _pstn_ensure_live_includes,
which already patches the live pjsip.conf/extensions.conf #include lines
and reloads res_pjsip/dialplan — the restart accomplished nothing the
reload doesn't, while dropping any calls already in progress. Confirmed
live as a real double-restart when this runs chained from
asterisk-digital-ocean.sh, which had just rebuilt/restarted the same
container moments earlier.
Fresh installs keep their existing (still restart-first) apply step
unchanged — only "update in place" loses the redundant restart.
_ini_get() in _pstn_write_personal_group_ring_script split on '=' and
compared $1 == key without trimming whitespace, but configparser.write()
(used to write pstn-groups.conf and pstn-permissions.conf) pads '=' with
spaces by default. Every lookup — including the group's own members= line —
silently returned empty, so group-owned personal DID ring-groups never
actually rang anyone regardless of member tier/whitelist config. Trim
whitespace around both the extracted key and value before comparing.
Also switch the script's final echo to printf to remove any ambiguity in
the dialplan's SHELL()-empty-string check.
Confirmed live on a real droplet: secdash could read pjsip.conf fine but
/api/pstn-status kept returning false even though pstn-trunk-dialplan.conf
genuinely existed. Root cause: the Asterisk container's own entrypoint runs
`chown -R asterisk:asterisk /etc/asterisk` on every container start, and
the numeric UID/GID it resolves to inside the container coincidentally
collided with unrelated host system accounts (config/asterisk ended up
owned by messagebus:uuidd on this box) - silently reverting whatever group
membership _secdash_grant_asterisk_access had granted secdash at install
time. This wasn't a one-time misconfiguration; it would have silently
broken again on every future container restart.
Switched the grant mechanism from chmod + usermod group membership to
POSIX ACLs (setfacl) - chown doesn't touch ACL entries (only chmod
recalculates the ACL mask, and nothing in this flow chmods after install),
so the grant survives the container's own maintenance chown. Default ACLs
(-d) also make newly-created files (a regenerated dialplan, a fresh
personal-DID entry) inherit the same access automatically.
Also added _secdash_grant_ancestor_traversal, which grants execute-only ACL
traversal up the directory tree - needed on any box where DOCKER_DIR sits
under a restrictive parent (e.g. /root on some cloud images defaults to
700, blocking a non-root secdash from ever reaching deeper directories no
matter what those directories themselves grant). Falls back to the old
chmod/group approach with a warning if the 'acl' package is somehow
unavailable (it's installed automatically otherwise).
Verified with a real non-root system test user against a fixture
reproducing the exact failure (a leaf directory with no "other" access,
owned by an unrelated user/group): confirmed blocked before the fix,
confirmed read+write access after, and confirmed access survives a
simulated `chown -R` (the container restart scenario) plus correct
inheritance onto a freshly-created file afterward - all without re-running
the grant.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ho9mZgAkVpdz7S5wJkg8Nf
asterisk.sh and asterisk-digital-ocean.sh now offer, at the end of both
their fresh-install and update-mode paths, to also set up the Security
Dashboard and configure a real PSTN trunk in the same run - one script
walks through the whole stack instead of needing to separately remember
and run `sudo ./setup.sh security-dashboard` / `sudo ./setup.sh pstn-trunk`
afterward.
Both target services keep their own register_service call and stay fully
independently invocable - this is purely an additive convenience layer
(_asterisk_offer_dashboard_and_trunk / _asterisk_do_offer_dashboard_and_trunk),
not a replacement. An already-installed piece is silently refreshed
(install_security-dashboard/install_pstn-trunk each already have their own
update/fresh/cancel reinstall-mode gate, so calling them again just does
the right thing); a not-yet-installed piece gets one y/n instead of every
detailed prompt firing.
Guarded with declare -F so a standalone `sudo bash asterisk.sh` copy (no
sibling services/*.sh files sourced) skips both cleanly with an
explanatory message instead of erroring on an undefined function.
Verified: full sourcing simulation resolves all four install_* functions
correctly, `setup.sh --dry-run --unattended asterisk` and
`asterisk-digital-ocean` both complete cleanly end-to-end, and
`setup.sh --list` still shows all four services as independently
selectable.
Documented the pattern in CLAUDE.md under a new "Chaining into another
service from within your own" section for future contributors.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ho9mZgAkVpdz7S5wJkg8Nf
Supersedes the previous commit's reverse-proxy approach entirely: instead
of Caddy routing to Easy Asterisk's own separate vendored web admin
process, the dashboard now reimplements that admin's functionality
natively - one process, one page, real tab-switching, no separate app to
proxy, patch, or embed. Reverts asterisk.sh/asterisk-digital-ocean.sh's
WEBADMIN_BASE_PATH vendor patching and Caddy-skip logic back to their
pre-proxy state (confirmed identical via diff) since neither is needed
anymore.
security-dashboard.sh additions:
- ea_* functions covering full device/category/room parity with
vendor/easy-asterisk/easy-asterisk-v0.10.0.sh's own web admin: list/add/
delete/rename/change-category for devices, list/create/delete/rename for
categories, list/create/delete/rename/add-member/remove-member for
rooms, plus live registered/unregistered status. Reads go straight
through the host-side bind-mounted config files (same as the existing
list_extensions() already does for pjsip.conf); writes go through
`docker exec -i <container> tee <path>` instead of a direct host-side
write, since Easy Asterisk's container writes these files as its own
internal user and a host-side write would just be fighting that
ownership again on the next container restart.
- Found and fixed a real bug (inherited from the vendored admin's own
template, not introduced here): a plain non-mobile LAN device leaves
both the keepalive and ice template lines empty, producing two
consecutive blank lines inside the endpoint's pjsip.conf stanza instead
of one - which broke the delete/rename/category-change parsers' "blank
line ends this device's block" boundary detection, leaving an orphaned
tail of config behind on delete. Fixed by building the endpoint block
from a filtered line list instead of positional template blanks.
Confirmed via a full synthetic add/rename/category-change/delete cycle
against realistic pjsip.conf/categories.conf/rooms.conf fixtures (with
docker exec mocked to a local file) - round-trips back to the original
fixture correctly.
- New plumbing: _secdash_grant_asterisk_access grants read-only access to
categories.conf/rooms.conf's directory (separate from pjsip.conf's,
confirmed against the vendored source - /etc/easy-asterisk/*, not
/etc/asterisk/*); _secdash_write_sudoers adds six exact (no wildcards)
docker-exec sudoers entries scoped to the one Asterisk container
actually installed, validated live with visudo -c; _secdash_write_systemd_unit
passes the new ASTERISK_EA_CONFIG_DIR/ASTERISK_EA_CONTAINER env vars and
adds the config dir to ReadOnlyPaths, validated live with
systemd-analyze verify.
- New UI: Asterisk Admin tab with Devices/Categories/Rooms cards, sortable
tables matching the existing style, inline category-reassignment
dropdowns, and per-room member chips with an inline add-member picker.
Nav button visibility now checks live container reachability
(/api/ea-status) instead of just Asterisk-install detection.
Still unverified: the actual `docker exec` calls (module reload, dialplan
rebuild, live status) against a real running Easy Asterisk container -
only the file-parsing/transformation logic itself has been exercised, via
mocked writes, not the real container plumbing.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ho9mZgAkVpdz7S5wJkg8Nf
No more cross-origin iframe: the Security Dashboard now reverse-proxies
the real Asterisk web admin natively at /asterisk-admin/ on its own
domain via Caddy's handle_path, instead of embedding a separate site in
a frame. One domain, one login wall, for both.
- services/asterisk.sh / services/asterisk-digital-ocean.sh: patch the
vendored web admin's one hardcoded absolute API path
(`const API_BASE = '/api'`, confirmed via the real vendored source to
be the only absolute-path reference anywhere in its HTML/JS - no other
hrefs, no login-page redirect, plain HTTP Basic Auth instead) so it
resolves correctly when mounted under a sub-path, via a new
WEBADMIN_BASE_PATH env var threaded through entrypoint.sh. Verified
against the real vendored file: patched output is
'/asterisk-admin/api' with the env var set, unchanged '/api' without
it. Skip each service's own dedicated admin Caddy domain when the
Security Dashboard is already installed, since it fronts the admin
instead.
- services/security-dashboard.sh: _secdash_configure_caddy now accepts
the admin's port and Asterisk's own directory/domain, path-routes
/asterisk-admin/* alongside the dashboard's own handle{} block, and
writes WEB_ADMIN_BASE_PATH into Asterisk's .env + restarts that
container once proxying is confirmed live. Defaults the dashboard's
own domain prompt to the droplet's DOMAIN_NAME when detected, since
Caddy's SIP-TLS cert sync already depends on serving that exact
domain. Removed the old CSP frame-ancestors patching and the iframe
itself; the nav is now a plain link, shown only once the proxy is
confirmed wired up.
- Fixed _secdash_remove_caddy_block's marker match to tolerate the
dashboard's reverse_proxy line now living one indent level deeper
(inside its own handle{} block) - verified against a synthetic
Caddyfile that it still finds and removes exactly the right block.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ho9mZgAkVpdz7S5wJkg8Nf
Restructure the nav so it reflects what's actually installed on this box,
letting one dashboard URL cover everything from a bare LAN Asterisk box up
to a full droplet with a trunk and CrowdSec:
- Security Log and a new Extensions tab (Groups + Internal SIP messaging,
split out of the old "PSTN Trunk" tab) are always available - they only
need Asterisk itself, not a trunk or CrowdSec.
- Asterisk Admin, PSTN Trunk, and CrowdSec each check their own live
install state on every page load and hide their own nav button entirely
when not present, instead of showing an empty/placeholder tab.
- Add crowdsec_installed() (checks for /usr/bin/cscli) and a
/api/crowdsec-status endpoint, mirroring the existing pstn_installed()/
/api/pstn-status pattern.
This fixes the earlier design where messaging/groups management lived
inside the PSTN Trunk tab even though both work with plain Asterisk and no
trunk at all - hiding that tab would have taken them down with it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ho9mZgAkVpdz7S5wJkg8Nf
- Security Log tab: replace the per-column text filter row with clickable
sortable column headers (same pattern as the CrowdSec bans table).
- PSTN Trunk tab: add sortable headers to the permissions, personal-DID
(numeric on DID), and groups tables.
- Move the "Internal SIP messaging" card to the bottom of the PSTN tab and
replace its per-row table+Save-button layout with a checkbox chip row
and a single Save-changes button.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ho9mZgAkVpdz7S5wJkg8Nf
The dashboard's Personal Numbers card now accepts a group (stored as
"@GroupName", unambiguous against a same-named numeric extension) as well
as a plain extension. A group-owned DID rings every CURRENT member whose
own tier/approved-numbers authorize the caller, computed fresh on every
call by a generated pstn-personal-group-ring.sh (invoked via the
dialplan's SHELL() function) rather than unrolled at install time, since
group membership can change any time via the dashboard with no reinstall
- unlike the shared ring-group, which is fixed at install/update time.
Applies the identical per-member permission check the shared ring-group
already bakes into the dialplan, just computed in a plain shell loop
against the same two config files - group ownership doesn't bypass the
tier/approved-numbers model. Tested standalone against mock config data
(full/restricted/internal mix, matching/non-matching caller, empty and
nonexistent groups) - all four cases behaved correctly. The dialplan's own
SHELL() invocation is still unverified against a real call.
Group ownership never touches pstn-permissions.conf's personal_did
(outbound Caller-ID override) field, since there's no single extension to
hang that on for a group.
Replaces the static wall of warnings with a real 5-step guided walkthrough
(account/funding, DID ordering, both trunk objects, confirmed rate),
pausing for confirmation between each step and showing this box's actual
public IP inline for direct copy-paste into Anveo's Authorized IP
Addresses and SIP Trunk forwarding fields. Built directly from
docs/anveo-direct-setup-guide.md now that the whole flow is confirmed
working end-to-end, so a fresh run shouldn't need the trial-and-error
session that guide was written from.
Documents the actual end-to-end sequence that got a real DID working
(account setup, DID ordering, both trunk objects, droplet configuration,
permission setup, testing), including the real confirmed values (the
$0.00388/min rate, the port-5060 SIP URI forwarding format, the two
separate trunk-object types) and a summary of every real bug hit and
fixed along the way. Linked from the existing planning-notes doc.
Confirmed live: PSTN_PERSONAL_OWNER came back empty despite a real
assignment existing, because the Security Dashboard stores personal DIDs
as 10 digits (PERSONAL_DID_RE = ^\d{10}$) while Anveo's inbound INVITE
delivers the called number as 11-digit E.164 (with leading 1) in ${EXTEN}
- confirmed via PSTN_DID_CALLED=15557776655 in the live test. AST_CONFIG()
looking up an 11-digit key never found the 10-digit section.
Normalizes to 10 digits before the lookup (strips a leading digit only
when the string is actually 11 characters, so this doesn't misfire against
a provider that already sends 10). Also fixed the dashboard's own DID
input placeholder, which contradicted its own 10-digit validation error.
Confirmed live: a ring-group list with a repeated extension ("201 301 313
312 313") generated duplicate ring<ext>/skip<ext> priority labels in the
same [from-pstn-trunk] extension. Asterisk doesn't error on this - it
silently resolves Goto() to the wrong occurrence and loops between the two
blocks forever, so the call never reaches the actual Dial() and nothing
ever rings, with no error to point at the cause. Dedupes (first-occurrence
order preserved) regardless of how the duplicate got in there.
Confirmed live: Dial() actually placed the call to "PJSIP/pstn_check_busy@
pstn-trunk" instead of the dialed number. Root cause is systemic, not one
line — Goto(label,1) to a *different named extension* (as opposed to a
same-extension priority label, which _pstn_ring_member_block already knew
to avoid) resets ${EXTEN} to that label's own name. Every helper extension
downstream of a Goto (pstn_check_allow_out, pstn_check_busy,
pstn_intl_check_country, pstn_intl_country_denied, pstn_personal_inbound)
was reading ${EXTEN} expecting the originally-dialed number and getting
its own extension name instead.
This wasn't just cosmetic: pstn_check_allow_out's REGEX approved-number
check compared against the label name too, meaning restricted-tier
extensions could never successfully place a call regardless of whether
the number was actually approved.
Fixed by capturing the real dialed number into PSTN_DIALED (outbound) /
PSTN_DID_CALLED (inbound) at each entry point, before any Goto, and using
that instead of ${EXTEN} in every downstream extension.
Confirmed live: "Unknown or unavailable item requested: 'peername'" left
PSTN_CALLER empty, which made AST_CONFIG() error ("requires a category")
and PSTN_TIER come back empty too — every call fell through to "no PSTN
permission" and got busy-signaled regardless of the extension's actual
tier. Replaced with CUT(CHANNEL,/,2) then CUT(...,-,1), extracting the
endpoint name from the channel name itself (e.g. "PJSIP/201-00000006" ->
"201") — a plain string operation with no dependency on which channel
function items this particular Asterisk build supports.
_1NXXNXXXXX / _NXXNXXXXX were one digit short (10/9 characters instead of
the correct 11/10 for a full NANP number: 1 + area code(3) + exchange(3) +
subscriber(4)). Asterisk's exact-length pattern matching never matched a
real dialed number as a result — confirmed live via "extension not found
in context 'intercom'" on every outbound test call, traced by dumping
`dialplan show intercom` and counting characters against the actual
pattern. This was the real root cause underneath everything else fixed
along the way (stale settings file, the #include not reaching the live
config) — those were real bugs too, but this one was still there
underneath all of them.
setup.sh runs with set -uo pipefail, and source-ing a settings file with
any stray unexpanded/unset variable reference (confirmed live on an older
deployed copy) was fatal to the entire script, not just that one prompt.
Wraps the source in set +u/-u and falls back to sane defaults for anything
missing, but still hard-errors with a clear message if TRUNK_SERVER/
TRUNK_DID themselves are missing, rather than silently writing a broken
trunk config.
Ports the internal SIP MESSAGE dialplan enforcement, extension presence
(online/offline) ntfy alerts, and the live-config-include patching from
asterisk-digital-ocean.sh, for feature parity between the two Asterisk
flavors. The pstn-trunk.sh live-include fix already applied to both, since
that's shared code parametrized by which flavor is installed.
Removed the full/restricted-extension, approved-numbers pool/whiptail,
messaging-extensions, and personal-DID-assignment prompts from the
installer. All four are already live-editable, no-restart-needed settings
the Security Dashboard's PSTN Trunk tab manages end to end — the CLI wall
of prompts (that then needed a full reinstall to change) just duplicated
that with more friction. Every extension now starts at internal tier (no
PSTN, no messaging) until granted via the dashboard. Kept: provider/DID
setup, concurrency caps, and the inbound ring-group (basic trunk wiring,
not a permission). Also updated the generated README and CLI summary to
match, and refreshed the stale "known gap" messaging section that predated
the dialplan enforcement built earlier this session.
Confirmed live via a real failed test call ("extension not found in context
'intercom'"): Easy Asterisk's entrypoint only regenerates pjsip.conf/
extensions.conf if they don't already exist. Patching the vendor generator
functions (as both pstn-trunk.sh and the earlier SIP MESSAGE work did) only
takes effect on a future full regeneration — a box that already has devices
configured, the normal case, never triggers one on a plain restart, so the
#include lines never actually reached the live files despite the generator
patch succeeding.
Both installers now also patch the live pjsip.conf/extensions.conf directly
(same anchors, idempotent) and force a reload, so the trunk/messaging
dialplan actually takes effect immediately regardless of whether Easy
Asterisk ever regenerates its config on its own.
0.00388/min for standard US-to-US domestic, confirmed against Anveo's own
Prime rate card CSV (the route set the outbound trunk's LCR config actually
pulls from) rather than the earlier ~0.001 ballpark guess. Defaults to it
automatically when the Anveo Direct quick-pick was chosen.
Restricted extensions previously required retyping the full 11-digit
number list from scratch at each extension's own prompt. Now offers
entering a pool once, then a whiptail checklist per extension to pick a
subset (falls back to typing numbers directly, or 'all' for the whole
pool, when whiptail isn't available or the run is unattended).
User feedback: reading the tier list (internal/restricted/full) right before
"Full-PSTN extensions:" read as ambiguous about whether to type the tier
word or extension numbers. Reworded both prompts to say "extension NUMBERS"
explicitly and added inline examples (e.g. '999 213').
Confirmed live: pressing Enter on any "(y/n): y"-style prompt set the
variable to an empty string instead of the stated default, since prompt_yn
had no ${response:-$default} fallback (prompt_text already had one). Every
downstream [[ "$VAR" =~ ^[Yy]$ ]] check treated "just press Enter" as no.
Also shows the default value in the prompt text itself for both helpers,
since neither displayed it before.
The dialing-prefix ambiguity is resolved: Anveo's own trunk edit screen has
no required-field marker on Dialing Prefix, confirming it's optional. Also
documents the separate Outbound Service Trunk vs. inbound SIP Trunk
distinction discovered while walking through a real account setup.
Confirmed against a live install's pjsip.conf/extensions.conf that every
endpoint falls back to context=intercom for messaging (message_context
blank), and that [intercom] owns one exact-match per-device dial pattern
regenerated on every dialplan rebuild. Rather than risk racing that, every
endpoint now gets message_context=sip-messaging (patched into both of Easy
Asterisk's device-creation code paths, plus a one-time migration for
existing devices), routing messages to their own [sip-messaging] context
gated on the existing pstn-permissions.conf messaging flag.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ho9mZgAkVpdz7S5wJkg8Nf
- ntfy.sh: auth-default-access was deny-all, silently blocking both publish
and anonymous subscribe on every topic once an auth-file is present.
Default to read-write (same model as public ntfy.sh) so alerts from
crowdsec.sh/pstn-trunk.sh actually get delivered.
- crowdsec.sh: add update/fresh/cancel reinstall gating — reruns previously
re-asked every optional prompt (ASN exempt, geo-allowlist, ntfy, remote
LAPI) unconditionally with no way to just refresh in place.
- asterisk-digital-ocean.sh: add optional ntfy alerts on extension
registration going offline/online, checked every 2 minutes via systemd
timer (cron.d fallback), offered on both fresh install and update.
- security-dashboard.sh: replace the outbound-only Asterisk Web Admin link
with an embedded, lazy-loaded iframe tab, with a best-effort Caddy
frame-ancestors patch and an always-available "open in new tab" fallback.
Groups: a new always-available "Groups" card in the PSTN tab (same place
as Internal SIP messaging, no dependency on a PSTN trunk being installed)
lets you name a set of extensions and bulk-enable/disable messaging for
all of them at once. Purely a management-layer convenience - pstn-groups.conf
is never read by the dialplan, which only ever looks at per-extension keys
in pstn-permissions.conf. Applying a group action just calls the same
write_messaging() each individual checkbox uses, once per current member.
Editing membership never retroactively changes anything already applied,
and deleting a group never touches members' own settings - confirmed with
tests covering create/apply/edit-membership/re-apply/delete.
Security Log: added a per-column filter row (Time/Event/Account/Remote/
Severity), live as you type, filtering client-side against the
already-fetched events rather than re-querying - persists correctly across
the existing 30-second auto-refresh.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ho9mZgAkVpdz7S5wJkg8Nf
Messaging has no dependency on a PSTN trunk existing (no cost, no
carrier, no DID), but the whole PSTN Trunk tab - including the messaging
checkbox added last commit - was hidden behind pstn_installed(), which
only becomes true once services/pstn-trunk.sh's dialplan is actually
wired in. That meant enabling messaging required going through full SIP
trunk/provider setup first for no real reason.
Added a standalone "Internal SIP messaging" card that's always visible in
the tab regardless of trunk status, backed by a new write_messaging()
that only touches the messaging key (leaving tier/allowed_numbers/
personal_did untouched) and creates pstn-permissions.conf from scratch if
it doesn't exist yet. Confirmed the systemd unit's ReadWritePaths and the
group/chmod grants already covered this - both are set up whenever a base
Asterisk install is detected, independent of pstn-trunk - so no
permission-layer changes were needed, only the dashboard's own artificial
UI gate.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ho9mZgAkVpdz7S5wJkg8Nf
get_all_permissions()/write_permission() now handle messaging alongside
tier/allowed_numbers as one save action, and the Security Dashboard's PSTN
Trunk table gets a Messaging checkbox column - no more needing to re-run
pstn-trunk.sh's CLI just to change who can use internal SIP texting,
matching how tier/allowed_numbers/personal_did already worked.
Tested that messaging correctly survives tier changes and personal-DID
assignment/removal on the same extension (independent axes, as intended).
Still explicitly not done, and said so in both READMEs rather than
implying otherwise now that there's a nice UI for it: the actual SIP
MESSAGE dialplan wiring that would make Asterisk enforce this flag. That
gap hasn't changed - only the permission storage/UI layer around it has.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ho9mZgAkVpdz7S5wJkg8Nf