@pegma/support-desk-application 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pegma contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,142 @@
1
+ # @pegma/support-desk-application
2
+
3
+ Authorized customer ticket services and provider-neutral durable outbox
4
+ records for Support Desk. The host supplies an `@pegma/storage-core` `Store`
5
+ and a trusted Authorization Core access context.
6
+
7
+ Customer create uses `support.ticket.create`; list and read both use the
8
+ documented `support.ticket.read.own` permission and then confirm authoritative
9
+ ownership; reply uses `support.ticket.reply.own`.
10
+
11
+ Staff services use the same application factory and ticket partition:
12
+
13
+ | Method | Permission(s) | Effect |
14
+ | ------------------------ | ------------------------------------------------- | ------------------------------------------------------------------- |
15
+ | `readStaffTicket` | `support.queue.read` | Authoritative ticket plus every message (including internal notes) |
16
+ | `listStaffQueue` | `support.queue.read` | Bounded projection scan; confirm each row; filter/sort in memory |
17
+ | `queueProjectionHealth` | (none; process-local) | Consecutive projection failures and last failure metadata |
18
+ | `replyAsStaff` | `support.queue.read` + `support.ticket.reply.any` | Customer-visible staff reply → `waiting_on_customer`; optional mail |
19
+ | `addNote` | `support.queue.read` + `support.ticket.note` | Internal note; no mail; status and `customerUpdatedAt` unchanged |
20
+ | `assignTicket` | `support.queue.read` + `support.ticket.assign` | Assign or unassign; status unchanged |
21
+ | `changePriority` | `support.queue.read` + `support.ticket.manage` | Priority only; status unchanged |
22
+ | `resolveTicket` | `support.queue.read` + `support.ticket.manage` | → `resolved` (closed tickets must reopen first) |
23
+ | `closeTicket` | `support.queue.read` + `support.ticket.manage` | → `closed` (resolved only) |
24
+ | `reopenTicket` | `support.queue.read` + `support.ticket.manage` | resolved/closed → `waiting_on_support` |
25
+ | `readTicketAuditHistory` | `support.audit.read` | Ordered Audit history for one ticket |
26
+
27
+ ### Staff queue projection
28
+
29
+ `support-desk.queue-index.v1` holds one projection row per ticket (partition =
30
+ ticket ID, id = `queue`). After every successful ticket-changing command the
31
+ application calls `projectTicketToQueue`, which reloads the authoritative
32
+ ticket and writes a revision-fenced row. Projection failure is logged and
33
+ reflected in `queueProjectionHealth`; it never rolls back the committed
34
+ command.
35
+
36
+ Online `listStaffQueue` starts at a null cursor, scans the projection for one
37
+ complete cycle with request-local cursors only, confirms every candidate
38
+ against the ticket, then filters and sorts in memory. Separate budgets
39
+ (`queueScanBudgets`: physical rows, pages, active results) fail closed with
40
+ `SupportDeskQueueCapacityError` and no partial result. Hosts schedule
41
+ `repairQueueProjectionPage` (authoritative `support-desk.records.v1` ticket
42
+ scan; persist cursor after each complete page) and
43
+ `sweepInactiveQueueProjections` (reload ticket, then `deleteIfUnchanged` when
44
+ still terminal beyond the shared cutoff). Both workers require an explicit
45
+ `terminalRetentionMilliseconds` — hosts must pass the same value configured on
46
+ the application so projection, repair, and sweep share one cutoff. The
47
+ projection is never ownership or permission evidence.
48
+
49
+ Staff reads return `StaffTicketView` (full `Ticket` and all `TicketMessage`
50
+ rows). Mutations that return that view also require `support.queue.read` so a
51
+ narrow write permission cannot bypass the staff-detail boundary. They do not
52
+ require queue membership claims or browser-supplied actor IDs—only an
53
+ `AccessContext` and the authoritative ticket id. Unknown ticket IDs throw the
54
+ same content-free not-found error used for customer ownership misses. A staff
55
+ reply to a closed ticket moves it directly to `waiting_on_customer` without a
56
+ separate reopen event (MVP lifecycle rule). Lifecycle commands may include an
57
+ optional customer-visible system message and notification so resolution mail
58
+ commits in the same ticket transaction as the status change. Partition reads
59
+ and mutations also enforce `limits.maxRecordsPerTicket` (default 512).
60
+
61
+ Customer create, list, read, and reply return explicit safe DTOs
62
+ (`CustomerTicketSummary` / `CustomerMessage` / `CustomerTicketView`), not the
63
+ authoritative `Ticket` or `TicketMessage`. Summaries include id, number,
64
+ subject, optional category, status, channel, `createdAt`, and
65
+ `customerUpdatedAt`. They omit requester evidence, priority, assignee,
66
+ staff-facing `updatedAt`, revision, audit history, and delivery state.
67
+ Customer messages omit principal IDs and provider threading metadata. List
68
+ order uses `customerUpdatedAt`. Internal notes never appear in customer views
69
+ or customer-visible notification content.
70
+
71
+ Hosts pass a frozen, deduplicated `allowedCategories` option (at most 32
72
+ values matching `^[a-z][a-z0-9_]{0,31}$`). A supplied create category must be
73
+ on that allowlist; category never changes authorization or initial priority
74
+ and is part of the create idempotency fingerprint. Category is preserved for
75
+ the ticket life.
76
+
77
+ Ticket numbers are reserved by the application from
78
+ `support-desk.ticket-numbers.v1` (`instance` / `ticket-number`) before create.
79
+ Controllers supply server-minted ticket and command IDs but not ticket numbers.
80
+ Replay returns the committed number without reserving another; gaps after a
81
+ failed create are accepted; exhaustion fails closed before ticket persistence.
82
+ When a create includes a notification, Support Desk sets the
83
+ `ticket_number` variable to the reserved number and substitutes
84
+ `{{ticket_number}}` in the notification subject.
85
+
86
+ `customerUpdatedAt` advances on create and on customer-visible messages or
87
+ lifecycle changes. Internal notes, assignment, and priority changes advance
88
+ only staff-facing `updatedAt` (in core workflow events).
89
+
90
+ Accepted-change history is exact `@pegma/audit@0.1.0` projected into the
91
+ ticket partition with `defineAudit`. Customer create/reply and every staff
92
+ mutation drop Audit transaction actions beside the state change; history is
93
+ read through `readTicketAuditHistory` / Audit, not a private sorter. Domain
94
+ `TicketEvent` remains pure workflow input and is not stored as a second audit
95
+ shape. Authorization Core is exact `0.1.2`.
96
+
97
+ Customer and staff commands are snapshotted from own data properties exactly
98
+ once before validation, idempotency fingerprinting, or persistence. Accessors
99
+ are rejected without being executed. Each mutation uses a server-minted
100
+ command ID, a SHA-256 request fingerprint, bounded conflict retry, and
101
+ trusted ticket time clamped so it never moves backward. Optional requester email is a contact
102
+ snapshot, never an identity key: surrounding whitespace is removed, the DNS
103
+ domain is lowercased, and plain-address syntax, controls, markup, and a
104
+ 254-character maximum are enforced.
105
+ Pruning and receipt-retention inputs use the same rule. Partition keys and
106
+ cutoffs therefore cannot change between a read and its conditional write or
107
+ delete.
108
+ Outbound `Message-ID` values must be at most 254 ASCII characters with a
109
+ dot-atom local part, a valid DNS domain, and no controls or malformed dots.
110
+
111
+ Outbound state is the application projection of published exact
112
+ `@pegma/mail@0.1.0`. Support Desk keeps its `delivery:*` physical record and
113
+ stores immutable template, subject, and `Message-ID` content on the causal
114
+ message in the same transaction. The generic package owns claims, provider
115
+ idempotency, submission generations, retry/reconciliation transitions,
116
+ terminal acknowledgement, authoritative collection-wide scans, and sweeping.
117
+ Hosts resolve a job's `contentRef` from the message record and keep send,
118
+ reconciliation, and terminal-sweep scan cursors separate.
119
+
120
+ This package implements persistence coordination, not persistence. It has no
121
+ provider SDK and no role, plan, or entitlement model.
122
+
123
+ Delivery callback recording also requires the host `Clock`: provider
124
+ `occurredAt` remains event data, while receipt retention and the enforced
125
+ 30-day deduplication horizon use trusted host processing time. Every callback
126
+ also carries the provider submission generation before it is delegated to
127
+ `@pegma/mail`, fencing delayed events from newer submissions.
128
+ Reply timestamps are sampled and validated on every transaction attempt, then
129
+ clamped to the stored ticket's `updatedAt` so clock skew cannot move a
130
+ conversation backward.
131
+
132
+ Inbound processing remains a future phase, but its declared receipt collection
133
+ is already hard-bounded to 256 hash slots per partition. Terminal receipt
134
+ retention uses trusted processing time, a 30-day deduplication horizon, and
135
+ version-conditional deletion; in-flight receipts are never swept.
136
+
137
+ ## Host composition
138
+
139
+ Map application errors to HTTP and schedule host-owned workers using
140
+ [docs/HOST_COMPOSITION.md](../../docs/HOST_COMPOSITION.md). A complete
141
+ memory-backed example is under
142
+ [examples/composition](../../examples/composition/README.md).