@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 +21 -0
- package/README.md +142 -0
- package/dist/index.d.ts +535 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3048 -0
- package/dist/index.js.map +1 -0
- package/package.json +45 -0
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).
|