frontpage-host 0.2.1 → 0.2.2
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/CUSTOMER-INBOX.md +10 -2
- package/dist/commands/customer-inbox.js +9 -9
- package/package.json +1 -1
package/CUSTOMER-INBOX.md
CHANGED
|
@@ -60,7 +60,7 @@ Lock a knowledge note with `locked:true`. The support agent cannot change locked
|
|
|
60
60
|
| --- | --- | --- |
|
|
61
61
|
| `conversations` | GET | Paginated list; `?limit=50&after=<id>` |
|
|
62
62
|
| `conversations/<id>` | GET | Thread and latest public message |
|
|
63
|
-
| `conversations/<id>/messages` | GET | `?
|
|
63
|
+
| `conversations/<id>/messages` | GET | `?timeline=true&limit=50&audience=all`; follow the returned message-ID `nextCursor` with `after`; viewers get public history only |
|
|
64
64
|
| `conversations/<id>/routing` | GET | Current handler/version, People link and delivery states |
|
|
65
65
|
| `conversations/<id>/reply` | POST | `{"text":"Tuesday at 6:30 works. Keep staffing private.","handlerVersion":0,"privateGuidance":true}` |
|
|
66
66
|
| `conversations/<id>/control` | POST | `{"action":"takeover","handlerVersion":0}` or `{"action":"return","handlerVersion":1}` |
|
|
@@ -68,6 +68,10 @@ Lock a knowledge note with `locked:true`. The support agent cannot change locked
|
|
|
68
68
|
| `files?messageId=<id>` | GET | Bound message files; editor/admin only |
|
|
69
69
|
| `files/<id>` | GET | Short-lived download grant; editor/admin only |
|
|
70
70
|
|
|
71
|
+
One verified customer has one continuous thread per site, across direct email, replies and verified website chat. Email subject changes do not create new cases. Closing reopens that same thread on the next message; Trash needs owner restoration. Inbox joins retained history without moving messages, files or paid receipts. Old conversation links and recipient-bound reply addresses resolve to the current thread. Public chat includes customer-visible email replies and excludes owner guidance and notes.
|
|
72
|
+
|
|
73
|
+
Use `timeline=true` for combined history. Its opaque `nextCursor` is a message ID, not a sequence or offset; pass it unchanged as `after` or `before`. `latest=true` returns the latest bounded page, and `before=<cursor>` loads older messages. A cursor must belong to that conversation and audience. Sequence-based reads without `timeline=true` remain available for the root segment; use the combined timeline for every linked segment. The CLI export follows the complete combined timeline. The widget opens the latest 50 public messages and offers **Load earlier messages**, so a returning customer does not wait for years of history to download. The support agent's `history_read` tool likewise follows its returned `nextBefore` message ID.
|
|
74
|
+
|
|
71
75
|
Private guidance stays in the same internal thread. The agent asks by a private email and stores its question in the history. Replying by email or UI gives context and leaves the agent responsible for the customer. The agent uses that context to answer and can update unlocked internal knowledge.
|
|
72
76
|
|
|
73
77
|
General owner facts, such as support hours and standard policies, update related unlocked knowledge without an explicit save request. One-time customer exceptions and private staffing details stay in the conversation. The agent reads the full note and records verified evidence before changing it. Late private guidance can still update knowledge after the customer has an answer; that work cannot send a duplicate customer reply. Locks, manual handling, paused automation and work limits still apply.
|
|
@@ -178,12 +182,14 @@ const identity = encoded + "." + createHmac("sha256", process.env.INBOX_IDENTITY
|
|
|
178
182
|
|
|
179
183
|
The service verifies the signature, site, origin and expiry. Assertions can last at most five minutes. They grant customer identity only, never owner or manager authority. Every app-session request must include the current signed identity matching that session's subject and email. Put a server-rendered assertion in `data-identity`, or preferably provide `data-identity-url="/api/support-identity"`. That same-origin HTTPS endpoint authenticates the current app user and returns `{"identity":"<signed assertion>"}`. The widget refreshes it; after logout or account changes, old app sessions cannot be read without the matching identity. Anonymous widgets need no assertion. Each approved origin has its own visitor session; email continues the bound conversation.
|
|
180
184
|
|
|
181
|
-
Browser access lasts 30 days from creation or successful email verification/recovery. Verification links last one hour and work once; confirming rotates the credential. The widget retains access in that browser's storage and keeps it through temporary network failures. Returning browsers with valid access reopen their history automatically. When access is missing,
|
|
185
|
+
Browser access lasts 30 days from creation or successful email verification/recovery. Verification links last one hour and work once; confirming rotates the credential. The widget retains access in that browser's storage and keeps it through temporary network failures. Returning browsers with valid access reopen their history automatically. When access is missing, ordinary **Continue** and email confirmation select the verified customer's continuous public-support conversation for that site. Both empty and nonempty provisional chats join only after proof; approved public origins can share the customer history through their own verified sessions. Typed email never restores history. Revoked credentials remain revoked; fresh proof does not reactivate an old token. Signed app identities remain separate by site, origin and subject. The recovery endpoint remains available, but customers need no separate Recover workflow. An empty unverified saved session with an email requests confirmation automatically; a receipt prevents duplicate requests during the link's one-hour lifetime, then rotates so an expired link cannot block a fresh confirmation.
|
|
182
186
|
|
|
183
187
|
The same conversation's customer-visible email and chat messages load together, including owner replies relayed through Inbox. They stay stored after browser access expires. Private guidance, internal notes and system notices never enter that public history. Recovery restores access to the saved conversation rather than starting its history over.
|
|
184
188
|
|
|
185
189
|
## Conversation state, exports and shared usage
|
|
186
190
|
|
|
191
|
+
Use `frontpage-host` 0.2.2 or later to export every linked history segment through message-ID cursors. Update with `npm install -g frontpage-host@latest`.
|
|
192
|
+
|
|
187
193
|
| Resource | Method | Body or behavior |
|
|
188
194
|
| --- | --- | --- |
|
|
189
195
|
| `conversations?state=active` | GET | Open threads; also `closed`, `trash` or `all` |
|
|
@@ -237,6 +243,8 @@ The support agent has its own system prompt plus the versioned private business
|
|
|
237
243
|
| `wait` | `reason` | Pause safely without sending. |
|
|
238
244
|
| `finish` | None | Complete this bounded job. |
|
|
239
245
|
|
|
246
|
+
Read tools can run in one batch. Inbox defers `reply_customer`, `ask_owner`, `wait` and `finish` when the model requests them alongside other tools, so it observes the results before taking a terminal action. This does not change business permissions or allow extra work beyond the existing budget and step limits.
|
|
247
|
+
|
|
240
248
|
Decisions screens malicious customer input and interprets authenticated owner-email intent. It never chooses IDs, recipients, permissions, business answers, support tools or knowledge changes. Owner intent uses bounded labeled context, one primary Luna call, at most one bounded same-Luna control fallback, and the same exact-token, raw-cost, 10× ledger receipt as support. Shared inference utilities provide the existing same-model credential/transport fallback; no expensive model substitution or customer model picker is added.
|
|
241
249
|
|
|
242
250
|
The installed widget loads two public static scripts: `/customer-inbox/widget.js` and `/customer-inbox/view.js` on `https://app.frontpage.host`. The shared renderer contains no session, owner data or credentials. If your app sets a Content Security Policy, allow those script URLs and connections to `https://frontpage-inbox-api.fly.dev`; keep the existing widget origin and identity checks. Owner Inbox pages remain authenticated.
|
|
@@ -49,7 +49,7 @@ export async function runCustomerInbox(args) {
|
|
|
49
49
|
const prefix = `/api/v1/sites/${encodeURIComponent(slug)}/customer-inbox`;
|
|
50
50
|
if (operation.method === "DOWNLOAD") {
|
|
51
51
|
const record = await v1Get(`${prefix}/files/${operation.resource}`);
|
|
52
|
-
const response = await fetch(record.
|
|
52
|
+
const response = await fetch(record.downloadUrl, {
|
|
53
53
|
redirect: "error",
|
|
54
54
|
signal: AbortSignal.timeout(60000),
|
|
55
55
|
});
|
|
@@ -64,25 +64,25 @@ export async function runCustomerInbox(args) {
|
|
|
64
64
|
}
|
|
65
65
|
else {
|
|
66
66
|
const conversation = await v1Get(`${prefix}/conversations/${operation.resource}`);
|
|
67
|
-
await handle.writeFile(JSON.stringify({ type: "conversation", value: conversation
|
|
67
|
+
await handle.writeFile(JSON.stringify({ type: "conversation", value: conversation }) +
|
|
68
68
|
"\n");
|
|
69
|
-
let cursor =
|
|
69
|
+
let cursor = null;
|
|
70
70
|
for (;;) {
|
|
71
|
-
const page = await v1Get(`${prefix}/conversations/${operation.resource}/messages?
|
|
72
|
-
for (const message of page.
|
|
71
|
+
const page = await v1Get(`${prefix}/conversations/${operation.resource}/messages?timeline=true&limit=100&audience=all${cursor ? `&after=${cursor}` : ""}`);
|
|
72
|
+
for (const message of page.messages) {
|
|
73
73
|
const attachments = await v1Get(`${prefix}/files?messageId=${message.id}`);
|
|
74
74
|
await handle.writeFile(JSON.stringify({
|
|
75
75
|
type: "message",
|
|
76
76
|
value: message,
|
|
77
|
-
attachments
|
|
77
|
+
attachments,
|
|
78
78
|
}) + "\n");
|
|
79
79
|
count++;
|
|
80
80
|
}
|
|
81
|
-
if (page.
|
|
81
|
+
if (page.nextCursor === null)
|
|
82
82
|
break;
|
|
83
|
-
if (page.
|
|
83
|
+
if (page.nextCursor === cursor)
|
|
84
84
|
throw new Error("History cursor did not advance.");
|
|
85
|
-
cursor = page.
|
|
85
|
+
cursor = page.nextCursor;
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
await handle.sync();
|