frontpage-host 0.1.26 → 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 +74 -12
- package/dist/commands/people.js +4 -0
- package/dist/registry.js +1 -1
- package/package.json +1 -1
package/PEOPLE.md
CHANGED
|
@@ -102,11 +102,12 @@ frontpage people groups create --file repeat-buyers.json --site YOUR_SITE --json
|
|
|
102
102
|
frontpage people membership override --person PERSON_ID --group GROUP_ID --override include --site YOUR_SITE --json
|
|
103
103
|
frontpage people membership override --person PERSON_ID --group GROUP_ID --override exclude --site YOUR_SITE --json
|
|
104
104
|
frontpage people membership override --person PERSON_ID --group GROUP_ID --override auto --site YOUR_SITE --json
|
|
105
|
-
frontpage people membership hold --person PERSON_ID --group GROUP_ID --hold true --site YOUR_SITE --json
|
|
106
105
|
frontpage people membership hold --person PERSON_ID --group GROUP_ID --hold false --site YOUR_SITE --json
|
|
107
106
|
```
|
|
108
107
|
|
|
109
|
-
`include` keeps one person in an automatic group; `exclude` keeps them out. `auto` clears the exception.
|
|
108
|
+
`include` keeps one person in an automatic group; `exclude` keeps them out. `auto` clears the exception and applies that group’s conditions immediately when the needed facts are available. It can add or remove membership and run the group’s normal rules. It does not change other groups or email permission. An earlier saved pause still prevents automatic changes.
|
|
109
|
+
|
|
110
|
+
**Earlier pauses.** New `--hold true` requests fail unless the person already has that pause; an existing pause remains unchanged. Use `--hold false` to resume a saved pause. This keeps membership, stage and exception, restarts the time-rule clock, and skips earlier pending work for this person/group. It fails with HTTP 409 while a group rule is running; try again after it finishes. Archived groups must be restored first. No saved pause is cleared automatically. Failed runs remain available for an explicit review and retry; resuming does not retry them.
|
|
110
111
|
|
|
111
112
|
**Preserve existing records.**
|
|
112
113
|
|
|
@@ -337,18 +338,78 @@ Copy the attachment’s returned rule ID before enabling it. Pass `--enabled fal
|
|
|
337
338
|
|
|
338
339
|
See the [Email guide](/developers/mail) for content blocks, template variables, series step delays, sender settings, and email limits.
|
|
339
340
|
|
|
340
|
-
##
|
|
341
|
+
## Marketing email permission
|
|
342
|
+
|
|
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. |
|
|
341
353
|
|
|
342
|
-
|
|
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.
|
|
343
355
|
|
|
344
356
|
```
|
|
345
|
-
|
|
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
|
|
346
363
|
frontpage people membership get --person PERSON_ID --site YOUR_SITE --json
|
|
347
|
-
#
|
|
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
|
|
348
366
|
frontpage people person permission --person PERSON_ID --allowed true --site YOUR_SITE --json
|
|
349
367
|
```
|
|
350
368
|
|
|
351
|
-
`membership.get`
|
|
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.
|
|
352
413
|
|
|
353
414
|
## Import a customer list from CSV
|
|
354
415
|
|
|
@@ -363,7 +424,7 @@ Alex Rivera,alex@example.com,,
|
|
|
363
424
|
Phone customer,,+12025550199,
|
|
364
425
|
```
|
|
365
426
|
|
|
366
|
-
Map exact header names in `mapping.json`. The permission column is optional
|
|
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.
|
|
367
428
|
|
|
368
429
|
```
|
|
369
430
|
{
|
|
@@ -375,7 +436,8 @@ Map exact header names in `mapping.json`. The permission column is optional; omi
|
|
|
375
436
|
},
|
|
376
437
|
"groupIds": [
|
|
377
438
|
"GROUP_ID"
|
|
378
|
-
]
|
|
439
|
+
],
|
|
440
|
+
"emailPermission": "unknown"
|
|
379
441
|
}
|
|
380
442
|
```
|
|
381
443
|
|
|
@@ -528,12 +590,12 @@ The agent can create and edit saved templates and series with the existing Email
|
|
|
528
590
|
| stages.reorder | groupId, stageIds | Save every active stage ID in order, once each. |
|
|
529
591
|
| stages.archive / stages.restore | id, groupId | Keep assignments and history while archiving or restoring. |
|
|
530
592
|
| membership.get | personId | Memberships and email eligibility. |
|
|
531
|
-
| membership.add / membership.remove | personId, groupId; reset? on add | Join or leave a
|
|
593
|
+
| membership.add / membership.remove | personId, groupId; reset? on add | Join or leave a group; a conditions group stores an include or exclude exception. |
|
|
532
594
|
| membership.move | personId, fromGroupId, groupId, reset? | Leave one manual group and join another. |
|
|
533
595
|
| membership.stage | personId, groupId, stageId | Set a stage; null clears it without leaving the group. |
|
|
534
596
|
| membership.advance | personId, groupId | Move to the next nonterminal stage. |
|
|
535
597
|
| membership.override | personId, groupId, override | Use include, exclude, or null for automatic membership. |
|
|
536
|
-
| membership.hold | personId, groupId, hold |
|
|
598
|
+
| membership.hold | personId, groupId, hold | Resume an earlier saved pause; creating a new pause is no longer supported. |
|
|
537
599
|
| rules.list | archived? | List rules; archived:true includes archived rules. |
|
|
538
600
|
| rules.get | id | Read the saved rule and version. |
|
|
539
601
|
| rules.create | name, trigger, conditions?, actions, enabled? | Create a rule. Pass enabled:false to review it first. |
|
|
@@ -544,7 +606,7 @@ The agent can create and edit saved templates and series with the existing Email
|
|
|
544
606
|
| rules.runs | ruleId?, cursor?, limit? | Inspect saved outcomes and failed actions. |
|
|
545
607
|
| rules.retry | runId or ruleId | Retry eligible failed work; completed action receipts stay saved. |
|
|
546
608
|
| attachments.create | groupId, stageId?, emailKind, emailId, enabled?, name?, conditions? | Create an ordinary group/stage entry rule for a template or series. |
|
|
547
|
-
| person.permission
|
|
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. |
|
|
548
610
|
| audience.preview | audience | Preview matched and eligible People for a campaign. |
|
|
549
611
|
| import.preview | csv, mapping?, headersOnly? | Validate a CSV and match existing contacts; sends nothing. |
|
|
550
612
|
| import.start | csv, mapping?, groupIds?, requestKey | Start a saved import. |
|
package/dist/commands/people.js
CHANGED
|
@@ -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",
|