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 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 | `?after=0&limit=50&audience=all`; viewers get public history only |
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, the ordinary **Continue** and email-confirmation flow can reconnect an empty replacement session to the latest open conversation with a matching verified email on that same website origin. A typed email alone never restores history. Nonempty conversations remain separate; revoked access and authenticated app identities cannot use this path. The developer recovery endpoint remains available, but customers do not need a separate Recover workflow. Email continuation uses an inline email field. An empty unverified saved session with an email requests confirmation automatically; a stable receipt prevents reloads from sending duplicate emails.
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.data.downloadUrl, {
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.data }) +
67
+ await handle.writeFile(JSON.stringify({ type: "conversation", value: conversation }) +
68
68
  "\n");
69
- let cursor = 0;
69
+ let cursor = null;
70
70
  for (;;) {
71
- const page = await v1Get(`${prefix}/conversations/${operation.resource}/messages?after=${cursor}&limit=100&audience=all`);
72
- for (const message of page.data.messages) {
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: attachments.data,
77
+ attachments,
78
78
  }) + "\n");
79
79
  count++;
80
80
  }
81
- if (page.data.nextCursor === null)
81
+ if (page.nextCursor === null)
82
82
  break;
83
- if (page.data.nextCursor <= cursor)
83
+ if (page.nextCursor === cursor)
84
84
  throw new Error("History cursor did not advance.");
85
- cursor = page.data.nextCursor;
85
+ cursor = page.nextCursor;
86
86
  }
87
87
  }
88
88
  await handle.sync();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frontpage-host",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Frontpage CLI — manage your site from the terminal",
5
5
  "type": "module",
6
6
  "bin": {