frontpage-host 0.1.27 → 0.1.28

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/PEOPLE.md CHANGED
@@ -338,18 +338,78 @@ Copy the attachment’s returned rule ID before enabling it. Pass `--enabled fal
338
338
 
339
339
  See the [Email guide](/developers/mail) for content blocks, template variables, series step delays, sender settings, and email limits.
340
340
 
341
- ## Email permission is independent of groups
341
+ ## Marketing email permission
342
342
 
343
- Group membership never grants email permission. A person who opts out stays in their groups and stages, while automated marketing emails and campaigns stop. Every delivery checks permission again, so an unsubscribe after audience selection still stops delivery.
343
+ Permission belongs to the email address and site. Groups and stages never grant it. A person who opts out stays in their groups and stages. Campaign selection, rules, direct-address sends, series and delivery enforce saved withdrawals and restrictions.
344
+
345
+ ### Defaults and explicit choices
346
+
347
+ | Entry point | New contact | Existing contact |
348
+ | --- | --- | --- |
349
+ | Owner UI, `people add`, public API `POST /people`, agent `add_person` | `allowed` by default, recorded as an owner assertion | Omitted choice preserves the saved decision. Explicit choices can fill unknown permission; add cannot reverse a recorded refusal, mailing-list unsubscribe or suppression. |
350
+ | CSV import | Unknown unless an explicit list-wide choice or row value supplies permission | List-wide choices leave existing decisions alone. Row values can record permission, but cannot reverse a refusal, unsubscribe or suppression. |
351
+ | Forms, orders, bookings and account events | Unknown unless a supported subscriber signup records agreement | Preserve saved permission; purchases and signup timestamps are not email agreement. |
352
+ | Group, stage, name, phone, note and follow-up changes | No permission change | No permission change. Changing an email does not transfer an old address's grant; a recorded refusal stays in effect. |
353
+
354
+ The add choice is `allowed`, `unknown`, or `not_allowed`. API and agent callers use the same `emailPermission` string field. CLI callers use `--email-permission`. Recording `allowed` asserts that you have permission; it does not obtain permission from the person.
344
355
 
345
356
  ```
346
- frontpage people person permission --person PERSON_ID --allowed false --site YOUR_SITE --json
357
+ # New owner-added contacts default to permission given. Repeated adds preserve decisions.
358
+ frontpage people add --email alex@example.com --site YOUR_SITE --json
359
+ # Choose unknown when you do not have a recorded agreement:
360
+ frontpage people add --email pat@example.com --email-permission unknown --site YOUR_SITE --json
361
+ # Record refusal without enabling marketing:
362
+ frontpage people add --email sam@example.com --email-permission not_allowed --site YOUR_SITE --json
347
363
  frontpage people membership get --person PERSON_ID --site YOUR_SITE --json
348
- # Only record permission when the person has actually given it:
364
+ # Explicitly stop or record a new owner decision on the person:
365
+ frontpage people person permission --person PERSON_ID --allowed false --site YOUR_SITE --json
349
366
  frontpage people person permission --person PERSON_ID --allowed true --site YOUR_SITE --json
