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 +69 -8
- package/dist/commands/people.js +4 -0
- package/dist/registry.js +1 -1
- package/package.json +1 -1
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
|
-
##
|
|
341
|
+
## Marketing email permission
|
|
342
342
|
|
|
343
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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`
|
|
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
|
|
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
|
|
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. |
|
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",
|