350
367
  ```
351
368
 
352
- `membership.get` includes email eligibility and the reason a contact cannot receive mail. Missing email, hidden state, missing permission, saved unsubscribe, and suppression can block delivery. An owner can record a new permission decision, but cannot reverse a mailing-list unsubscribe or suppression. CSV values cannot reverse an existing opt-out, including a recorded false decision. Do not add a permission check to every group or stage; delivery already enforces it.
369
+ `membership.get` reports eligibility and the reason mail cannot send. A missing/invalid email, hidden person, missing permission, recorded refusal, subscriber unsubscribe or suppression can block marketing. An active subscriber agreement can establish permission even without a separate CRM flag. A requested test copy can go to its test recipient without a recorded grant or a new subscription. Saved withdrawals, hidden state and suppression still apply. Transactional confirmations keep their own existing sending flow.
370
+
371
+ An explicit address outside People is an owner-asserted recipient and does not create a CRM permission record. If that address matches a saved person, the normal CRM eligibility check applies. Known subscribers' unsubscribe and saved suppressions also apply when no CRM person matches. Once queued, a known person's ID stays attached so a later address change, hide or withdrawal prevents delivery. A test is not a way to send a customer campaign.
372
+
373
+ New CRM decisions store state, exact email, source, actor and recording time in `attrs.emailPermissionRecord`; the boolean `attrs.emailPermission` stays compatible with existing callers. Historical grants without that record remain supported. Permission events preserve the decision history. Do not add a permission condition to every rule; the sender already checks it.
374
+
375
+ ### Repair reviewed historical permission without running rules
376
+
377
+ Review each person's current address, agreement evidence, saved withdrawals and restrictions first. This operation only fills missing permission. It never adds a person, changes their groups/stages, clears a restriction, or replays automation.
378
+
379
+ Save `repair.json` with real values:
380
+
381
+ ```json
382
+ {
383
+ "personId": "PERSON_ID",
384
+ "allowed": true,
385
+ "backfill": true,
386
+ "expectedEmail": "alex@example.com",
387
+ "evidence": "Owner confirmed this person's marketing-email agreement on 2026-10-12",
388
+ "requestKey": "UNIQUE_REPAIR_UUID"
389
+ }
390
+ ```
391
+
392
+ ```
393
+ frontpage people person permission --file repair.json --site YOUR_SITE --json
394
+ frontpage people membership get --person PERSON_ID --site YOUR_SITE --json
395
+ ```
396
+
397
+ Reuse the same key and evidence after a lost response. A repeat returns `duplicate:true` without changing the decision again. If the person withdrew permission afterward, a retry does not restore it. A changed address, hidden person, existing decision, active subscriber agreement, unsubscribe or suppression refuses a new backfill. A new explicit owner decision uses the normal `allowed` operation instead; it still cannot reverse a subscriber unsubscribe or suppression.
398
+
399
+ For a reviewed cohort, submit at most 25 people in each bounded batch. Each row needs its own stable request key:
400
+
401
+ ```json
402
+ {
403
+ "allowed": true,
404
+ "backfill": true,
405
+ "evidence": "Owner confirmed marketing-email permission for this reviewed cohort",
406
+ "people": [
407
+ { "personId": "PERSON_ID", "expectedEmail": "alex@example.com", "requestKey": "UNIQUE_REPAIR_UUID" }
408
+ ]
409
+ }
410
+ ```
411
+
412
+ Run the same `person permission --file reviewed-batch.json` command. Read every row in `results`: domain conflicts include `error` and `status`, while successful rows include `emailPermission` and `duplicate`. The batch can partly succeed; receipts make a retry safe. Malformed batch input fails before any permission writes. Infrastructure errors can leave completed receipts, so retry the exact batch rather than inventing new keys. This capability works through the public API and `manage_people` too; it has no company-only endpoint.
353
413
 
354
414
  ## Import a customer list from CSV
355
415
 
@@ -364,7 +424,7 @@ Alex Rivera,alex@example.com,,
364
424
  Phone customer,,+12025550199,
365
425
  ```
366
426
 
367
- Map exact header names in `mapping.json`. The permission column is optional; omit it when permission is unknown. `yes`/`true` records existing permission and `no`/`false` records no permission.
427
+ Map exact header names in `mapping.json`. The permission column is optional. `yes`/`true` records an owner assertion and `no`/`false` records refusal. A supplied row value takes priority over the list-wide choice; duplicate rows keep a negative decision. Set top-level `emailPermission` to `allowed`, `unknown`, or `not_allowed` for new people whose row has no permission value. Omission defaults to unknown. List-wide choices never change existing decisions. Imports preserve saved refusals, unsubscribe and suppression. Changing the list choice requires a new request key.
368
428
 
369
429
  ```
370
430
  {
@@ -376,7 +436,8 @@ Map exact header names in `mapping.json`. The permission column is optional; omi
376
436
  },
377
437
  "groupIds": [
378
438
  "GROUP_ID"
379
- ]
439
+ ],
440
+ "emailPermission": "unknown"
380
441
  }
381
442
  ```
382
443
 
@@ -545,7 +606,7 @@ The agent can create and edit saved templates and series with the existing Email
545
606
  | rules.runs | ruleId?, cursor?, limit? | Inspect saved outcomes and failed actions. |
546
607
  | rules.retry | runId or ruleId | Retry eligible failed work; completed action receipts stay saved. |
547
608
  | attachments.create | groupId, stageId?, emailKind, emailId, enabled?, name?, conditions? | Create an ordinary group/stage entry rule for a template or series. |
548
- | person.permission | personId, allowed | Record an email-permission decision; mailing-list unsubscribe and suppression still win. |
609
+ | person.permission | personId, allowed; backfill?, expectedEmail?, evidence?, requestKey?; or backfill batch with people (1–25) | Record a decision or repair reviewed missing permission without automation. Unsubscribe and suppression still win. |
549
610
  | audience.preview | audience | Preview matched and eligible People for a campaign. |
550
611
  | import.preview | csv, mapping?, headersOnly? | Validate a CSV and match existing contacts; sends nothing. |
551
612
  | import.start | csv, mapping?, groupIds?, requestKey | Start a saved import. |
@@ -161,6 +161,9 @@ export async function runPeople(args) {
161
161
  const phone = takeFlag(work, "--phone");
162
162
  const name = takeFlag(work, "--name");
163
163
  const note = takeFlag(work, "--note");
164
+ const emailPermission = takeFlag(work, "--email-permission");
165
+ if (emailPermission !== undefined && !["allowed", "unknown", "not_allowed"].includes(emailPermission))
166
+ throw new CliHttpError("--email-permission is allowed, unknown, or not_allowed.", EXIT_USAGE, 0, "bad_request");
164
167
  assertNoUnknownFlags(work, "people");
165
168
  if (!email && !phone) {
166
169
  console.error("Usage: frontpage people add --email you@example.com | --phone +15551234567 [--name …] [--note …]");
@@ -171,6 +174,7 @@ export async function runPeople(args) {
171
174
  phone,
172
175
  name,
173
176
  note,
177
+ ...(emailPermission !== undefined ? { emailPermission } : {}),
174
178
  });
175
179
  if (isJson()) {
176
180
  emitOk("people", data);
package/dist/registry.js CHANGED
@@ -466,7 +466,7 @@ export const COMMANDS = [
466
466
  "CSV: --csv PATH plus --file mapping.json. import start requires a stable --request-key. Retry the same import with the same key and contents.",
467
467
  "list/export: --group GROUP_ID --stage STAGE_ID. See package PEOPLE.md for full examples and every JSON shape.",
468
468
  "list: --q --status --source --form-slug --follow-up due|upcoming|none --hidden --sort --last-seen-days --tag --limit --cursor",
469
- "add requires --email and/or --phone (never name alone)",
469
+ "add requires --email and/or --phone (never name alone). New contacts default to owner-asserted email permission; --email-permission allowed|unknown|not_allowed changes this choice. Existing default decisions stay saved; withdrawals/restrictions cannot be overridden by add.",
470
470
  "tag: --add <label> | --remove <slug>",
471
471
  "export writes UTF-8 CSV (same filters as list). Optional --out <file>",
472
472
  "Hide is not forget. Unique email/phone stay; later ingest un-hides",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frontpage-host",
3
- "version": "0.1.27",
3
+ "version": "0.1.28",
4
4
  "description": "Frontpage CLI — manage your site from the terminal",
5
5
  "type": "module",
6
6
  "bin": {