primcli 3.0.0-preview.5 → 3.0.0-preview.7

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.
@@ -1,6 +1,7 @@
1
- import { F as ReplyStateUnsupportedError, G as SENT_EMAIL_EVENT_TYPES, H as isAwaitingRejectedError, I as assertAwaitingFilterRows, K as WEBHOOK_EVENT_TYPES, L as awaitingRejectedError, Qt as getThread, R as ensureSearchReportsReplyState, Vn as sdk_gen_exports, W as replyStateSurfaceForOperation, at as expectedAutomatedVerdicts, et as AutomatedFilterUnsupportedError, it as ensureSearchReportsAutomated, nt as automatedRejectedError, pn as listEndpoints, rt as automatedSurfaceForOperation, st as isAutomatedRejectedError, tt as assertAutomatedVerdict, v as parseRetryAfter, zt as getEmail } from "./repeats-Dj_o6c-X-Ix28GRRc.js";
1
+ import { B as ensureSearchReportsReplyState, Ht as getEmail, J as SENT_EMAIL_EVENT_TYPES, K as replyStateSurfaceForOperation, L as ReplyStateUnsupportedError, R as assertAwaitingFilterRows, W as isAwaitingRejectedError, Wn as sdk_gen_exports, Y as WEBHOOK_EVENT_TYPES, Zt as getOwnGrant, at as automatedRejectedError, ct as expectedAutomatedVerdicts, gn as listEndpoints, it as assertAutomatedVerdict, ot as automatedSurfaceForOperation, q as EMAIL_RULE_REQUEST_EVENT_TYPES, rt as AutomatedFilterUnsupportedError, st as ensureSearchReportsAutomated, tn as getThread, ut as isAutomatedRejectedError, v as parseRetryAfter, z as awaitingRejectedError } from "./repeats-Dj_o6c-X-C6tOKAqk.js";
2
2
  import { C as saveCliCredentials, F as agentProfilesDirectory, H as requireDefaultLoginProfile, N as agentProfileDirectory, R as loadConnectedAgentProfile, S as resolveCliAuth, W as PrimitiveApiClient, _ as deleteCliCredentials, b as loadCliCredentials, c as resolveConfigEnvironment, d as validateCliHeaderName, f as validateCliHeaderValue, i as loadCliConfig, j as AGENT_PROFILE_ENV, m as cliAccessTokenExpiresAt, p as acquireCliCredentialsLock, w as saveSignupCredentials, x as normalizeApiBaseUrl, y as detectPrimitiveKeyEnvMisname } from "./cli-config-Ms_0vNjq.js";
3
3
  import "./oclif/global-org-flag.js";
4
+ import { a as thrownErrorBody } from "./json-output-ch756TXV.js";
4
5
  import { d as readMailJson, h as writeMailJson, m as withMailLock, p as syncMailDirectory, u as privateMailPermissions } from "./shared-mail-files-BAQSGxxz.js";
5
6
  import { Args, Command, Errors, Flags } from "@oclif/core";
6
7
  import { existsSync, lstatSync, readFileSync, readdirSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
@@ -663,7 +664,7 @@ const operationManifest = [
663
664
  "binaryResponse": false,
664
665
  "bodyRequired": false,
665
666
  "command": "whoami",
666
- "description": "Current authenticated identity. member_address is null for machine credentials or an unavailable member address.",
667
+ "description": "Current authenticated identity. member_address is null for machine credentials or an unavailable member address. email (your own sign-in email) and role_name (your role as the members list labels it) are set only when you are signed in as yourself, in the dashboard or with your own Primitive CLI or app login, and are null for API keys, Functions, connected agents, agent signup logins and other apps acting for you.",
667
668
  "hasJsonBody": false,
668
669
  "method": "GET",
669
670
  "operationId": "whoami",
@@ -721,6 +722,14 @@ const operationManifest = [
721
722
  "request_id": { "type": "string" },
722
723
  "auth_method": { "type": ["string", "null"] },
723
724
  "key_id": { "type": ["string", "null"] },
725
+ "email": {
726
+ "description": "Your own sign-in email, when you are signed in as yourself: in the dashboard, or with your own Primitive CLI or app login. Null for API keys, Functions, connected agents, agent signup logins and other apps acting for you.",
727
+ "type": ["string", "null"]
728
+ },
729
+ "role_name": {
730
+ "description": "Your role as the organization's members list labels it: a template or custom role as \"<name> (template)\" or \"<name> (custom)\", otherwise Owner, Admin, Member or Restricted. Null for the same credentials as email.",
731
+ "type": ["string", "null"]
732
+ },
724
733
  "grant": {
725
734
  "type": ["object", "null"],
726
735
  "propertyNames": { "type": "string" },
@@ -757,7 +766,9 @@ const operationManifest = [
757
766
  "role",
758
767
  "request_id",
759
768
  "auth_method",
760
- "key_id"
769
+ "key_id",
770
+ "email",
771
+ "role_name"
761
772
  ]
762
773
  },
763
774
  "sdkName": "whoami",
@@ -769,7 +780,7 @@ const operationManifest = [
769
780
  "binaryResponse": false,
770
781
  "bodyRequired": false,
771
782
  "command": "list-activity-objects",
772
- "description": "The objects the caller created, newest first, each with where it stands now, including ones since disabled, deleted or revoked where the object keeps a row: Functions, endpoints, Function triggers, pull subscriptions, recipient routes, filters, wake schedules, repeating sends and scheduled sends. An API key, an app or an agent sees what it created itself; a webhook given to an API key lists under that key. A person (a dashboard session, or their own Primitive CLI or app login) sees what they created through any credential of theirs, each item naming it, and also their API keys, personal agents, email rules and rule requests. Each kind is listed only if the caller could list it through that kind's own route; a kind it could not is named in withheld, with no count. Ids, names and states only: never a webhook URL or mail content. Needs activity:read_self.",
783
+ "description": "The objects the caller created, newest first, each with where it stands now, including ones since disabled, deleted or revoked where the object keeps a row: Functions, endpoints, Function triggers, pull subscriptions, recipient routes, filters, wake schedules, repeating sends and scheduled sends. An API key, an app or an agent sees what it created itself; a webhook given to an API key lists under that key. A person (a dashboard session, or their own Primitive CLI or app login) sees what they created through any credential of theirs, each item naming it, and also their API keys, personal agents, email rules and rule requests. Each kind is listed only if the caller could list it through that kind's own route; a kind it could not is named in withheld, with no count. Ids, names and states only: never a webhook URL or mail content. Needs activity:read_self. With relation=asked it lists instead what other members asked of the person, open or answered: the rule requests made to them, under their current membership, each naming the member who asked in created_through. A key, an app or an agent has none, and gets email_rule_request in withheld.",
773
784
  "hasJsonBody": false,
774
785
  "method": "GET",
775
786
  "operationId": "listActivityObjects",
@@ -777,7 +788,15 @@ const operationManifest = [
777
788
  "pathParams": [],
778
789
  "queryParams": [
779
790
  {
780
- "description": "Only these kinds, comma-separated: function, endpoint, function_trigger, pull_subscription, recipient_route, filter_rule, wake_schedule, repeating_send, scheduled_send, api_key, agent_connection, email_rule, email_rule_request. Defaults to every kind the caller can have created (a person's own kinds only for a person).",
791
+ "default": "created",
792
+ "description": "created (the default) or asked.",
793
+ "enum": ["created", "asked"],
794
+ "name": "relation",
795
+ "required": false,
796
+ "type": "string"
797
+ },
798
+ {
799
+ "description": "Only these kinds, comma-separated: function, endpoint, function_trigger, pull_subscription, recipient_route, filter_rule, wake_schedule, repeating_send, scheduled_send, api_key, agent_connection, email_rule, email_rule_request. Defaults to every kind the caller can have created (a person's own kinds only for a person). With relation=asked, only email_rule_request, which is also the default.",
781
800
  "enum": null,
782
801
  "name": "kind",
783
802
  "required": false,
@@ -822,7 +841,7 @@ const operationManifest = [
822
841
  "properties": {
823
842
  "covers": {
824
843
  "type": "string",
825
- "description": "you: what a person created through any credential of theirs (their dashboard session, their own CLI or app login, apps and agent delegates acting for them, their API keys while theirs, their personal agents). this_credential: what the calling key, app or agent created itself."
844
+ "description": "you: what a person created through any credential of theirs, or with relation=asked what others asked of them (their dashboard session, their own CLI or app login, apps and agent delegates acting for them, their API keys while theirs, their personal agents). this_credential: what the calling key, app or agent created itself."
826
845
  },
827
846
  "items": {
828
847
  "type": "array",
@@ -854,21 +873,21 @@ const operationManifest = [
854
873
  "owned_now": {
855
874
  "type": "string",
856
875
  "enum": ["you", "someone_else"],
857
- "description": "you: it is still yours: never transferred, or transferred back to you. someone_else: it was transferred and another person holds it now."
876
+ "description": "you: it is still yours: never transferred, or transferred back to you. someone_else: it was transferred and another person holds it now, or, with relation=asked, another member made it."
858
877
  },
859
878
  "created_through": {
860
879
  "type": "object",
861
880
  "properties": {
862
881
  "kind": {
863
882
  "type": "string",
864
- "description": "The kind of credential it was created through: org_key, function_key, connected_key, oauth, agent_delegate or member (the person themselves)."
883
+ "description": "The kind of credential it was created through: org_key, function_key, connected_key, oauth, agent_delegate or member (the person themselves). With relation=asked, member: the member who asked."
865
884
  },
866
885
  "id": {
867
- "description": "In a person's view, the person's own user id for member, and an API key's id where they may read their keys (for a person limited to their own keys, only a key they still hold). Null otherwise, and always null in one credential's view, where it is the calling credential itself.",
886
+ "description": "In a person's view, the person's own user id for member, and an API key's id where they may read their keys (for a person limited to their own keys, only a key they still hold). Null otherwise, and always null in one credential's view, where it is the calling credential itself. With relation=asked, the user id of the member who asked.",
868
887
  "type": ["string", "null"]
869
888
  },
870
889
  "name": {
871
- "description": "In a person's view, what that credential of theirs is called: an app login by its app, an API key by its name, an agent's key by its address. Null otherwise, and for the person themselves.",
890
+ "description": "In a person's view, what that credential of theirs is called: an app login by its app, an API key by its name, an agent's key by its address. Null otherwise, and for the person themselves. With relation=asked, the name of the member who asked.",
872
891
  "type": ["string", "null"]
873
892
  }
874
893
  },
@@ -929,7 +948,7 @@ const operationManifest = [
929
948
  ]
930
949
  },
931
950
  "sdkName": "listActivityObjects",
932
- "summary": "List what you created",
951
+ "summary": "List what you created, or what you were asked",
933
952
  "tag": "Activity",
934
953
  "tagCommand": "activity"
935
954
  },
@@ -9450,7 +9469,7 @@ const operationManifest = [
9450
9469
  "binaryResponse": false,
9451
9470
  "bodyRequired": false,
9452
9471
  "command": "get-key",
9453
- "description": "One key's metadata, never its secret, with whether it works now (status). Its grant's mail_scope.depth is raw only when the key holds mail.raw:read and its scope lists an address or a domain, otherwise content. The person needs access to the whole organization's mail. Needs the organization's credential features to be enabled; while they are not, this answers 404 feature_disabled, with details.feature naming the feature. Contact Primitive to turn them on.\n\nPerson only: a dashboard session or the person's own Primitive CLI token; no other OAuth app. The person's role must hold keys:read. API keys, agent connection keys and agents are refused.",
9472
+ "description": "One key's metadata, never its secret, with whether it works now (status). A revoked key whose record is kept is answered too, with status revoked, revoked_at and revoked_by, under the same access rules as a key that works: one the caller may not read is 404, revoked or not. Its grant's mail_scope.depth is raw only when the key holds mail.raw:read and its scope lists an address or a domain, otherwise content. A rule the key lists that reads nothing any more (a deleted rule, or a share that ended) is listed under grant.mail_scope.ended_rules, each with its id and reason (rule_deleted or share_ended), and not under rules. The person needs access to the whole organization's mail. Needs the organization's credential features to be enabled; while they are not, this answers 404 feature_disabled, with details.feature naming the feature. Contact Primitive to turn them on.\n\nPerson only: a dashboard session or the person's own Primitive CLI token; no other OAuth app. The person's role must hold keys:read. API keys, agent connection keys and agents are refused.",
9454
9473
  "hasJsonBody": false,
9455
9474
  "method": "GET",
9456
9475
  "operationId": "getKey",
@@ -9484,12 +9503,25 @@ const operationManifest = [
9484
9503
  "grant": {
9485
9504
  "type": "object",
9486
9505
  "properties": {
9506
+ "ref": {
9507
+ "description": "The grant this read shows. Send it as expected_grant_ref on PATCH /v1/keys/{id} so the change is refused (409 conflict, details.reason grant_changed) if the grant has changed since this read.",
9508
+ "type": "string"
9509
+ },
9487
9510
  "template": { "type": ["string", "null"] },
9488
9511
  "permissions": {
9489
9512
  "type": ["array", "null"],
9490
9513
  "items": { "type": "string" }
9491
9514
  },
9492
- "mail_scope": {}
9515
+ "mail_scope": {},
9516
+ "resources": {
9517
+ "type": ["object", "null"],
9518
+ "propertyNames": { "type": "string" },
9519
+ "additionalProperties": {
9520
+ "type": "array",
9521
+ "items": { "type": "string" }
9522
+ },
9523
+ "description": "The Functions, domains, endpoints or routes the key is limited to, by type; null when it has no such limits. A grant change on a key with limits must drop them on purpose, with grant.resources {} (otherwise 409 conflict, details.reason key_has_resource_limits)."
9524
+ }
9493
9525
  },
9494
9526
  "required": [
9495
9527
  "template",
@@ -9505,14 +9537,31 @@ const operationManifest = [
9505
9537
  "expires_at": { "type": ["string", "null"] },
9506
9538
  "suspended": { "type": "boolean" },
9507
9539
  "status": {
9508
- "description": "Whether the key works now: active; suspended (refused until the suspension is lifted); expired (refused since expires_at); or inactive, for an agent connection key whose connection cannot act (not claimed, revoked, its domain not verified, or its owner no longer a member). A revoked key is not listed. An endpoint's owner_key_status reports an expired key as suspended.",
9540
+ "description": "Whether the key works now: active; suspended (refused until the suspension is lifted); expired (refused since expires_at); inactive, for an agent connection key whose connection cannot act (not claimed, revoked, its domain not verified, or its owner no longer a member); or revoked, which it stays for good and which no unrevoked key ever shows. A revoked key is listed only with include_revoked=true, and GET /v1/keys/{id} answers it. An endpoint's owner_key_status reports an expired key as suspended.",
9509
9541
  "type": "string",
9510
9542
  "enum": [
9511
9543
  "active",
9512
9544
  "suspended",
9513
9545
  "expired",
9514
- "inactive"
9546
+ "inactive",
9547
+ "revoked"
9515
9548
  ]
9549
+ },
9550
+ "revoked_at": {
9551
+ "description": "When the key was revoked (ISO 8601); null for a key that is not revoked.",
9552
+ "type": ["string", "null"]
9553
+ },
9554
+ "revoked_by": {
9555
+ "description": "The user id of the person who revoked the key from their own dashboard session, step-up or Primitive CLI login; null for a key that is not revoked, and for one revoked any other way (by an API key, another app or token, or an automatic process such as secret scanning).",
9556
+ "type": ["string", "null"]
9557
+ },
9558
+ "rotated_from": {
9559
+ "description": "The id of the key this key was rotated from (POST /v1/keys/{id}/rotate made this key in its place); null for a key not made by rotation, when the caller may not read that key, or when its record is gone. A revoked one is still named: GET /v1/keys/{id} answers it.",
9560
+ "type": ["string", "null"]
9561
+ },
9562
+ "rotated_to": {
9563
+ "description": "The id of the newest key rotated from this key that the caller may read; null until it is rotated, or when the caller may read no key rotated from it. A revoked one is still named: GET /v1/keys/{id} answers it.",
9564
+ "type": ["string", "null"]
9516
9565
  }
9517
9566
  },
9518
9567
  "required": [
@@ -10049,7 +10098,7 @@ const operationManifest = [
10049
10098
  "binaryResponse": false,
10050
10099
  "bodyRequired": false,
10051
10100
  "command": "list-credentials",
10052
- "description": "Every credential that can act in the organization: API keys, Function keys, agent connection keys, connected apps (OAuth), agent delegates and, where enabled, CLI logins. Secrets are never returned. A key's grant.mail_scope.depth is raw only when it holds mail.raw:read and its scope lists an address or a domain, otherwise content. Filter with kind. Needs the organization's credential features to be enabled; while they are not, this answers 404 feature_disabled, with details.feature naming the feature. Contact Primitive to turn them on.\n\nPerson only: a dashboard session. The person's role must hold credentials:read. API keys, agent connection keys and agents are refused.",
10101
+ "description": "Every credential that can act in the organization: API keys, Function keys, agent connection keys, connected apps (OAuth), agent delegates and, where enabled, CLI logins. Secrets are never returned. A key's grant.mail_scope.depth is raw only when it holds mail.raw:read and its scope lists an address or a domain, otherwise content. A rule the key lists that reads nothing any more (a deleted rule, or a share that ended) is listed under grant.mail_scope.ended_rules, each with its id and reason (rule_deleted or share_ended), and not under rules. Filter with kind. Needs the organization's credential features to be enabled; while they are not, this answers 404 feature_disabled, with details.feature naming the feature. Contact Primitive to turn them on.\n\nPerson only: a dashboard session. The person's role must hold credentials:read. API keys, agent connection keys and agents are refused.",
10053
10102
  "hasJsonBody": false,
10054
10103
  "method": "GET",
10055
10104
  "operationId": "listCredentials",
@@ -10286,7 +10335,7 @@ const operationManifest = [
10286
10335
  "binaryResponse": false,
10287
10336
  "bodyRequired": false,
10288
10337
  "command": "list-keys",
10289
- "description": "Key metadata, never a secret: name, prefix, grant, creator, last use, expiry, suspension and whether each key works now (status). A grant's mail_scope.depth is raw only when the key holds mail.raw:read and its scope lists an address or a domain, otherwise content. The person needs access to the whole organization's mail. Needs the organization's credential features to be enabled; while they are not, this answers 404 feature_disabled, with details.feature naming the feature. Contact Primitive to turn them on.\n\nPerson only: a dashboard session or the person's own Primitive CLI token; no other OAuth app. The person's role must hold keys:read. API keys, agent connection keys and agents are refused.",
10338
+ "description": "Key metadata, never a secret: name, prefix, grant, creator, last use, expiry, suspension and whether each key works now (status). Revoked keys are left out unless include_revoked is true, which lists them too with status revoked, revoked_at and revoked_by, under the same access rules as keys that work. A grant's mail_scope.depth is raw only when the key holds mail.raw:read and its scope lists an address or a domain, otherwise content. A rule the key lists that reads nothing any more (a deleted rule, or a share that ended) is listed under grant.mail_scope.ended_rules, each with its id and reason (rule_deleted or share_ended), and not under rules. The person needs access to the whole organization's mail. Needs the organization's credential features to be enabled; while they are not, this answers 404 feature_disabled, with details.feature naming the feature. Contact Primitive to turn them on.\n\nPerson only: a dashboard session or the person's own Primitive CLI token; no other OAuth app. The person's role must hold keys:read. API keys, agent connection keys and agents are refused.",
10290
10339
  "hasJsonBody": false,
10291
10340
  "method": "GET",
10292
10341
  "operationId": "listKeys",
@@ -10298,6 +10347,12 @@ const operationManifest = [
10298
10347
  "name": "cursor",
10299
10348
  "required": false,
10300
10349
  "type": "string"
10350
+ }, {
10351
+ "description": "true to list revoked keys as well, newest first among the rest; false (the default) lists only keys that are not revoked. The cursor does not carry it: send it with every page.",
10352
+ "enum": ["true", "false"],
10353
+ "name": "include_revoked",
10354
+ "required": false,
10355
+ "type": "string"
10301
10356
  }],
10302
10357
  "headerParams": [],
10303
10358
  "permissions": ["keys:read"],
@@ -10325,12 +10380,25 @@ const operationManifest = [
10325
10380
  "grant": {
10326
10381
  "type": "object",
10327
10382
  "properties": {
10383
+ "ref": {
10384
+ "description": "The grant this read shows. Send it as expected_grant_ref on PATCH /v1/keys/{id} so the change is refused (409 conflict, details.reason grant_changed) if the grant has changed since this read.",
10385
+ "type": "string"
10386
+ },
10328
10387
  "template": { "type": ["string", "null"] },
10329
10388
  "permissions": {
10330
10389
  "type": ["array", "null"],
10331
10390
  "items": { "type": "string" }
10332
10391
  },
10333
- "mail_scope": {}
10392
+ "mail_scope": {},
10393
+ "resources": {
10394
+ "type": ["object", "null"],
10395
+ "propertyNames": { "type": "string" },
10396
+ "additionalProperties": {
10397
+ "type": "array",
10398
+ "items": { "type": "string" }
10399
+ },
10400
+ "description": "The Functions, domains, endpoints or routes the key is limited to, by type; null when it has no such limits. A grant change on a key with limits must drop them on purpose, with grant.resources {} (otherwise 409 conflict, details.reason key_has_resource_limits)."
10401
+ }
10334
10402
  },
10335
10403
  "required": [
10336
10404
  "template",
@@ -10346,14 +10414,31 @@ const operationManifest = [
10346
10414
  "expires_at": { "type": ["string", "null"] },
10347
10415
  "suspended": { "type": "boolean" },
10348
10416
  "status": {
10349
- "description": "Whether the key works now: active; suspended (refused until the suspension is lifted); expired (refused since expires_at); or inactive, for an agent connection key whose connection cannot act (not claimed, revoked, its domain not verified, or its owner no longer a member). A revoked key is not listed. An endpoint's owner_key_status reports an expired key as suspended.",
10417
+ "description": "Whether the key works now: active; suspended (refused until the suspension is lifted); expired (refused since expires_at); inactive, for an agent connection key whose connection cannot act (not claimed, revoked, its domain not verified, or its owner no longer a member); or revoked, which it stays for good and which no unrevoked key ever shows. A revoked key is listed only with include_revoked=true, and GET /v1/keys/{id} answers it. An endpoint's owner_key_status reports an expired key as suspended.",
10350
10418
  "type": "string",
10351
10419
  "enum": [
10352
10420
  "active",
10353
10421
  "suspended",
10354
10422
  "expired",
10355
- "inactive"
10423
+ "inactive",
10424
+ "revoked"
10356
10425
  ]
10426
+ },
10427
+ "revoked_at": {
10428
+ "description": "When the key was revoked (ISO 8601); null for a key that is not revoked.",
10429
+ "type": ["string", "null"]
10430
+ },
10431
+ "revoked_by": {
10432
+ "description": "The user id of the person who revoked the key from their own dashboard session, step-up or Primitive CLI login; null for a key that is not revoked, and for one revoked any other way (by an API key, another app or token, or an automatic process such as secret scanning).",
10433
+ "type": ["string", "null"]
10434
+ },
10435
+ "rotated_from": {
10436
+ "description": "The id of the key this key was rotated from (POST /v1/keys/{id}/rotate made this key in its place); null for a key not made by rotation, when the caller may not read that key, or when its record is gone. A revoked one is still named: GET /v1/keys/{id} answers it.",
10437
+ "type": ["string", "null"]
10438
+ },
10439
+ "rotated_to": {
10440
+ "description": "The id of the newest key rotated from this key that the caller may read; null until it is rotated, or when the caller may read no key rotated from it. A revoked one is still named: GET /v1/keys/{id} answers it.",
10441
+ "type": ["string", "null"]
10357
10442
  }
10358
10443
  },
10359
10444
  "required": [
@@ -10472,7 +10557,7 @@ const operationManifest = [
10472
10557
  "binaryResponse": false,
10473
10558
  "bodyRequired": true,
10474
10559
  "command": "revoke-credential",
10475
- "description": "Revoke one credential by kind and id. A Function key is rotated, never revoked (409 rotate_instead). An API key is revoked only by a person whose own access covers the key's access (an owner covers every key); otherwise 403 grant_forbidden, details.reason target_not_covered. What the credential set up keeps working and is listed in standing_objects for review. The person's own Primitive CLI login revokes an API key (kind org_key) as the person, with no step-up unless the organization's credential policy sets cli_writes_require_approval (then it takes a keys.create step-up); every other kind (a connected key named as org_key included, 403 grant_forbidden target_not_covered), and every other app even with a step-up, is refused outside the dashboard (403). Needs the organization's credential features to be enabled; while they are not, this answers 404 feature_disabled, with details.feature naming the feature. Contact Primitive to turn them on.\n\nPerson only: a dashboard session or the person's own Primitive CLI token; no other OAuth app. The person's role must hold keys:manage. API keys, agent connection keys and agents are refused.",
10560
+ "description": "Revoke one credential by kind and id. A Function key is rotated, never revoked (409 rotate_instead). An API key is revoked only by a person whose own access covers the key's access (an owner covers every key); otherwise 403 grant_forbidden, details.reason target_not_covered. What the credential set up or owns is listed in standing_objects for review, and for an API key also what it carries on for the keys it was rotated from. An organization-owned object (org_owned true) keeps working; any other keeps working only while someone can still act for it, such as a key of the same rotation that still works, or whoever it was given to, except that a block filter an app made keeps blocking, within the reach the app had, unless the person revoking chose to stop it. An API key (kind org_key or connected_key) that is already revoked, and whose record is kept, answers 200 with already_revoked true and revoked 0, to a caller who could have revoked it; anyone else gets the answer they would get for a key that does not exist. The person's own Primitive CLI login revokes an API key (kind org_key) as the person, with no step-up unless the organization's credential policy sets cli_writes_require_approval (then it takes a keys.create step-up); every other kind (a connected key named as org_key included, 403 grant_forbidden target_not_covered), and every other app even with a step-up, is refused outside the dashboard (403). Needs the organization's credential features to be enabled; while they are not, this answers 404 feature_disabled, with details.feature naming the feature. Contact Primitive to turn them on.\n\nPerson only: a dashboard session or the person's own Primitive CLI token; no other OAuth app. The person's role must hold keys:manage. API keys, agent connection keys and agents are refused.",
10476
10561
  "hasJsonBody": true,
10477
10562
  "method": "POST",
10478
10563
  "operationId": "revokeCredential",
@@ -10583,6 +10668,51 @@ const operationManifest = [
10583
10668
  },
10584
10669
  "required": ["kind", "id"]
10585
10670
  }
10671
+ },
10672
+ "already_revoked": {
10673
+ "description": "true when the API key was already revoked, so nothing changed and revoked is 0. Absent otherwise. GET /v1/keys/{id} says when and by whom, to a caller who may read the key.",
10674
+ "type": "boolean"
10675
+ },
10676
+ "keep_failed": {
10677
+ "description": "Items you chose to keep that could not be taken over after the revocation. Each stopped with it, unless someone else had taken it over in the meantime.",
10678
+ "type": "array",
10679
+ "items": {
10680
+ "type": "object",
10681
+ "properties": {
10682
+ "kind": { "type": "string" },
10683
+ "id": { "type": "string" }
10684
+ },
10685
+ "required": ["kind", "id"]
10686
+ }
10687
+ },
10688
+ "standing_objects": {
10689
+ "description": "What the credential set up or owns, listed for review, and for an API key also what it carries on for the keys it was rotated from. Revoking deletes none of it.",
10690
+ "type": "array",
10691
+ "items": {
10692
+ "type": "object",
10693
+ "properties": {
10694
+ "kind": {
10695
+ "type": "string",
10696
+ "description": "endpoint, function_trigger, pull_subscription, recipient_route, filter_rule or wake_schedule."
10697
+ },
10698
+ "id": { "type": "string" },
10699
+ "label": {
10700
+ "type": "string",
10701
+ "description": "A webhook's URL, a pull subscription's name (or pull when it has none), a route's or filter's pattern, or a wake schedule's command and the address it wakes (command to address). A Function trigger's label is function: look it up by id."
10702
+ },
10703
+ "org_owned": {
10704
+ "type": "boolean",
10705
+ "description": "true: the organization owns it, and it keeps working. false: the credential owns it itself, as with everything a third-party app or agent delegate made, a webhook made with a narrowed API key, or a wake schedule made with a key that does not cover super_admin. It keeps working only while someone can still act for it, such as a key of the same rotation that still works, or whoever it was given to or kept it. Revoking a third-party app or agent delegate turns these off, except that a block filter it made keeps blocking, within the reach the app had, unless the person revoking chose to stop it."
10706
+ }
10707
+ },
10708
+ "required": [
10709
+ "kind",
10710
+ "id",
10711
+ "label",
10712
+ "org_owned"
10713
+ ],
10714
+ "additionalProperties": {}
10715
+ }
10586
10716
  }
10587
10717
  },
10588
10718
  "required": [
@@ -10601,7 +10731,7 @@ const operationManifest = [
10601
10731
  "binaryResponse": false,
10602
10732
  "bodyRequired": true,
10603
10733
  "command": "rotate-key",
10604
- "description": "Mint a successor with a new id and the same name, grant, expiry and conditions, and return its secret once. The old key keeps working for overlap_seconds, then stops: 24 hours when overlap_seconds is omitted, at most 7 days, never past its own expires_at, and at once with 0 (which is how to rotate a key whose secret leaked). previous_key_expires_at says when the old key stops. The person rotating must cover the key's access (403 grant_forbidden, details.reason target_not_covered), and a key an owner holds is rotated by an owner only (403 forbidden, details.reason owner_key_rotation), since rotating hands over the new secret. The person's own Primitive CLI login needs no step-up (it acts as the person, held to the same rules) unless the organization's credential policy sets cli_writes_require_approval; the Primitive app's token is refused, even with one. Needs the organization's credential features to be enabled; while they are not, this answers 404 feature_disabled, with details.feature naming the feature. Contact Primitive to turn them on.\n\nPerson only: a dashboard session, the person's own Primitive CLI login with no step-up (unless the organization's credential policy sets cli_writes_require_approval), or the person's own OAuth token with a step-up token for purpose `keys.create` in the Primitive-Step-Up header. Without one any other OAuth token is answered 403 step_up_required, whose details carry the dashboard URL that issues it (or 403 grant_forbidden when the person's role, or the client, could not do it anyway). The person's role must hold keys:manage. API keys and agent connection keys are refused. An agent acting for the person is refused unless it carries a step-up token the person issued in the dashboard.",
10734
+ "description": "Mint a successor with a new id and the same name, grant, expiry and conditions, and return its secret once. The old key keeps working for overlap_seconds, then stops: 24 hours when overlap_seconds is omitted, at most 7 days, never past its own expires_at, and at once with 0 (which is how to rotate a key whose secret leaked). previous_key_expires_at says when the old key stops. Rotating does not extend a key: the successor keeps the old expiry unless expires_at gives it a new one. That needs keys:manage (403 forbidden otherwise), is held to the credential policy (400 when past its maximum lifetime, with the latest expiry it allows now in the message), and someone limited to their own keys can only keep or bring it forward (403 grant_forbidden, details.reason own_key_tighten_only); the old key then never outlives the new one. The person rotating must cover the key's access (403 grant_forbidden, details.reason target_not_covered), and a key an owner holds is rotated by an owner only (403 forbidden, details.reason owner_key_rotation), since rotating hands over the new secret. The person's own Primitive CLI login needs no step-up (it acts as the person, held to the same rules) unless the organization's credential policy sets cli_writes_require_approval; the Primitive app's token is refused, even with one. Needs the organization's credential features to be enabled; while they are not, this answers 404 feature_disabled, with details.feature naming the feature. Contact Primitive to turn them on.\n\nPerson only: a dashboard session, the person's own Primitive CLI login with no step-up (unless the organization's credential policy sets cli_writes_require_approval), or the person's own OAuth token with a step-up token for purpose `keys.create` in the Primitive-Step-Up header. Without one any other OAuth token is answered 403 step_up_required, whose details carry the dashboard URL that issues it (or 403 grant_forbidden when the person's role, or the client, could not do it anyway). The person's role must hold keys:manage. API keys and agent connection keys are refused. An agent acting for the person is refused unless it carries a step-up token the person issued in the dashboard.",
10605
10735
  "hasJsonBody": true,
10606
10736
  "method": "POST",
10607
10737
  "operationId": "rotateKey",
@@ -10631,12 +10761,20 @@ const operationManifest = [
10631
10761
  "access": "step_up",
10632
10762
  "requestSchema": {
10633
10763
  "type": "object",
10634
- "properties": { "overlap_seconds": {
10635
- "description": "How long the old key keeps working, in seconds, up to 604800 (7 days), and never past its own expires_at. 86400 (24 hours) when omitted. 0 stops the old key at once: use it when rotating because the secret leaked.",
10636
- "type": "integer",
10637
- "minimum": 0,
10638
- "maximum": 604800
10639
- } },
10764
+ "properties": {
10765
+ "overlap_seconds": {
10766
+ "description": "How long the old key keeps working, in seconds, up to 604800 (7 days), and never past its own expires_at. 86400 (24 hours) when omitted. 0 stops the old key at once: use it when rotating because the secret leaked.",
10767
+ "type": "integer",
10768
+ "minimum": 0,
10769
+ "maximum": 604800
10770
+ },
10771
+ "expires_at": {
10772
+ "type": ["string", "null"],
10773
+ "format": "date-time",
10774
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$",
10775
+ "description": "The new key's expiry. Omitted, it keeps the old key's expiry: rotating alone never extends a key. A time sets it, within the organization's credential policy (its maximum key lifetime counts from now); null makes it never expire, where the policy allows. Needs keys:manage, as changing a key does. The old key stops when the overlap ends, or at this expiry if that comes first."
10776
+ }
10777
+ },
10640
10778
  "additionalProperties": false
10641
10779
  },
10642
10780
  "responseSchema": {
@@ -10655,7 +10793,7 @@ const operationManifest = [
10655
10793
  "type": "string",
10656
10794
  "format": "date-time",
10657
10795
  "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$",
10658
- "description": "When the old key stops working: the time of the rotation plus overlap_seconds (24 hours when it was omitted, now when it was 0), or the key's own expires_at when that comes first."
10796
+ "description": "When the old key stops working: the time of the rotation plus overlap_seconds (24 hours when it was omitted, now when it was 0), or the key's own expires_at, or the expires_at given to the new key, whichever comes first."
10659
10797
  }
10660
10798
  },
10661
10799
  "required": [
@@ -10767,7 +10905,7 @@ const operationManifest = [
10767
10905
  "binaryResponse": false,
10768
10906
  "bodyRequired": true,
10769
10907
  "command": "update-key",
10770
- "description": "Rename a key, change its expiry, its grant or its conditions, or suspend it. Any change needs the person's own access to cover the key's access now (an owner covers every key); otherwise 403 grant_forbidden, details.reason target_not_covered. A grant change is a new version of the key and is held to the same rules as creating one (a custom grant that does not say which mail it reaches is refused with details.reason mail_scope_required), so it takes the same step-up. The person's own Primitive CLI login needs no step-up (it acts as the person, held to the same rules) unless the organization's credential policy sets cli_writes_require_approval; the Primitive app's token is refused, even with one. Needs the organization's credential features to be enabled; while they are not, this answers 404 feature_disabled, with details.feature naming the feature. Contact Primitive to turn them on.\n\nPerson only: a dashboard session, the person's own Primitive CLI login with no step-up (unless the organization's credential policy sets cli_writes_require_approval), or the person's own OAuth token with a step-up token for purpose `keys.create` in the Primitive-Step-Up header. Without one any other OAuth token is answered 403 step_up_required, whose details carry the dashboard URL that issues it (or 403 grant_forbidden when the person's role, or the client, could not do it anyway). The person's role must hold keys:manage. API keys and agent connection keys are refused. An agent acting for the person is refused unless it carries a step-up token the person issued in the dashboard.",
10908
+ "description": "Rename a key, change its expiry, its grant or its conditions, or suspend it. A later expires_at extends the key, which is how to keep a key working past its expiry (rotating keeps it, unless the rotation gives the new key an expires_at): it is held to the credential policy (400 when past its maximum lifetime, with the latest expiry it allows now in the message), and like any loosening needs the person to be allowed to create a key with the key's access. An expired key's expiry cannot be changed (409 conflict, details.reason key_expired); make a new key instead. Any change needs the person's own access to cover the key's access now (an owner covers every key); otherwise 403 grant_forbidden, details.reason target_not_covered. A grant change is a new version of the key and is held to the same rules as creating one (a custom grant that does not say which mail it reaches is refused with details.reason mail_scope_required), so it takes the same step-up. It replaces the whole grant: send expected_grant_ref (grant.ref from GET /v1/keys/{id}) to have it refused (409 conflict, details.reason grant_changed) if the grant changed since that read; a grant change sent without it answers grant_unguarded: true. On a key limited to selected resources (grant.resources), a grant change must drop the limits on purpose with a custom grant and grant.resources {}; one that leaves them out is refused (409 conflict, details.reason key_has_resource_limits) and nothing changes. The person's own Primitive CLI login needs no step-up (it acts as the person, held to the same rules) unless the organization's credential policy sets cli_writes_require_approval; the Primitive app's token is refused, even with one. Needs the organization's credential features to be enabled; while they are not, this answers 404 feature_disabled, with details.feature naming the feature. Contact Primitive to turn them on.\n\nPerson only: a dashboard session, the person's own Primitive CLI login with no step-up (unless the organization's credential policy sets cli_writes_require_approval), or the person's own OAuth token with a step-up token for purpose `keys.create` in the Primitive-Step-Up header. Without one any other OAuth token is answered 403 step_up_required, whose details carry the dashboard URL that issues it (or 403 grant_forbidden when the person's role, or the client, could not do it anyway). The person's role must hold keys:manage. API keys and agent connection keys are refused. An agent acting for the person is refused unless it carries a step-up token the person issued in the dashboard.",
10771
10909
  "hasJsonBody": true,
10772
10910
  "method": "PATCH",
10773
10911
  "operationId": "updateKey",
@@ -10800,7 +10938,8 @@ const operationManifest = [
10800
10938
  "expires_at": {
10801
10939
  "type": ["string", "null"],
10802
10940
  "format": "date-time",
10803
- "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
10941
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$",
10942
+ "description": "A new expiry. A later time extends the key and an earlier one ends it sooner, within the organization's credential policy (its maximum key lifetime counts from now); null removes the expiry, where the policy allows. A rotated-out key's expiry can only be brought forward, and an expired key's cannot be changed."
10804
10943
  },
10805
10944
  "grant": { "anyOf": [{
10806
10945
  "type": "string",
@@ -10956,6 +11095,11 @@ const operationManifest = [
10956
11095
  ],
10957
11096
  "additionalProperties": {}
10958
11097
  }
11098
+ },
11099
+ "grant_unguarded": {
11100
+ "description": "Present, as true, when the grant was changed without expected_grant_ref, so the change was not checked against changes made since the key was read. Send grant.ref from GET /v1/keys/{id} as expected_grant_ref to have such a change refused instead (409 conflict, details.reason grant_changed).",
11101
+ "type": "boolean",
11102
+ "const": true
10959
11103
  }
10960
11104
  },
10961
11105
  "required": [
@@ -12773,7 +12917,7 @@ const operationManifest = [
12773
12917
  "binaryResponse": false,
12774
12918
  "bodyRequired": false,
12775
12919
  "command": "accept-email-rule-request",
12776
- "description": "Writes the rule as yours, over your own mailboxes only, turns it on, and shares what it matches from now on with the member who asked. Send no body to accept it as asked, or change it first with english and tree together (and an explanation, or the server writes one). Because this gives your mail to someone else, it is limited to the dashboard and the Primitive CLI: other OAuth apps and agent tokens are refused. Also needs mail:share_own (sharing your own mail through email rules, which only shows it and never sends): a role without it is refused, whether or not it can send. Needs mail:read, a member's own credential with their full access (not an API key, agent key or narrowed token), and the email rules feature.",
12920
+ "description": "Writes the rule as yours, over your own mailboxes only, turns it on, and shares what it matches from now on with the member who asked. Send no body to accept it as asked, or change it first with english and tree together (and an explanation, or the server writes one). Because this gives your mail to someone else, it is limited to the dashboard and the Primitive CLI: other OAuth apps and agent tokens are refused. Before accepting, test it on your mail: pass the request's tree to POST /v1/email-rules/test. A test on mail already received shows only what has arrived. To see what the rule would do with an email that has not, such as one you would never want shared, pass the request's tree to POST /v1/email-rules/test-sample (testEmailRuleOnSample; in the Primitive CLI, primitive email-rules test-email-rule-on-sample --body-file sample.json) with an email you make up. Nothing is stored, and it needs no access beyond testing a rule of your own. Accepting also needs mail:share_own (sharing your own mail through email rules, which only shows it and never sends): a role without it is refused, whether or not it can send. Needs mail:read, a member's own credential with their full access (not an API key, agent key or narrowed token), and the email rules feature.",
12777
12921
  "hasJsonBody": true,
12778
12922
  "method": "POST",
12779
12923
  "operationId": "acceptEmailRuleRequest",
@@ -12871,6 +13015,25 @@ const operationManifest = [
12871
13015
  "type": "string",
12872
13016
  "minLength": 1,
12873
13017
  "maxLength": 64
13018
+ },
13019
+ "breadth": {
13020
+ "description": "Ignored (the compile answer's breadth).",
13021
+ "anyOf": [{
13022
+ "type": "object",
13023
+ "properties": {
13024
+ "checked": {
13025
+ "type": "integer",
13026
+ "minimum": 0,
13027
+ "description": "Classified emails the conditions were decided for: of the newest 200 from the last 30 days in your own mailboxes (for a grant rule, those you may share), the ones you can still read."
13028
+ },
13029
+ "matched": {
13030
+ "type": "integer",
13031
+ "minimum": 0,
13032
+ "description": "Of those, how many they hold for (for a do-not-share rule, one they cannot decide counts)."
13033
+ }
13034
+ },
13035
+ "required": ["checked", "matched"]
13036
+ }, { "type": "null" }]
12874
13037
  }
12875
13038
  },
12876
13039
  "additionalProperties": false
@@ -12933,7 +13096,8 @@ const operationManifest = [
12933
13096
  },
12934
13097
  "warnings": {
12935
13098
  "type": "array",
12936
- "items": { "type": "string" }
13099
+ "items": { "type": "string" },
13100
+ "description": "As the member who asked sent them, normally from their compile. The compiler's note that the rule matches nearly everything, in its own wording, counted that member's recent mail: anyone else reads it naming them."
12937
13101
  },
12938
13102
  "compiled_by": { "type": "string" },
12939
13103
  "vocabulary": { "type": "string" },
@@ -13016,6 +13180,14 @@ const operationManifest = [
13016
13180
  "type": ["string", "null"],
13017
13181
  "format": "uuid",
13018
13182
  "description": "The share accepting created, while it exists."
13183
+ },
13184
+ "message": {
13185
+ "description": "What the member asking wrote about why; null when they wrote nothing.",
13186
+ "type": ["string", "null"]
13187
+ },
13188
+ "decline_reason": {
13189
+ "description": "Why the member asked declined it; null unless they gave a reason when declining.",
13190
+ "type": ["string", "null"]
13019
13191
  }
13020
13192
  },
13021
13193
  "required": [
@@ -13092,10 +13264,37 @@ const operationManifest = [
13092
13264
  "type": ["integer", "null"],
13093
13265
  "exclusiveMinimum": 0
13094
13266
  },
13267
+ "revision": {
13268
+ "description": "Moves on by one with every update. Send it as if_revision.",
13269
+ "type": "integer",
13270
+ "exclusiveMinimum": 0
13271
+ },
13095
13272
  "author_user_id": {
13096
13273
  "type": "string",
13097
13274
  "format": "uuid"
13098
13275
  },
13276
+ "author": {
13277
+ "type": "object",
13278
+ "properties": {
13279
+ "user_id": {
13280
+ "type": "string",
13281
+ "format": "uuid"
13282
+ },
13283
+ "name": {
13284
+ "description": "Their name; null unless you can list members or this is you, and when they have none set.",
13285
+ "type": ["string", "null"]
13286
+ },
13287
+ "email": {
13288
+ "description": "Sign-in email; null unless you can list member emails or this is you.",
13289
+ "type": ["string", "null"]
13290
+ }
13291
+ },
13292
+ "required": [
13293
+ "user_id",
13294
+ "name",
13295
+ "email"
13296
+ ]
13297
+ },
13099
13298
  "is_author": {
13100
13299
  "type": "boolean",
13101
13300
  "description": "Whether you are its author, in your current membership. Only its author edits it; others who see it may only disable or delete it (while shared, only if they manage members)."
@@ -13108,91 +13307,94 @@ const operationManifest = [
13108
13307
  "type": "string",
13109
13308
  "format": "date-time"
13110
13309
  },
13111
- "current": { "anyOf": [{
13112
- "type": "object",
13113
- "properties": {
13114
- "version": {
13115
- "type": "integer",
13116
- "exclusiveMinimum": 0
13117
- },
13118
- "english": { "type": "string" },
13119
- "tree": {
13120
- "type": "object",
13121
- "properties": {
13122
- "version": {
13123
- "type": "number",
13124
- "const": 1
13125
- },
13126
- "vocabulary": {
13127
- "type": "string",
13128
- "description": "Vocabulary version, such as v1."
13129
- },
13130
- "when": {
13131
- "type": "object",
13132
- "propertyNames": { "type": "string" },
13133
- "additionalProperties": {},
13134
- "description": "A condition {column, op, value?, min_confidence?} (see GET /email-rules/vocabulary) or {all: [...]}, {any: [...]}, {not: ...}."
13135
- },
13136
- "questions": {
13137
- "type": "array",
13138
- "items": { "type": "string" },
13139
- "description": "Yes/no questions asked once the conditions match; all must be yes."
13140
- }
13310
+ "current": {
13311
+ "description": "Its current version: its text (english, tree, explanation, assumptions, warnings) and who compiled it. current_version is its number. GET /email-rules lists rules without it.",
13312
+ "anyOf": [{
13313
+ "type": "object",
13314
+ "properties": {
13315
+ "version": {
13316
+ "type": "integer",
13317
+ "exclusiveMinimum": 0
13141
13318
  },
13142
- "required": [
13143
- "version",
13144
- "vocabulary",
13145
- "when",
13146
- "questions"
13147
- ]
13148
- },
13149
- "explanation": { "type": "string" },
13150
- "assumptions": {
13151
- "type": "array",
13152
- "items": { "type": "string" }
13153
- },
13154
- "warnings": {
13155
- "type": "array",
13156
- "items": { "type": "string" }
13157
- },
13158
- "compiled_by": {
13159
- "type": "string",
13160
- "description": "Who produced the tree, as stated when saved (not verified), such as rule-compiler-v1 or member."
13161
- },
13162
- "vocabulary": { "type": "string" },
13163
- "created_at": {
13164
- "type": "string",
13165
- "format": "date-time"
13166
- },
13167
- "sql": {
13168
- "description": "Present only when include_sql was true.",
13169
- "type": "object",
13170
- "properties": {
13171
- "expression": {
13172
- "type": "string",
13173
- "description": "A parameterized boolean expression over one row per email."
13319
+ "english": { "type": "string" },
13320
+ "tree": {
13321
+ "type": "object",
13322
+ "properties": {
13323
+ "version": {
13324
+ "type": "number",
13325
+ "const": 1
13326
+ },
13327
+ "vocabulary": {
13328
+ "type": "string",
13329
+ "description": "Vocabulary version, such as v1."
13330
+ },
13331
+ "when": {
13332
+ "type": "object",
13333
+ "propertyNames": { "type": "string" },
13334
+ "additionalProperties": {},
13335
+ "description": "A condition {column, op, value?, min_confidence?} (see GET /email-rules/vocabulary) or {all: [...]}, {any: [...]}, {not: ...}."
13336
+ },
13337
+ "questions": {
13338
+ "type": "array",
13339
+ "items": { "type": "string" },
13340
+ "description": "Yes/no questions asked once the conditions match; all must be yes."
13341
+ }
13174
13342
  },
13175
- "params": {
13176
- "type": "array",
13177
- "items": {},
13178
- "description": "The values bound to $1, $2 and so on."
13179
- }
13343
+ "required": [
13344
+ "version",
13345
+ "vocabulary",
13346
+ "when",
13347
+ "questions"
13348
+ ]
13180
13349
  },
13181
- "required": ["expression", "params"]
13182
- }
13183
- },
13184
- "required": [
13185
- "version",
13186
- "english",
13187
- "tree",
13188
- "explanation",
13189
- "assumptions",
13190
- "warnings",
13191
- "compiled_by",
13192
- "vocabulary",
13193
- "created_at"
13194
- ]
13195
- }, { "type": "null" }] }
13350
+ "explanation": { "type": "string" },
13351
+ "assumptions": {
13352
+ "type": "array",
13353
+ "items": { "type": "string" }
13354
+ },
13355
+ "warnings": {
13356
+ "type": "array",
13357
+ "items": { "type": "string" }
13358
+ },
13359
+ "compiled_by": {
13360
+ "type": "string",
13361
+ "description": "Who produced the tree, as stated when saved (not verified), such as rule-compiler-v1 or member."
13362
+ },
13363
+ "vocabulary": { "type": "string" },
13364
+ "created_at": {
13365
+ "type": "string",
13366
+ "format": "date-time"
13367
+ },
13368
+ "sql": {
13369
+ "description": "Present only when include_sql was true.",
13370
+ "type": "object",
13371
+ "properties": {
13372
+ "expression": {
13373
+ "type": "string",
13374
+ "description": "A parameterized boolean expression over one row per email."
13375
+ },
13376
+ "params": {
13377
+ "type": "array",
13378
+ "items": {},
13379
+ "description": "The values bound to $1, $2 and so on."
13380
+ }
13381
+ },
13382
+ "required": ["expression", "params"]
13383
+ }
13384
+ },
13385
+ "required": [
13386
+ "version",
13387
+ "english",
13388
+ "tree",
13389
+ "explanation",
13390
+ "assumptions",
13391
+ "warnings",
13392
+ "compiled_by",
13393
+ "vocabulary",
13394
+ "created_at"
13395
+ ]
13396
+ }, { "type": "null" }]
13397
+ }
13196
13398
  },
13197
13399
  "required": [
13198
13400
  "id",
@@ -13232,8 +13434,10 @@ const operationManifest = [
13232
13434
  "removed_by_beneficiary",
13233
13435
  "stopped_by_admin",
13234
13436
  "rule_changed",
13437
+ "rule_deleted",
13235
13438
  null
13236
- ]
13439
+ ],
13440
+ "description": "Why it ended, once it has: stopped_by_granter, removed_by_beneficiary, stopped_by_admin, rule_changed (a new version of the rule was saved) or rule_deleted (the rule was deleted; the share keeps its record, and the rule is no longer shown). Null while it has not."
13237
13441
  },
13238
13442
  "accepted_rule_version": {
13239
13443
  "type": "integer",
@@ -13459,7 +13663,7 @@ const operationManifest = [
13459
13663
  "binaryResponse": false,
13460
13664
  "bodyRequired": true,
13461
13665
  "command": "compile-email-rule",
13462
- "description": "Turns a plain-English description of which received emails to match into a rule tree over the published column contract, with a plain-English explanation of what it matches, the assumptions made, and warnings for anything the columns cannot express. Where the columns fall short the rule may carry up to three yes/no questions, asked about an email only after its conditions match. Nothing is saved. A rule with no questions has its conditions counted, read only, over the caller's recent classified mail (what a test of it covers by default), so a warning can say when it matches nearly all of it. The SQL the rule renders to is returned only when include_sql is true. kind do_not_share compiles a do-not-share rule for recall: conditions only as narrow as the description says, the judgement of what it describes put into questions, and with nothing a column decides for certain, conditions that hold for every email. Needs mail:read, a member's own credential with their full access (not an API key, agent key or narrowed token), and the email rules feature.",
13666
+ "description": "Turns a plain-English description of which received emails to match into a rule tree over the published column contract, with a plain-English explanation of what it matches, the assumptions made, and warnings for anything the columns cannot express. Where the columns fall short the rule may carry up to three yes/no questions, asked about an email only after its conditions match. Nothing is saved. A rule with no questions has its conditions counted, read only, over the caller's recent classified mail (what a test of it covers by default): breadth is that count, and a warning says when it matches nearly all of it. The SQL the rule renders to is returned only when include_sql is true. kind do_not_share compiles a do-not-share rule for recall: conditions only as narrow as the description says, the judgement of what it describes put into questions, and with nothing a column decides for certain, conditions that hold for every email. Needs mail:read, a member's own credential with their full access (not an API key, agent key or narrowed token), and the email rules feature.",
13463
13667
  "hasJsonBody": true,
13464
13668
  "method": "POST",
13465
13669
  "operationId": "compileEmailRule",
@@ -13532,7 +13736,7 @@ const operationManifest = [
13532
13736
  },
13533
13737
  "explanation": {
13534
13738
  "type": "string",
13535
- "description": "What the rule matches, in plain English."
13739
+ "description": "What the rule matches, in plain English: only which emails, not what it grants or shares, which follows from its state, its scope and the keys, webhooks and people it reaches. The compiler leaves out a sentence that plainly says what the rule hands over."
13536
13740
  },
13537
13741
  "assumptions": {
13538
13742
  "type": "array",
@@ -13548,6 +13752,25 @@ const operationManifest = [
13548
13752
  "type": "string",
13549
13753
  "description": "An opaque name for the version of the rule compiler that produced the rule, such as rule-compiler-v1. Send it back as compiled_by when you save the rule."
13550
13754
  },
13755
+ "breadth": {
13756
+ "description": "How many of your recent emails its conditions hold for, counted over your own mailboxes whatever scope you save it with, warning or not. Null when not counted in full: a rule with questions, headers-only access, no test of it possible as you, or a count that failed or ran out of time. Writes ignore it, so a compile answer (without include_sql) can be sent back unchanged.",
13757
+ "anyOf": [{
13758
+ "type": "object",
13759
+ "properties": {
13760
+ "checked": {
13761
+ "type": "integer",
13762
+ "minimum": 0,
13763
+ "description": "Classified emails the conditions were decided for: of the newest 200 from the last 30 days in your own mailboxes (for a grant rule, those you may share), the ones you can still read."
13764
+ },
13765
+ "matched": {
13766
+ "type": "integer",
13767
+ "minimum": 0,
13768
+ "description": "Of those, how many they hold for (for a do-not-share rule, one they cannot decide counts)."
13769
+ }
13770
+ },
13771
+ "required": ["checked", "matched"]
13772
+ }, { "type": "null" }]
13773
+ },
13551
13774
  "sql": {
13552
13775
  "description": "Present only when include_sql was true.",
13553
13776
  "type": "object",
@@ -13743,6 +13966,25 @@ const operationManifest = [
13743
13966
  "type": "string",
13744
13967
  "minLength": 1,
13745
13968
  "maxLength": 64
13969
+ },
13970
+ "breadth": {
13971
+ "description": "Ignored (the compile answer's breadth).",
13972
+ "anyOf": [{
13973
+ "type": "object",
13974
+ "properties": {
13975
+ "checked": {
13976
+ "type": "integer",
13977
+ "minimum": 0,
13978
+ "description": "Classified emails the conditions were decided for: of the newest 200 from the last 30 days in your own mailboxes (for a grant rule, those you may share), the ones you can still read."
13979
+ },
13980
+ "matched": {
13981
+ "type": "integer",
13982
+ "minimum": 0,
13983
+ "description": "Of those, how many they hold for (for a do-not-share rule, one they cannot decide counts)."
13984
+ }
13985
+ },
13986
+ "required": ["checked", "matched"]
13987
+ }, { "type": "null" }]
13746
13988
  }
13747
13989
  },
13748
13990
  "required": [
@@ -13803,10 +14045,37 @@ const operationManifest = [
13803
14045
  "type": ["integer", "null"],
13804
14046
  "exclusiveMinimum": 0
13805
14047
  },
14048
+ "revision": {
14049
+ "description": "Moves on by one with every update. Send it as if_revision.",
14050
+ "type": "integer",
14051
+ "exclusiveMinimum": 0
14052
+ },
13806
14053
  "author_user_id": {
13807
14054
  "type": "string",
13808
14055
  "format": "uuid"
13809
14056
  },
14057
+ "author": {
14058
+ "type": "object",
14059
+ "properties": {
14060
+ "user_id": {
14061
+ "type": "string",
14062
+ "format": "uuid"
14063
+ },
14064
+ "name": {
14065
+ "description": "Their name; null unless you can list members or this is you, and when they have none set.",
14066
+ "type": ["string", "null"]
14067
+ },
14068
+ "email": {
14069
+ "description": "Sign-in email; null unless you can list member emails or this is you.",
14070
+ "type": ["string", "null"]
14071
+ }
14072
+ },
14073
+ "required": [
14074
+ "user_id",
14075
+ "name",
14076
+ "email"
14077
+ ]
14078
+ },
13810
14079
  "is_author": {
13811
14080
  "type": "boolean",
13812
14081
  "description": "Whether you are its author, in your current membership. Only its author edits it; others who see it may only disable or delete it (while shared, only if they manage members)."
@@ -13819,91 +14088,94 @@ const operationManifest = [
13819
14088
  "type": "string",
13820
14089
  "format": "date-time"
13821
14090
  },
13822
- "current": { "anyOf": [{
13823
- "type": "object",
13824
- "properties": {
13825
- "version": {
13826
- "type": "integer",
13827
- "exclusiveMinimum": 0
13828
- },
13829
- "english": { "type": "string" },
13830
- "tree": {
13831
- "type": "object",
13832
- "properties": {
13833
- "version": {
13834
- "type": "number",
13835
- "const": 1
13836
- },
13837
- "vocabulary": {
13838
- "type": "string",
13839
- "description": "Vocabulary version, such as v1."
13840
- },
13841
- "when": {
13842
- "type": "object",
13843
- "propertyNames": { "type": "string" },
13844
- "additionalProperties": {},
13845
- "description": "A condition {column, op, value?, min_confidence?} (see GET /email-rules/vocabulary) or {all: [...]}, {any: [...]}, {not: ...}."
13846
- },
13847
- "questions": {
13848
- "type": "array",
13849
- "items": { "type": "string" },
13850
- "description": "Yes/no questions asked once the conditions match; all must be yes."
13851
- }
14091
+ "current": {
14092
+ "description": "Its current version: its text (english, tree, explanation, assumptions, warnings) and who compiled it. current_version is its number. GET /email-rules lists rules without it.",
14093
+ "anyOf": [{
14094
+ "type": "object",
14095
+ "properties": {
14096
+ "version": {
14097
+ "type": "integer",
14098
+ "exclusiveMinimum": 0
13852
14099
  },
13853
- "required": [
13854
- "version",
13855
- "vocabulary",
13856
- "when",
13857
- "questions"
13858
- ]
13859
- },
13860
- "explanation": { "type": "string" },
13861
- "assumptions": {
13862
- "type": "array",
13863
- "items": { "type": "string" }
13864
- },
13865
- "warnings": {
13866
- "type": "array",
13867
- "items": { "type": "string" }
13868
- },
13869
- "compiled_by": {
13870
- "type": "string",
13871
- "description": "Who produced the tree, as stated when saved (not verified), such as rule-compiler-v1 or member."
13872
- },
13873
- "vocabulary": { "type": "string" },
13874
- "created_at": {
13875
- "type": "string",
13876
- "format": "date-time"
13877
- },
13878
- "sql": {
13879
- "description": "Present only when include_sql was true.",
13880
- "type": "object",
13881
- "properties": {
13882
- "expression": {
13883
- "type": "string",
13884
- "description": "A parameterized boolean expression over one row per email."
14100
+ "english": { "type": "string" },
14101
+ "tree": {
14102
+ "type": "object",
14103
+ "properties": {
14104
+ "version": {
14105
+ "type": "number",
14106
+ "const": 1
14107
+ },
14108
+ "vocabulary": {
14109
+ "type": "string",
14110
+ "description": "Vocabulary version, such as v1."
14111
+ },
14112
+ "when": {
14113
+ "type": "object",
14114
+ "propertyNames": { "type": "string" },
14115
+ "additionalProperties": {},
14116
+ "description": "A condition {column, op, value?, min_confidence?} (see GET /email-rules/vocabulary) or {all: [...]}, {any: [...]}, {not: ...}."
14117
+ },
14118
+ "questions": {
14119
+ "type": "array",
14120
+ "items": { "type": "string" },
14121
+ "description": "Yes/no questions asked once the conditions match; all must be yes."
14122
+ }
13885
14123
  },
13886
- "params": {
13887
- "type": "array",
13888
- "items": {},
13889
- "description": "The values bound to $1, $2 and so on."
13890
- }
14124
+ "required": [
14125
+ "version",
14126
+ "vocabulary",
14127
+ "when",
14128
+ "questions"
14129
+ ]
13891
14130
  },
13892
- "required": ["expression", "params"]
13893
- }
13894
- },
13895
- "required": [
13896
- "version",
13897
- "english",
13898
- "tree",
13899
- "explanation",
13900
- "assumptions",
13901
- "warnings",
13902
- "compiled_by",
13903
- "vocabulary",
13904
- "created_at"
13905
- ]
13906
- }, { "type": "null" }] },
14131
+ "explanation": { "type": "string" },
14132
+ "assumptions": {
14133
+ "type": "array",
14134
+ "items": { "type": "string" }
14135
+ },
14136
+ "warnings": {
14137
+ "type": "array",
14138
+ "items": { "type": "string" }
14139
+ },
14140
+ "compiled_by": {
14141
+ "type": "string",
14142
+ "description": "Who produced the tree, as stated when saved (not verified), such as rule-compiler-v1 or member."
14143
+ },
14144
+ "vocabulary": { "type": "string" },
14145
+ "created_at": {
14146
+ "type": "string",
14147
+ "format": "date-time"
14148
+ },
14149
+ "sql": {
14150
+ "description": "Present only when include_sql was true.",
14151
+ "type": "object",
14152
+ "properties": {
14153
+ "expression": {
14154
+ "type": "string",
14155
+ "description": "A parameterized boolean expression over one row per email."
14156
+ },
14157
+ "params": {
14158
+ "type": "array",
14159
+ "items": {},
14160
+ "description": "The values bound to $1, $2 and so on."
14161
+ }
14162
+ },
14163
+ "required": ["expression", "params"]
14164
+ }
14165
+ },
14166
+ "required": [
14167
+ "version",
14168
+ "english",
14169
+ "tree",
14170
+ "explanation",
14171
+ "assumptions",
14172
+ "warnings",
14173
+ "compiled_by",
14174
+ "vocabulary",
14175
+ "created_at"
14176
+ ]
14177
+ }, { "type": "null" }]
14178
+ },
13907
14179
  "scope_warnings": {
13908
14180
  "description": "Names in scope_selection that match nothing this organization receives mail at (a typo, or an inbox not yet in use). Present only when there are some.",
13909
14181
  "type": "array",
@@ -13935,7 +14207,7 @@ const operationManifest = [
13935
14207
  }
13936
14208
  },
13937
14209
  "expectations_check": {
13938
- "description": "After a write that saved a version or changed its expectations, if it has some: them, judged on that version from stored classifications without asking its questions. Undecided: one that needs answers not yet kept for that version, is unclassified, skipped or not reached, or that its author does not read. Absent when it could not be judged (the rule changed again, its author cannot be resolved, your access shows headers only, it could not finish in time, or the check failed). Test by rule_id for the rest.",
14210
+ "description": "After a write that saved a version or changed its expectations, if it has some: them, judged on that version from stored classifications without asking its questions. Undecided: one that needs answers not yet kept for that version, is unclassified, skipped or not reached, or that its author does not read. Absent when it could not be judged (the rule changed again, its author cannot be resolved, your access shows headers only, it could not finish in time, or the check failed). Test by rule_id for the rest. With if_expectations_pass, the judgment made before the save, which passed.",
13939
14211
  "type": "object",
13940
14212
  "properties": {
13941
14213
  "version": {
@@ -13963,6 +14235,10 @@ const operationManifest = [
13963
14235
  "type": "string",
13964
14236
  "format": "uuid"
13965
14237
  }
14238
+ },
14239
+ "note": {
14240
+ "description": "On an update that saved a version that did not pass: that it was saved anyway, is current, and whether it is live.",
14241
+ "type": "string"
13966
14242
  }
13967
14243
  },
13968
14244
  "required": [
@@ -13971,6 +14247,41 @@ const operationManifest = [
13971
14247
  "failed",
13972
14248
  "undecided"
13973
14249
  ]
14250
+ },
14251
+ "restored_from": {
14252
+ "description": "When this write saved an earlier version again (restore_version): which, and that the scope is not part of a version.",
14253
+ "type": "object",
14254
+ "properties": {
14255
+ "version": {
14256
+ "type": "integer",
14257
+ "exclusiveMinimum": 0
14258
+ },
14259
+ "note": { "type": "string" }
14260
+ },
14261
+ "required": ["version", "note"]
14262
+ },
14263
+ "affects": {
14264
+ "description": "After an update that saved a version: API keys listing the rule, the webhooks they own, and shares this write ended. The rule keeps its state. Absent if not read.",
14265
+ "type": "object",
14266
+ "properties": {
14267
+ "keys": {
14268
+ "type": "integer",
14269
+ "minimum": 0
14270
+ },
14271
+ "endpoints": {
14272
+ "type": "integer",
14273
+ "minimum": 0
14274
+ },
14275
+ "shares_ended": {
14276
+ "type": "integer",
14277
+ "minimum": 0
14278
+ }
14279
+ },
14280
+ "required": [
14281
+ "keys",
14282
+ "endpoints",
14283
+ "shares_ended"
14284
+ ]
13974
14285
  }
13975
14286
  },
13976
14287
  "required": [
@@ -13994,7 +14305,7 @@ const operationManifest = [
13994
14305
  "binaryResponse": false,
13995
14306
  "bodyRequired": true,
13996
14307
  "command": "create-email-rule-request",
13997
- "description": "Asks another member of the organization for a rule over their own mail, from a compile response or a tree you wrote. Name them by user id, or by their member mailbox address, or by their verified work email if you can list members (at most 30 lookups by address an hour). Nothing is evaluated and nothing is shared until they accept, and they may change the rule first; they see the request and can preview what it matches in their own mail. You may have at most 5 pending requests to one member and 50 in all. If you can list members (members:read), the answer also says whether they can accept it under their role (target_can_accept), and if not, why (target_warning); the request is sent either way. To find out before asking, use POST /v1/email-rule-requests/target-check. Needs mail:read, a member's own credential with their full access (not an API key, agent key or narrowed token), and the email rules feature.",
14308
+ "description": "Asks another member of the organization for a rule over their own mail, from a compile response or a tree you wrote. Name them by user id, or by their member mailbox address, or by their verified work email if you can list members (at most 30 lookups by address an hour). Nothing is evaluated and nothing is shared until they accept, and they may change the rule first; they see the request and can preview what it matches in their own mail. A message (up to 500 characters) tells them why you are asking; they see it with the request, and it cannot be changed later. You may have at most 5 pending requests to one member and 50 in all. If you can list members (members:read), the answer also says whether they can accept it under their role (target_can_accept), and if not, why (target_warning); the request is sent either way. To find out before asking, use POST /v1/email-rule-requests/target-check. Needs mail:read, a member's own credential with their full access (not an API key, agent key or narrowed token), and the email rules feature.",
13998
14309
  "hasJsonBody": true,
13999
14310
  "method": "POST",
14000
14311
  "operationId": "createEmailRuleRequest",
@@ -14031,6 +14342,12 @@ const operationManifest = [
14031
14342
  "maxLength": 200,
14032
14343
  "description": "A short name for the rule."
14033
14344
  },
14345
+ "message": {
14346
+ "type": ["string", "null"],
14347
+ "minLength": 1,
14348
+ "maxLength": 500,
14349
+ "description": "A short note to the member asked about why you are asking, up to 500 characters; null or absent for none. They see it with the request; it cannot be changed later."
14350
+ },
14034
14351
  "english": {
14035
14352
  "type": "string",
14036
14353
  "minLength": 1,
@@ -14098,6 +14415,25 @@ const operationManifest = [
14098
14415
  "type": "string",
14099
14416
  "minLength": 1,
14100
14417
  "maxLength": 64
14418
+ },
14419
+ "breadth": {
14420
+ "description": "Ignored (the compile answer's breadth).",
14421
+ "anyOf": [{
14422
+ "type": "object",
14423
+ "properties": {
14424
+ "checked": {
14425
+ "type": "integer",
14426
+ "minimum": 0,
14427
+ "description": "Classified emails the conditions were decided for: of the newest 200 from the last 30 days in your own mailboxes (for a grant rule, those you may share), the ones you can still read."
14428
+ },
14429
+ "matched": {
14430
+ "type": "integer",
14431
+ "minimum": 0,
14432
+ "description": "Of those, how many they hold for (for a do-not-share rule, one they cannot decide counts)."
14433
+ }
14434
+ },
14435
+ "required": ["checked", "matched"]
14436
+ }, { "type": "null" }]
14101
14437
  }
14102
14438
  },
14103
14439
  "required": [
@@ -14162,7 +14498,8 @@ const operationManifest = [
14162
14498
  },
14163
14499
  "warnings": {
14164
14500
  "type": "array",
14165
- "items": { "type": "string" }
14501
+ "items": { "type": "string" },
14502
+ "description": "As the member who asked sent them, normally from their compile. The compiler's note that the rule matches nearly everything, in its own wording, counted that member's recent mail: anyone else reads it naming them."
14166
14503
  },
14167
14504
  "compiled_by": { "type": "string" },
14168
14505
  "vocabulary": { "type": "string" },
@@ -14246,6 +14583,14 @@ const operationManifest = [
14246
14583
  "format": "uuid",
14247
14584
  "description": "The share accepting created, while it exists."
14248
14585
  },
14586
+ "message": {
14587
+ "description": "What the member asking wrote about why; null when they wrote nothing.",
14588
+ "type": ["string", "null"]
14589
+ },
14590
+ "decline_reason": {
14591
+ "description": "Why the member asked declined it; null unless they gave a reason when declining.",
14592
+ "type": ["string", "null"]
14593
+ },
14249
14594
  "target_can_accept": {
14250
14595
  "description": "Whether the member asked can accept it now under their role. Null unless you can list members and their roles (members:read), or when it could not be read.",
14251
14596
  "type": ["boolean", "null"]
@@ -14287,8 +14632,8 @@ const operationManifest = [
14287
14632
  "binaryResponse": false,
14288
14633
  "bodyRequired": false,
14289
14634
  "command": "decline-email-rule-request",
14290
- "description": "Declines a pending request made to you. Nothing is written but the decision. Needs mail:read, a member's own credential with their full access (not an API key, agent key or narrowed token), and the email rules feature.",
14291
- "hasJsonBody": false,
14635
+ "description": "Declines a pending request made to you. Nothing is written but the decision and, if you give one, its reason (reason, up to 500 characters), which the member who asked sees with the request. Needs mail:read, a member's own credential with their full access (not an API key, agent key or narrowed token), and the email rules feature.",
14636
+ "hasJsonBody": true,
14292
14637
  "method": "POST",
14293
14638
  "operationId": "declineEmailRuleRequest",
14294
14639
  "path": "/email-rule-requests/{id}/decline",
@@ -14309,7 +14654,16 @@ const operationManifest = [
14309
14654
  }],
14310
14655
  "permissions": ["mail:read"],
14311
14656
  "access": null,
14312
- "requestSchema": null,
14657
+ "requestSchema": {
14658
+ "type": "object",
14659
+ "properties": { "reason": {
14660
+ "type": ["string", "null"],
14661
+ "minLength": 1,
14662
+ "maxLength": 500,
14663
+ "description": "Why you are declining, up to 500 characters; null or absent for none. The member who asked sees it with the request."
14664
+ } },
14665
+ "additionalProperties": false
14666
+ },
14313
14667
  "responseSchema": {
14314
14668
  "type": "object",
14315
14669
  "properties": {
@@ -14365,7 +14719,8 @@ const operationManifest = [
14365
14719
  },
14366
14720
  "warnings": {
14367
14721
  "type": "array",
14368
- "items": { "type": "string" }
14722
+ "items": { "type": "string" },
14723
+ "description": "As the member who asked sent them, normally from their compile. The compiler's note that the rule matches nearly everything, in its own wording, counted that member's recent mail: anyone else reads it naming them."
14369
14724
  },
14370
14725
  "compiled_by": { "type": "string" },
14371
14726
  "vocabulary": { "type": "string" },
@@ -14448,6 +14803,14 @@ const operationManifest = [
14448
14803
  "type": ["string", "null"],
14449
14804
  "format": "uuid",
14450
14805
  "description": "The share accepting created, while it exists."
14806
+ },
14807
+ "message": {
14808
+ "description": "What the member asking wrote about why; null when they wrote nothing.",
14809
+ "type": ["string", "null"]
14810
+ },
14811
+ "decline_reason": {
14812
+ "description": "Why the member asked declined it; null unless they gave a reason when declining.",
14813
+ "type": ["string", "null"]
14451
14814
  }
14452
14815
  },
14453
14816
  "required": [
@@ -14482,7 +14845,7 @@ const operationManifest = [
14482
14845
  "binaryResponse": false,
14483
14846
  "bodyRequired": false,
14484
14847
  "command": "delete-email-rule",
14485
- "description": "Deletes a rule with its versions, its decisions and the access it granted. The author, or a caller who reads the whole organization's mail, may delete it; while it is shared with another member, only the author or someone who manages members from the dashboard. Needs mail:read, a member's own credential with their full access (not an API key, agent key or narrowed token), and the email rules feature.",
14848
+ "description": "Deletes a rule with its versions, its decisions and the access it granted. A rule that was ever shared with another member keeps the record of its share: the share ends with end_reason rule_deleted, and the member who shared can still list what it handed over, while the rule is no longer shown and reads nothing. The answer lists the shares the delete ended (ended_shares) and the keys, not revoked, that still list the rule, which now read nothing through it: those you may read as key metadata by id, name and prefix (keys), and how many others (other_keys). keys and other_keys are left out when the keys could not be read. The author, or a caller who reads the whole organization's mail, may delete it; while it is shared with another member, only the author or someone who manages members from the dashboard. Needs mail:read, a member's own credential with their full access (not an API key, agent key or narrowed token), and the email rules feature.",
14486
14849
  "hasJsonBody": false,
14487
14850
  "method": "DELETE",
14488
14851
  "operationId": "deleteEmailRule",
@@ -14501,7 +14864,41 @@ const operationManifest = [
14501
14864
  "requestSchema": null,
14502
14865
  "responseSchema": {
14503
14866
  "type": "object",
14504
- "properties": { "deleted": { "type": "boolean" } },
14867
+ "properties": {
14868
+ "deleted": { "type": "boolean" },
14869
+ "ended_shares": {
14870
+ "description": "The shares of this rule that the delete ended (end_reason rule_deleted). Each keeps its record.",
14871
+ "type": "array",
14872
+ "items": {
14873
+ "type": "string",
14874
+ "format": "uuid"
14875
+ }
14876
+ },
14877
+ "keys": {
14878
+ "description": "The API keys, not revoked, that still list the rule and that you may read (GET /v1/keys). They now read nothing through it; edit or revoke them. Left out, with other_keys, when the keys could not be read.",
14879
+ "type": "array",
14880
+ "items": {
14881
+ "type": "object",
14882
+ "properties": {
14883
+ "id": {
14884
+ "type": "string",
14885
+ "format": "uuid"
14886
+ },
14887
+ "name": { "type": ["string", "null"] },
14888
+ "key_prefix": { "type": "string" }
14889
+ },
14890
+ "required": [
14891
+ "id",
14892
+ "name",
14893
+ "key_prefix"
14894
+ ]
14895
+ }
14896
+ },
14897
+ "other_keys": {
14898
+ "description": "How many more keys, not revoked, list the rule that you may not read. They are not named.",
14899
+ "type": "integer"
14900
+ }
14901
+ },
14505
14902
  "required": ["deleted"]
14506
14903
  },
14507
14904
  "sdkName": "deleteEmailRule",
@@ -14575,7 +14972,7 @@ const operationManifest = [
14575
14972
  "binaryResponse": false,
14576
14973
  "bodyRequired": false,
14577
14974
  "command": "get-email-rule",
14578
- "description": "A rule, its scope (evaluation_scope, and scope_selection for a selected rule), its current version and its saved expectations. What the scope comes to for its author now is GET /email-rules/{id}/scope. The SQL the version renders to is included only when include_sql is true. Needs mail:read, a member's own credential with their full access (not an API key, agent key or narrowed token), and the email rules feature.",
14975
+ "description": "A rule, its scope (evaluation_scope, and scope_selection for a selected rule), its current version and its saved expectations. The current version's text (english, tree, explanation, assumptions, warnings) is under current, and current_version is its number. What the scope comes to for its author now is GET /email-rules/{id}/scope. The SQL the version renders to is included only when include_sql is true. Needs mail:read, a member's own credential with their full access (not an API key, agent key or narrowed token), and the email rules feature.",
14579
14976
  "hasJsonBody": false,
14580
14977
  "method": "GET",
14581
14978
  "operationId": "getEmailRule",
@@ -14649,10 +15046,37 @@ const operationManifest = [
14649
15046
  "type": ["integer", "null"],
14650
15047
  "exclusiveMinimum": 0
14651
15048
  },
15049
+ "revision": {
15050
+ "description": "Moves on by one with every update. Send it as if_revision.",
15051
+ "type": "integer",
15052
+ "exclusiveMinimum": 0
15053
+ },
14652
15054
  "author_user_id": {
14653
15055
  "type": "string",
14654
15056
  "format": "uuid"
14655
15057
  },
15058
+ "author": {
15059
+ "type": "object",
15060
+ "properties": {
15061
+ "user_id": {
15062
+ "type": "string",
15063
+ "format": "uuid"
15064
+ },
15065
+ "name": {
15066
+ "description": "Their name; null unless you can list members or this is you, and when they have none set.",
15067
+ "type": ["string", "null"]
15068
+ },
15069
+ "email": {
15070
+ "description": "Sign-in email; null unless you can list member emails or this is you.",
15071
+ "type": ["string", "null"]
15072
+ }
15073
+ },
15074
+ "required": [
15075
+ "user_id",
15076
+ "name",
15077
+ "email"
15078
+ ]
15079
+ },
14656
15080
  "is_author": {
14657
15081
  "type": "boolean",
14658
15082
  "description": "Whether you are its author, in your current membership. Only its author edits it; others who see it may only disable or delete it (while shared, only if they manage members)."
@@ -14665,91 +15089,94 @@ const operationManifest = [
14665
15089
  "type": "string",
14666
15090
  "format": "date-time"
14667
15091
  },
14668
- "current": { "anyOf": [{
14669
- "type": "object",
14670
- "properties": {
14671
- "version": {
14672
- "type": "integer",
14673
- "exclusiveMinimum": 0
14674
- },
14675
- "english": { "type": "string" },
14676
- "tree": {
14677
- "type": "object",
14678
- "properties": {
14679
- "version": {
14680
- "type": "number",
14681
- "const": 1
14682
- },
14683
- "vocabulary": {
14684
- "type": "string",
14685
- "description": "Vocabulary version, such as v1."
14686
- },
14687
- "when": {
14688
- "type": "object",
14689
- "propertyNames": { "type": "string" },
14690
- "additionalProperties": {},
14691
- "description": "A condition {column, op, value?, min_confidence?} (see GET /email-rules/vocabulary) or {all: [...]}, {any: [...]}, {not: ...}."
14692
- },
14693
- "questions": {
14694
- "type": "array",
14695
- "items": { "type": "string" },
14696
- "description": "Yes/no questions asked once the conditions match; all must be yes."
14697
- }
15092
+ "current": {
15093
+ "description": "Its current version: its text (english, tree, explanation, assumptions, warnings) and who compiled it. current_version is its number. GET /email-rules lists rules without it.",
15094
+ "anyOf": [{
15095
+ "type": "object",
15096
+ "properties": {
15097
+ "version": {
15098
+ "type": "integer",
15099
+ "exclusiveMinimum": 0
14698
15100
  },
14699
- "required": [
14700
- "version",
14701
- "vocabulary",
14702
- "when",
14703
- "questions"
14704
- ]
14705
- },
14706
- "explanation": { "type": "string" },
14707
- "assumptions": {
14708
- "type": "array",
14709
- "items": { "type": "string" }
14710
- },
14711
- "warnings": {
14712
- "type": "array",
14713
- "items": { "type": "string" }
14714
- },
14715
- "compiled_by": {
14716
- "type": "string",
14717
- "description": "Who produced the tree, as stated when saved (not verified), such as rule-compiler-v1 or member."
14718
- },
14719
- "vocabulary": { "type": "string" },
14720
- "created_at": {
14721
- "type": "string",
14722
- "format": "date-time"
14723
- },
14724
- "sql": {
14725
- "description": "Present only when include_sql was true.",
14726
- "type": "object",
14727
- "properties": {
14728
- "expression": {
14729
- "type": "string",
14730
- "description": "A parameterized boolean expression over one row per email."
15101
+ "english": { "type": "string" },
15102
+ "tree": {
15103
+ "type": "object",
15104
+ "properties": {
15105
+ "version": {
15106
+ "type": "number",
15107
+ "const": 1
15108
+ },
15109
+ "vocabulary": {
15110
+ "type": "string",
15111
+ "description": "Vocabulary version, such as v1."
15112
+ },
15113
+ "when": {
15114
+ "type": "object",
15115
+ "propertyNames": { "type": "string" },
15116
+ "additionalProperties": {},
15117
+ "description": "A condition {column, op, value?, min_confidence?} (see GET /email-rules/vocabulary) or {all: [...]}, {any: [...]}, {not: ...}."
15118
+ },
15119
+ "questions": {
15120
+ "type": "array",
15121
+ "items": { "type": "string" },
15122
+ "description": "Yes/no questions asked once the conditions match; all must be yes."
15123
+ }
14731
15124
  },
14732
- "params": {
14733
- "type": "array",
14734
- "items": {},
14735
- "description": "The values bound to $1, $2 and so on."
14736
- }
15125
+ "required": [
15126
+ "version",
15127
+ "vocabulary",
15128
+ "when",
15129
+ "questions"
15130
+ ]
14737
15131
  },
14738
- "required": ["expression", "params"]
14739
- }
14740
- },
14741
- "required": [
14742
- "version",
14743
- "english",
14744
- "tree",
14745
- "explanation",
14746
- "assumptions",
14747
- "warnings",
14748
- "compiled_by",
14749
- "vocabulary",
14750
- "created_at"
14751
- ]
14752
- }, { "type": "null" }] },
15132
+ "explanation": { "type": "string" },
15133
+ "assumptions": {
15134
+ "type": "array",
15135
+ "items": { "type": "string" }
15136
+ },
15137
+ "warnings": {
15138
+ "type": "array",
15139
+ "items": { "type": "string" }
15140
+ },
15141
+ "compiled_by": {
15142
+ "type": "string",
15143
+ "description": "Who produced the tree, as stated when saved (not verified), such as rule-compiler-v1 or member."
15144
+ },
15145
+ "vocabulary": { "type": "string" },
15146
+ "created_at": {
15147
+ "type": "string",
15148
+ "format": "date-time"
15149
+ },
15150
+ "sql": {
15151
+ "description": "Present only when include_sql was true.",
15152
+ "type": "object",
15153
+ "properties": {
15154
+ "expression": {
15155
+ "type": "string",
15156
+ "description": "A parameterized boolean expression over one row per email."
15157
+ },
15158
+ "params": {
15159
+ "type": "array",
15160
+ "items": {},
15161
+ "description": "The values bound to $1, $2 and so on."
15162
+ }
15163
+ },
15164
+ "required": ["expression", "params"]
15165
+ }
15166
+ },
15167
+ "required": [
15168
+ "version",
15169
+ "english",
15170
+ "tree",
15171
+ "explanation",
15172
+ "assumptions",
15173
+ "warnings",
15174
+ "compiled_by",
15175
+ "vocabulary",
15176
+ "created_at"
15177
+ ]
15178
+ }, { "type": "null" }]
15179
+ },
14753
15180
  "expectations": {
14754
15181
  "description": "Its saved expectations, each list by email id.",
14755
15182
  "type": "object",
@@ -14793,7 +15220,7 @@ const operationManifest = [
14793
15220
  "binaryResponse": false,
14794
15221
  "bodyRequired": false,
14795
15222
  "command": "get-email-rule-request",
14796
- "description": "A rule request: the rule as proposed, who made it and to whom, and whether each of them is still a member. A request is visible to the member who made it, the member it was made to, and the organization's owners and admins. Needs mail:read, a member's own credential with their full access (not an API key, agent key or narrowed token), and the email rules feature.",
15223
+ "description": "A rule request: the rule as proposed, who made it and to whom, and whether each of them is still a member. A request is visible to the member who made it, the member it was made to, and the organization's owners and admins. If you are the member asked, you can try the rule as proposed before answering: pass the request's tree to POST /v1/email-rules/test to test it on mail you have received. A test on mail already received shows only what has arrived. To see what the rule would do with an email that has not, such as one you would never want shared, pass the request's tree to POST /v1/email-rules/test-sample (testEmailRuleOnSample; in the Primitive CLI, primitive email-rules test-email-rule-on-sample --body-file sample.json) with an email you make up. Nothing is stored, and it needs no access beyond testing a rule of your own. Needs mail:read, a member's own credential with their full access (not an API key, agent key or narrowed token), and the email rules feature.",
14797
15224
  "hasJsonBody": false,
14798
15225
  "method": "GET",
14799
15226
  "operationId": "getEmailRuleRequest",
@@ -14865,7 +15292,8 @@ const operationManifest = [
14865
15292
  },
14866
15293
  "warnings": {
14867
15294
  "type": "array",
14868
- "items": { "type": "string" }
15295
+ "items": { "type": "string" },
15296
+ "description": "As the member who asked sent them, normally from their compile. The compiler's note that the rule matches nearly everything, in its own wording, counted that member's recent mail: anyone else reads it naming them."
14869
15297
  },
14870
15298
  "compiled_by": { "type": "string" },
14871
15299
  "vocabulary": { "type": "string" },
@@ -14948,6 +15376,14 @@ const operationManifest = [
14948
15376
  "type": ["string", "null"],
14949
15377
  "format": "uuid",
14950
15378
  "description": "The share accepting created, while it exists."
15379
+ },
15380
+ "message": {
15381
+ "description": "What the member asking wrote about why; null when they wrote nothing.",
15382
+ "type": ["string", "null"]
15383
+ },
15384
+ "decline_reason": {
15385
+ "description": "Why the member asked declined it; null unless they gave a reason when declining.",
15386
+ "type": ["string", "null"]
14951
15387
  }
14952
15388
  },
14953
15389
  "required": [
@@ -15158,8 +15594,10 @@ const operationManifest = [
15158
15594
  "removed_by_beneficiary",
15159
15595
  "stopped_by_admin",
15160
15596
  "rule_changed",
15597
+ "rule_deleted",
15161
15598
  null
15162
- ]
15599
+ ],
15600
+ "description": "Why it ended, once it has: stopped_by_granter, removed_by_beneficiary, stopped_by_admin, rule_changed (a new version of the rule was saved) or rule_deleted (the rule was deleted; the share keeps its record, and the rule is no longer shown). Null while it has not."
15163
15601
  },
15164
15602
  "accepted_rule_version": {
15165
15603
  "type": "integer",
@@ -15553,6 +15991,39 @@ const operationManifest = [
15553
15991
  "type": "boolean",
15554
15992
  "const": true
15555
15993
  },
15994
+ "withheld_by": {
15995
+ "description": "Set when do-not-share rules kept this match from being shared, so no key or webhook got it through the rule: those you can see, and why.",
15996
+ "type": "array",
15997
+ "items": {
15998
+ "type": "object",
15999
+ "properties": {
16000
+ "id": {
16001
+ "type": "string",
16002
+ "format": "uuid"
16003
+ },
16004
+ "name": { "type": "string" },
16005
+ "reason": {
16006
+ "type": "string",
16007
+ "enum": [
16008
+ "matched",
16009
+ "undecided",
16010
+ "outside_scope_stale"
16011
+ ],
16012
+ "description": "undecided: it had not decided the email, which blocks too. outside_scope_stale: cleared as outside its scope, and its author now reads the email."
16013
+ }
16014
+ },
16015
+ "required": [
16016
+ "id",
16017
+ "name",
16018
+ "reason"
16019
+ ]
16020
+ }
16021
+ },
16022
+ "withheld_by_hidden": {
16023
+ "type": "integer",
16024
+ "minimum": 0,
16025
+ "description": "With withheld_by: how many more kept it back that you cannot see, or that were deleted since. Those rules are not shown; an admin can see them."
16026
+ },
15556
16027
  "created_at": {
15557
16028
  "type": "string",
15558
16029
  "format": "date-time"
@@ -15703,7 +16174,8 @@ const operationManifest = [
15703
16174
  },
15704
16175
  "warnings": {
15705
16176
  "type": "array",
15706
- "items": { "type": "string" }
16177
+ "items": { "type": "string" },
16178
+ "description": "As the member who asked sent them, normally from their compile. The compiler's note that the rule matches nearly everything, in its own wording, counted that member's recent mail: anyone else reads it naming them."
15707
16179
  },
15708
16180
  "compiled_by": { "type": "string" },
15709
16181
  "vocabulary": { "type": "string" },
@@ -15786,6 +16258,14 @@ const operationManifest = [
15786
16258
  "type": ["string", "null"],
15787
16259
  "format": "uuid",
15788
16260
  "description": "The share accepting created, while it exists."
16261
+ },
16262
+ "message": {
16263
+ "description": "What the member asking wrote about why; null when they wrote nothing.",
16264
+ "type": ["string", "null"]
16265
+ },
16266
+ "decline_reason": {
16267
+ "description": "Why the member asked declined it; null unless they gave a reason when declining.",
16268
+ "type": ["string", "null"]
15789
16269
  }
15790
16270
  },
15791
16271
  "required": [
@@ -15841,6 +16321,36 @@ const operationManifest = [
15841
16321
  "responseSchema": {
15842
16322
  "type": "object",
15843
16323
  "properties": {
16324
+ "email": {
16325
+ "description": "The email these results are about, so a saved answer says which one it was.",
16326
+ "type": "object",
16327
+ "properties": {
16328
+ "id": {
16329
+ "type": "string",
16330
+ "format": "uuid"
16331
+ },
16332
+ "from_address": {
16333
+ "description": "The sender address, as GET /v1/emails/{id} shows it.",
16334
+ "type": ["string", "null"]
16335
+ },
16336
+ "recipient": {
16337
+ "type": "string",
16338
+ "description": "The mailbox this copy of the email was received in, as GET /v1/emails/{id} shows it."
16339
+ },
16340
+ "subject": { "type": ["string", "null"] },
16341
+ "received_at": {
16342
+ "type": "string",
16343
+ "format": "date-time"
16344
+ }
16345
+ },
16346
+ "required": [
16347
+ "id",
16348
+ "from_address",
16349
+ "recipient",
16350
+ "subject",
16351
+ "received_at"
16352
+ ]
16353
+ },
15844
16354
  "rules": {
15845
16355
  "type": "array",
15846
16356
  "items": {
@@ -15873,6 +16383,10 @@ const operationManifest = [
15873
16383
  "description": "The version of the rule that decided."
15874
16384
  },
15875
16385
  "matched": { "type": "boolean" },
16386
+ "near_miss": {
16387
+ "description": "Not matched, but nearly: its questions did not all say yes, or it needs several parts and exactly one failed while at least one part that held is not an exclusion (a top-level not, or is_false). False when detail_withheld is true. Decisions recorded under the earlier definition counted an exclusion too.",
16388
+ "type": "boolean"
16389
+ },
15876
16390
  "parts": {
15877
16391
  "type": "array",
15878
16392
  "items": {
@@ -15948,10 +16462,43 @@ const operationManifest = [
15948
16462
  }, { "type": "null" }]
15949
16463
  },
15950
16464
  "detail_withheld": {
15951
- "description": "Present, as true, on a result for an email whose content cannot be read (withheld, failed to parse or discarded): matched still says what the rule decided, but how it got there was read from that content, so parts and question_answers are empty. Absent otherwise.",
16465
+ "description": "Present, as true, on a result for an email whose content cannot be read (withheld, failed to parse or discarded): matched still says what the rule decided, but how it got there was read from that content, so parts and question_answers are empty and near_miss is false. Absent otherwise.",
15952
16466
  "type": "boolean",
15953
16467
  "const": true
15954
16468
  },
16469
+ "withheld_by": {
16470
+ "description": "Set when do-not-share rules kept this match from being shared, so no key or webhook got it through the rule: those you can see, and why.",
16471
+ "type": "array",
16472
+ "items": {
16473
+ "type": "object",
16474
+ "properties": {
16475
+ "id": {
16476
+ "type": "string",
16477
+ "format": "uuid"
16478
+ },
16479
+ "name": { "type": "string" },
16480
+ "reason": {
16481
+ "type": "string",
16482
+ "enum": [
16483
+ "matched",
16484
+ "undecided",
16485
+ "outside_scope_stale"
16486
+ ],
16487
+ "description": "undecided: it had not decided the email, which blocks too. outside_scope_stale: cleared as outside its scope, and its author now reads the email."
16488
+ }
16489
+ },
16490
+ "required": [
16491
+ "id",
16492
+ "name",
16493
+ "reason"
16494
+ ]
16495
+ }
16496
+ },
16497
+ "withheld_by_hidden": {
16498
+ "type": "integer",
16499
+ "minimum": 0,
16500
+ "description": "With withheld_by: how many more kept it back that you cannot see, or that were deleted since. Those rules are not shown; an admin can see them."
16501
+ },
15955
16502
  "created_at": {
15956
16503
  "type": "string",
15957
16504
  "format": "date-time"
@@ -16524,8 +17071,10 @@ const operationManifest = [
16524
17071
  "removed_by_beneficiary",
16525
17072
  "stopped_by_admin",
16526
17073
  "rule_changed",
17074
+ "rule_deleted",
16527
17075
  null
16528
- ]
17076
+ ],
17077
+ "description": "Why it ended, once it has: stopped_by_granter, removed_by_beneficiary, stopped_by_admin, rule_changed (a new version of the rule was saved) or rule_deleted (the rule was deleted; the share keeps its record, and the rule is no longer shown). Null while it has not."
16529
17078
  },
16530
17079
  "accepted_rule_version": {
16531
17080
  "type": "integer",
@@ -16801,7 +17350,7 @@ const operationManifest = [
16801
17350
  "binaryResponse": false,
16802
17351
  "bodyRequired": false,
16803
17352
  "command": "list-email-rules",
16804
- "description": "The rules the caller can see, newest first: their own, or every member's rules for a caller who reads the whole organization's mail. Each carries its scope (evaluation_scope, and scope_selection for a selected rule). With matched_email_id, only the rules that matched that email. Needs mail:read, a member's own credential with their full access (not an API key, agent key or narrowed token), and the email rules feature.",
17353
+ "description": "The rules the caller can see, newest first: their own, or every member's rules for a caller who reads the whole organization's mail. Each carries its scope (evaluation_scope, and scope_selection for a selected rule) and current_version, but not the version's text: read that under current from GET /email-rules/{id}. With matched_email_id, only the rules that matched that email. Needs mail:read, a member's own credential with their full access (not an API key, agent key or narrowed token), and the email rules feature.",
16805
17354
  "hasJsonBody": false,
16806
17355
  "method": "GET",
16807
17356
  "operationId": "listEmailRules",
@@ -16890,10 +17439,37 @@ const operationManifest = [
16890
17439
  "type": ["integer", "null"],
16891
17440
  "exclusiveMinimum": 0
16892
17441
  },
17442
+ "revision": {
17443
+ "description": "Moves on by one with every update. Send it as if_revision.",
17444
+ "type": "integer",
17445
+ "exclusiveMinimum": 0
17446
+ },
16893
17447
  "author_user_id": {
16894
17448
  "type": "string",
16895
17449
  "format": "uuid"
16896
17450
  },
17451
+ "author": {
17452
+ "type": "object",
17453
+ "properties": {
17454
+ "user_id": {
17455
+ "type": "string",
17456
+ "format": "uuid"
17457
+ },
17458
+ "name": {
17459
+ "description": "Their name; null unless you can list members or this is you, and when they have none set.",
17460
+ "type": ["string", "null"]
17461
+ },
17462
+ "email": {
17463
+ "description": "Sign-in email; null unless you can list member emails or this is you.",
17464
+ "type": ["string", "null"]
17465
+ }
17466
+ },
17467
+ "required": [
17468
+ "user_id",
17469
+ "name",
17470
+ "email"
17471
+ ]
17472
+ },
16897
17473
  "is_author": {
16898
17474
  "type": "boolean",
16899
17475
  "description": "Whether you are its author, in your current membership. Only its author edits it; others who see it may only disable or delete it (while shared, only if they manage members)."
@@ -16997,8 +17573,10 @@ const operationManifest = [
16997
17573
  "removed_by_beneficiary",
16998
17574
  "stopped_by_admin",
16999
17575
  "rule_changed",
17576
+ "rule_deleted",
17000
17577
  null
17001
- ]
17578
+ ],
17579
+ "description": "Why it ended, once it has: stopped_by_granter, removed_by_beneficiary, stopped_by_admin, rule_changed (a new version of the rule was saved) or rule_deleted (the rule was deleted; the share keeps its record, and the rule is no longer shown). Null while it has not."
17002
17580
  },
17003
17581
  "accepted_rule_version": {
17004
17582
  "type": "integer",
@@ -17420,6 +17998,10 @@ const operationManifest = [
17420
17998
  "format": "date-time"
17421
17999
  },
17422
18000
  "from_address": { "type": ["string", "null"] },
18001
+ "recipient": {
18002
+ "description": "The mailbox this copy of the email was received in, as GET /v1/emails/{id} shows it. Given with received_at, from_address and subject, so absent for not_found.",
18003
+ "type": "string"
18004
+ },
17423
18005
  "subject": { "type": ["string", "null"] },
17424
18006
  "parts": {
17425
18007
  "description": "How each top-level part of the rule decided. Present for matched, near_miss and no_match.",
@@ -18088,7 +18670,7 @@ const operationManifest = [
18088
18670
  "binaryResponse": false,
18089
18671
  "bodyRequired": true,
18090
18672
  "command": "update-email-rule",
18091
- "description": "Renames the rule, saves a new version (english and tree together, with an explanation or one the server writes; earlier versions stay), changes its state (draft is off; observe; active), whose mail it runs over (evaluation_scope, with scope_selection when selected) or its saved expectations (each saved version and new set is judged in expectations_check). Anyone but its author who sees it may only disable it (state draft), and while it is shared only if they manage members from the dashboard. Without evaluation_scope the scope stays; a scope change saves a new version with the same conditions, and what was granted stays. A shared rule keeps its scope; a kind never changes. Send if_version, the current_version you read, so a write never replaces a version saved since. Needs mail:read, a member's own credential with their full access (not an API key, agent key or narrowed token), and the email rules feature.",
18673
+ "description": "Renames the rule, saves a new version (english and tree together, with an explanation or one the server writes; earlier versions stay) or an earlier one again (restore_version), changes its state (draft is off; observe; active), whose mail it runs over (evaluation_scope, with scope_selection when selected) or its saved expectations (each saved version and new set is judged in expectations_check). Anyone but its author who sees it may only disable it (state draft), and while it is shared only if they manage members from the dashboard. Without evaluation_scope the scope stays; a scope change saves a new version with the same conditions, and what was granted stays. A shared rule keeps its scope; a kind never changes. A saved version takes effect at once (an active rule stays active); affects counts what it reaches. Send if_version, the current_version you read, so a write never replaces a version saved since, or if_revision to refuse it after any update. Without if_expectations_pass a version is saved even when its expectations_check fails. Needs mail:read, a member's own credential with their full access (not an API key, agent key or narrowed token), and the email rules feature.",
18092
18674
  "hasJsonBody": true,
18093
18675
  "method": "PATCH",
18094
18676
  "operationId": "updateEmailRule",
@@ -18190,6 +18772,25 @@ const operationManifest = [
18190
18772
  "minLength": 1,
18191
18773
  "maxLength": 64
18192
18774
  },
18775
+ "breadth": {
18776
+ "description": "Ignored (the compile answer's breadth).",
18777
+ "anyOf": [{
18778
+ "type": "object",
18779
+ "properties": {
18780
+ "checked": {
18781
+ "type": "integer",
18782
+ "minimum": 0,
18783
+ "description": "Classified emails the conditions were decided for: of the newest 200 from the last 30 days in your own mailboxes (for a grant rule, those you may share), the ones you can still read."
18784
+ },
18785
+ "matched": {
18786
+ "type": "integer",
18787
+ "minimum": 0,
18788
+ "description": "Of those, how many they hold for (for a do-not-share rule, one they cannot decide counts)."
18789
+ }
18790
+ },
18791
+ "required": ["checked", "matched"]
18792
+ }, { "type": "null" }]
18793
+ },
18193
18794
  "evaluation_scope": {
18194
18795
  "type": "string",
18195
18796
  "enum": [
@@ -18227,7 +18828,7 @@ const operationManifest = [
18227
18828
  "description": "For a selected rule: inboxes, and domains (each address at them the author reads); each one you can read."
18228
18829
  },
18229
18830
  "if_version": {
18230
- "description": "The current_version you read. If the rule's now differs, nothing changes: 409 email_rule_conflict, details.reason version_moved and details.current_version. Send it with a new version so a concurrent edit is never replaced unseen. It guards versions only: a change to expectations alone saves none.",
18831
+ "description": "The current_version you read: if it has moved, nothing is saved (409 email_rule_conflict, version_moved). It guards versions only; if_revision guards every update.",
18231
18832
  "type": "integer",
18232
18833
  "minimum": 1
18233
18834
  },
@@ -18253,6 +18854,22 @@ const operationManifest = [
18253
18854
  },
18254
18855
  "additionalProperties": false,
18255
18856
  "description": "Replaces its saved expectations (author only); null removes them."
18857
+ },
18858
+ "if_revision": {
18859
+ "description": "The revision you read: if the rule was updated since, nothing is saved (409 email_rule_conflict, revision_moved).",
18860
+ "type": "integer",
18861
+ "minimum": 1,
18862
+ "maximum": 2147483647
18863
+ },
18864
+ "if_expectations_pass": {
18865
+ "description": "true: judge the rule as this write leaves it (new tree, restored version, new scope, or the version it activates) against the expectations it leaves, as POST /email-rules/test would, before saving; it can take as long as a test. Failed: 409 expectations_failed; undecided: 409 expectations_undecided (both with details.expectations_check); none to judge: 400 no_expectations. Refused writes save nothing; a pass saves unless the rule moved meanwhile (409 email_rule_conflict). Only writes that save a version or activate the rule are judged.",
18866
+ "type": "boolean"
18867
+ },
18868
+ "restore_version": {
18869
+ "description": "Save version n again as the next version (author only): its english, tree, explanation, assumptions, warnings and compiled_by. A version carries no scope: the rule keeps its own unless evaluation_scope is sent. 404 version_not_found; 400 already_current or version_not_renderable. Not with a version's own fields.",
18870
+ "type": "integer",
18871
+ "minimum": 1,
18872
+ "maximum": 2147483647
18256
18873
  }
18257
18874
  },
18258
18875
  "additionalProperties": false
@@ -18308,10 +18925,37 @@ const operationManifest = [
18308
18925
  "type": ["integer", "null"],
18309
18926
  "exclusiveMinimum": 0
18310
18927
  },
18928
+ "revision": {
18929
+ "description": "Moves on by one with every update. Send it as if_revision.",
18930
+ "type": "integer",
18931
+ "exclusiveMinimum": 0
18932
+ },
18311
18933
  "author_user_id": {
18312
18934
  "type": "string",
18313
18935
  "format": "uuid"
18314
18936
  },
18937
+ "author": {
18938
+ "type": "object",
18939
+ "properties": {
18940
+ "user_id": {
18941
+ "type": "string",
18942
+ "format": "uuid"
18943
+ },
18944
+ "name": {
18945
+ "description": "Their name; null unless you can list members or this is you, and when they have none set.",
18946
+ "type": ["string", "null"]
18947
+ },
18948
+ "email": {
18949
+ "description": "Sign-in email; null unless you can list member emails or this is you.",
18950
+ "type": ["string", "null"]
18951
+ }
18952
+ },
18953
+ "required": [
18954
+ "user_id",
18955
+ "name",
18956
+ "email"
18957
+ ]
18958
+ },
18315
18959
  "is_author": {
18316
18960
  "type": "boolean",
18317
18961
  "description": "Whether you are its author, in your current membership. Only its author edits it; others who see it may only disable or delete it (while shared, only if they manage members)."
@@ -18324,91 +18968,94 @@ const operationManifest = [
18324
18968
  "type": "string",
18325
18969
  "format": "date-time"
18326
18970
  },
18327
- "current": { "anyOf": [{
18328
- "type": "object",
18329
- "properties": {
18330
- "version": {
18331
- "type": "integer",
18332
- "exclusiveMinimum": 0
18333
- },
18334
- "english": { "type": "string" },
18335
- "tree": {
18336
- "type": "object",
18337
- "properties": {
18338
- "version": {
18339
- "type": "number",
18340
- "const": 1
18341
- },
18342
- "vocabulary": {
18343
- "type": "string",
18344
- "description": "Vocabulary version, such as v1."
18345
- },
18346
- "when": {
18347
- "type": "object",
18348
- "propertyNames": { "type": "string" },
18349
- "additionalProperties": {},
18350
- "description": "A condition {column, op, value?, min_confidence?} (see GET /email-rules/vocabulary) or {all: [...]}, {any: [...]}, {not: ...}."
18351
- },
18352
- "questions": {
18353
- "type": "array",
18354
- "items": { "type": "string" },
18355
- "description": "Yes/no questions asked once the conditions match; all must be yes."
18356
- }
18971
+ "current": {
18972
+ "description": "Its current version: its text (english, tree, explanation, assumptions, warnings) and who compiled it. current_version is its number. GET /email-rules lists rules without it.",
18973
+ "anyOf": [{
18974
+ "type": "object",
18975
+ "properties": {
18976
+ "version": {
18977
+ "type": "integer",
18978
+ "exclusiveMinimum": 0
18357
18979
  },
18358
- "required": [
18359
- "version",
18360
- "vocabulary",
18361
- "when",
18362
- "questions"
18363
- ]
18364
- },
18365
- "explanation": { "type": "string" },
18366
- "assumptions": {
18367
- "type": "array",
18368
- "items": { "type": "string" }
18369
- },
18370
- "warnings": {
18371
- "type": "array",
18372
- "items": { "type": "string" }
18373
- },
18374
- "compiled_by": {
18375
- "type": "string",
18376
- "description": "Who produced the tree, as stated when saved (not verified), such as rule-compiler-v1 or member."
18377
- },
18378
- "vocabulary": { "type": "string" },
18379
- "created_at": {
18380
- "type": "string",
18381
- "format": "date-time"
18382
- },
18383
- "sql": {
18384
- "description": "Present only when include_sql was true.",
18385
- "type": "object",
18386
- "properties": {
18387
- "expression": {
18388
- "type": "string",
18389
- "description": "A parameterized boolean expression over one row per email."
18980
+ "english": { "type": "string" },
18981
+ "tree": {
18982
+ "type": "object",
18983
+ "properties": {
18984
+ "version": {
18985
+ "type": "number",
18986
+ "const": 1
18987
+ },
18988
+ "vocabulary": {
18989
+ "type": "string",
18990
+ "description": "Vocabulary version, such as v1."
18991
+ },
18992
+ "when": {
18993
+ "type": "object",
18994
+ "propertyNames": { "type": "string" },
18995
+ "additionalProperties": {},
18996
+ "description": "A condition {column, op, value?, min_confidence?} (see GET /email-rules/vocabulary) or {all: [...]}, {any: [...]}, {not: ...}."
18997
+ },
18998
+ "questions": {
18999
+ "type": "array",
19000
+ "items": { "type": "string" },
19001
+ "description": "Yes/no questions asked once the conditions match; all must be yes."
19002
+ }
18390
19003
  },
18391
- "params": {
18392
- "type": "array",
18393
- "items": {},
18394
- "description": "The values bound to $1, $2 and so on."
18395
- }
19004
+ "required": [
19005
+ "version",
19006
+ "vocabulary",
19007
+ "when",
19008
+ "questions"
19009
+ ]
18396
19010
  },
18397
- "required": ["expression", "params"]
18398
- }
18399
- },
18400
- "required": [
18401
- "version",
18402
- "english",
18403
- "tree",
18404
- "explanation",
18405
- "assumptions",
18406
- "warnings",
18407
- "compiled_by",
18408
- "vocabulary",
18409
- "created_at"
18410
- ]
18411
- }, { "type": "null" }] },
19011
+ "explanation": { "type": "string" },
19012
+ "assumptions": {
19013
+ "type": "array",
19014
+ "items": { "type": "string" }
19015
+ },
19016
+ "warnings": {
19017
+ "type": "array",
19018
+ "items": { "type": "string" }
19019
+ },
19020
+ "compiled_by": {
19021
+ "type": "string",
19022
+ "description": "Who produced the tree, as stated when saved (not verified), such as rule-compiler-v1 or member."
19023
+ },
19024
+ "vocabulary": { "type": "string" },
19025
+ "created_at": {
19026
+ "type": "string",
19027
+ "format": "date-time"
19028
+ },
19029
+ "sql": {
19030
+ "description": "Present only when include_sql was true.",
19031
+ "type": "object",
19032
+ "properties": {
19033
+ "expression": {
19034
+ "type": "string",
19035
+ "description": "A parameterized boolean expression over one row per email."
19036
+ },
19037
+ "params": {
19038
+ "type": "array",
19039
+ "items": {},
19040
+ "description": "The values bound to $1, $2 and so on."
19041
+ }
19042
+ },
19043
+ "required": ["expression", "params"]
19044
+ }
19045
+ },
19046
+ "required": [
19047
+ "version",
19048
+ "english",
19049
+ "tree",
19050
+ "explanation",
19051
+ "assumptions",
19052
+ "warnings",
19053
+ "compiled_by",
19054
+ "vocabulary",
19055
+ "created_at"
19056
+ ]
19057
+ }, { "type": "null" }]
19058
+ },
18412
19059
  "scope_warnings": {
18413
19060
  "description": "Names in scope_selection that match nothing this organization receives mail at (a typo, or an inbox not yet in use). Present only when there are some.",
18414
19061
  "type": "array",
@@ -18440,7 +19087,7 @@ const operationManifest = [
18440
19087
  }
18441
19088
  },
18442
19089
  "expectations_check": {
18443
- "description": "After a write that saved a version or changed its expectations, if it has some: them, judged on that version from stored classifications without asking its questions. Undecided: one that needs answers not yet kept for that version, is unclassified, skipped or not reached, or that its author does not read. Absent when it could not be judged (the rule changed again, its author cannot be resolved, your access shows headers only, it could not finish in time, or the check failed). Test by rule_id for the rest.",
19090
+ "description": "After a write that saved a version or changed its expectations, if it has some: them, judged on that version from stored classifications without asking its questions. Undecided: one that needs answers not yet kept for that version, is unclassified, skipped or not reached, or that its author does not read. Absent when it could not be judged (the rule changed again, its author cannot be resolved, your access shows headers only, it could not finish in time, or the check failed). Test by rule_id for the rest. With if_expectations_pass, the judgment made before the save, which passed.",
18444
19091
  "type": "object",
18445
19092
  "properties": {
18446
19093
  "version": {
@@ -18468,6 +19115,10 @@ const operationManifest = [
18468
19115
  "type": "string",
18469
19116
  "format": "uuid"
18470
19117
  }
19118
+ },
19119
+ "note": {
19120
+ "description": "On an update that saved a version that did not pass: that it was saved anyway, is current, and whether it is live.",
19121
+ "type": "string"
18471
19122
  }
18472
19123
  },
18473
19124
  "required": [
@@ -18476,6 +19127,41 @@ const operationManifest = [
18476
19127
  "failed",
18477
19128
  "undecided"
18478
19129
  ]
19130
+ },
19131
+ "restored_from": {
19132
+ "description": "When this write saved an earlier version again (restore_version): which, and that the scope is not part of a version.",
19133
+ "type": "object",
19134
+ "properties": {
19135
+ "version": {
19136
+ "type": "integer",
19137
+ "exclusiveMinimum": 0
19138
+ },
19139
+ "note": { "type": "string" }
19140
+ },
19141
+ "required": ["version", "note"]
19142
+ },
19143
+ "affects": {
19144
+ "description": "After an update that saved a version: API keys listing the rule, the webhooks they own, and shares this write ended. The rule keeps its state. Absent if not read.",
19145
+ "type": "object",
19146
+ "properties": {
19147
+ "keys": {
19148
+ "type": "integer",
19149
+ "minimum": 0
19150
+ },
19151
+ "endpoints": {
19152
+ "type": "integer",
19153
+ "minimum": 0
19154
+ },
19155
+ "shares_ended": {
19156
+ "type": "integer",
19157
+ "minimum": 0
19158
+ }
19159
+ },
19160
+ "required": [
19161
+ "keys",
19162
+ "endpoints",
19163
+ "shares_ended"
19164
+ ]
18479
19165
  }
18480
19166
  },
18481
19167
  "required": [
@@ -18577,7 +19263,8 @@ const operationManifest = [
18577
19263
  },
18578
19264
  "warnings": {
18579
19265
  "type": "array",
18580
- "items": { "type": "string" }
19266
+ "items": { "type": "string" },
19267
+ "description": "As the member who asked sent them, normally from their compile. The compiler's note that the rule matches nearly everything, in its own wording, counted that member's recent mail: anyone else reads it naming them."
18581
19268
  },
18582
19269
  "compiled_by": { "type": "string" },
18583
19270
  "vocabulary": { "type": "string" },
@@ -18660,6 +19347,14 @@ const operationManifest = [
18660
19347
  "type": ["string", "null"],
18661
19348
  "format": "uuid",
18662
19349
  "description": "The share accepting created, while it exists."
19350
+ },
19351
+ "message": {
19352
+ "description": "What the member asking wrote about why; null when they wrote nothing.",
19353
+ "type": ["string", "null"]
19354
+ },
19355
+ "decline_reason": {
19356
+ "description": "Why the member asked declined it; null unless they gave a reason when declining.",
19357
+ "type": ["string", "null"]
18663
19358
  }
18664
19359
  },
18665
19360
  "required": [
@@ -20473,7 +21168,7 @@ const operationManifest = [
20473
21168
  "type": "string"
20474
21169
  },
20475
21170
  {
20476
- "description": "Exact case-insensitive delivered recipient mailbox, applied before pagination. Combines with existing filters and cursors; does not search message text.",
21171
+ "description": "Exact case-insensitive delivered recipient mailbox, applied before pagination. Combines with existing filters and cursors; does not search message text. It filters only the mail the caller can read, and is never refused for that (200): a mailbox outside the caller's mail access lists nothing, and one the caller reads only in part (for example through an email rule) lists only that part. For a caller limited to some mailboxes, meta.recipient_outside_access or meta.recipient_rules_only says so when the mailbox is neither one of its addresses nor on one of its domains.",
20477
21172
  "enum": null,
20478
21173
  "name": "recipient",
20479
21174
  "required": false,
@@ -21955,7 +22650,7 @@ const operationManifest = [
21955
22650
  "binaryResponse": false,
21956
22651
  "bodyRequired": true,
21957
22652
  "command": "create-endpoint",
21958
- "description": "Creates a new webhook endpoint. If a deactivated endpoint\nwith the same URL and domain exists, it is reactivated\ninstead. Subject to plan limits on the number of active\nendpoints.\n\n**Signing.** A webhook is signed with the organization's webhook secret (`GET /account/webhook-secret`) unless it has its own. A webhook made as a person's own, or by an API key limited to some mailboxes, a third-party app or an agent delegate for itself, is made with its own secret: the response carries it once as `signing_secret`, and `GET /endpoints/{id}/signing-secret` returns it afterwards. Other webhooks, those a person who can read every mailbox makes among them, sign with the organization's secret until given their own. `signed_with` on the endpoint says which secret signs it. See the API-level \"Webhook\nsigning\" section for the full wire format (header name,\nsigned string, hash algo, secret format, tolerance) and a\nlanguage-agnostic verification recipe.\n\nAfter creating the endpoint, fire a test delivery against\nit via `POST /endpoints/{id}/test` to confirm your verifier\naccepts the signature.\n\n\nFor local receiving, use kind=pull and a stable name, without url, function_id or domain_id. Named creation resumes the same active destination; omitted filters preserve its selection, while conflicting supplied configuration returns 409. New unfiltered destinations receive all subsequently ready eligible event types within their credential scope. Connected-agent credentials automatically create private subscriptions restricted to inbound email for their assigned address. Names are isolated per credential; revocation or replacement deletes its subscriptions. Address-scoped events contain parsed mail without raw MIME, signed download links, routing metadata, or sibling envelope recipients. Pull destinations do not occupy HTTP routing slots. Signing-secret setup and a test HTTP delivery are not required for pull receiving.\n\nFor local receiving, use kind=pull and a stable name, without url, function_id or domain_id. Named creation resumes the same active destination; omitted filters preserve its selection, while conflicting supplied configuration returns 409. Creating a pull destination requires a connected-agent credential; it creates a private subscription restricted to inbound email for the assigned address of that credential. Account API keys receive 403 pull_subscription_requires_agent_connection and can instead receive mail with GET /emails?since=<cursor>&wait=30 or an HTTP endpoint. Pulling or completing an account destination created earlier returns the same 403. Names are isolated per credential; revocation or replacement deletes its subscriptions. Address-scoped events contain parsed mail without raw MIME, signed download links, routing metadata, or sibling envelope recipients. Pull destinations do not occupy HTTP routing slots. Signing-secret setup and a test HTTP delivery are not required for pull receiving.\n\nAgent connection keys (pconn_) may use endpoints.pull:manage instead of endpoints:manage here, for their own pull subscriptions only.",
22653
+ "description": "Creates a new webhook endpoint. A URL can have one endpoint per domain, route targets included (unlike the one default destination per domain), and endpoints with no domain_id count as one domain. A disabled endpoint still holds its URL; a deleted one does not. A create at a URL another endpoint has on the same domain is refused (400). A deleted endpoint's URL can be used again: the create makes a new endpoint, with a new id, and the deleted one stays as it was. Subject to plan limits on the number of active\nendpoints.\n\n**Signing.** A webhook is signed with the organization's webhook secret (`GET /account/webhook-secret`) unless it has its own. A webhook made as a person's own, or by an API key limited to some mailboxes, a third-party app or an agent delegate for itself, is made with its own secret: the response carries it once as `signing_secret`, and `GET /endpoints/{id}/signing-secret` returns it afterwards. Other webhooks, those a person who can read every mailbox makes among them, sign with the organization's secret until given their own. `signed_with` on the endpoint says which secret signs it. See the API-level \"Webhook\nsigning\" section for the full wire format (header name,\nsigned string, hash algo, secret format, tolerance) and a\nlanguage-agnostic verification recipe.\n\nAfter creating the endpoint, fire a test delivery against\nit via `POST /endpoints/{id}/test` to confirm your verifier\naccepts the signature.\n\n\nFor local receiving, use kind=pull and a stable name, without url, function_id or domain_id. Named creation resumes the same active destination; omitted filters preserve its selection, while conflicting supplied configuration returns 409. New unfiltered destinations receive all subsequently ready eligible event types within their credential scope. Connected-agent credentials automatically create private subscriptions restricted to inbound email for their assigned address. Names are isolated per credential; revocation or replacement deletes its subscriptions. Address-scoped events contain parsed mail without raw MIME, signed download links, routing metadata, or sibling envelope recipients. Pull destinations do not occupy HTTP routing slots. Signing-secret setup and a test HTTP delivery are not required for pull receiving.\n\nFor local receiving, use kind=pull and a stable name, without url, function_id or domain_id. Named creation resumes the same active destination; omitted filters preserve its selection, while conflicting supplied configuration returns 409. Creating a pull destination requires a connected-agent credential; it creates a private subscription restricted to inbound email for the assigned address of that credential. Account API keys receive 403 pull_subscription_requires_agent_connection and can instead receive mail with GET /emails?since=<cursor>&wait=30 or an HTTP endpoint. Pulling or completing an account destination created earlier returns the same 403. Names are isolated per credential; revocation or replacement deletes its subscriptions. Address-scoped events contain parsed mail without raw MIME, signed download links, routing metadata, or sibling envelope recipients. Pull destinations do not occupy HTTP routing slots. Signing-secret setup and a test HTTP delivery are not required for pull receiving.\n\nAgent connection keys (pconn_) may use endpoints.pull:manage instead of endpoints:manage here, for their own pull subscriptions only.",
21959
22654
  "hasJsonBody": true,
21960
22655
  "method": "POST",
21961
22656
  "operationId": "createEndpoint",
@@ -22257,13 +22952,13 @@ const operationManifest = [
22257
22952
  "type": "boolean"
22258
22953
  },
22259
22954
  "owner_key_id": {
22260
- "description": "The API key that owns this webhook. In endpoint responses it is set only for a caller who may read API key metadata, and is null otherwise and when the organization owns the webhook. After a rotation, the key that carries on for the key first named while it can act.",
22955
+ "description": "The API key that owns this webhook. In endpoint responses it is set only for a caller who may read API key metadata, and is null otherwise and when the organization owns the webhook. After a rotation, the key that carries on for the key first named while it can act. When none can, the key delivery stops on: the key first named while it is suspended and not expired, otherwise the newest key of its rotation that is not revoked, otherwise the newest one (only the key first named when nothing may carry on for it).",
22261
22956
  "type": ["string", "null"],
22262
22957
  "format": "uuid",
22263
22958
  "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
22264
22959
  },
22265
22960
  "owner_key_status": {
22266
- "description": "Whether the owning key can act now. In endpoint responses it is set only for a caller who may read API key metadata. Suspended also covers an expired key. A suspended or revoked owner is sent nothing.",
22961
+ "description": "Whether the owning key can act now. In endpoint responses it is set only for a caller who may read API key metadata. It is the state delivery acts on. Suspended also covers an expired key. A suspended or revoked owner is sent nothing. Anyone who may read the endpoint learns that from delivery.reasons, which never names the key.",
22267
22962
  "type": ["string", "null"],
22268
22963
  "enum": [
22269
22964
  "active",
@@ -22272,6 +22967,65 @@ const operationManifest = [
22272
22967
  null
22273
22968
  ]
22274
22969
  },
22970
+ "disabled_reason": {
22971
+ "description": "Why the platform turned this endpoint off, when it names a reason. owner_changed: it was given to a new owner while its URL stayed, and the URL was chosen under the previous owner; turning it on or giving it a new URL starts it again. owner_credential_revoked: the third-party app or agent delegate grant that owned it was revoked; an owner or admin who turns it back on takes it over. Null while it is on and when it was turned off by hand. An access suspension does not set it (a suspension is decided through its own record). Other values may be added.",
22972
+ "type": ["string", "null"]
22973
+ },
22974
+ "delivery": {
22975
+ "description": "For a webhook (kind http) in GET /endpoints and GET /endpoints/{id}: whether it is being sent received mail, and why not. Shown to anyone who may read the endpoint; it names a state, never a key. Null when it could not be checked just now. Absent from other kinds of endpoint and from create and update answers.",
22976
+ "type": ["object", "null"],
22977
+ "properties": {
22978
+ "delivering": {
22979
+ "type": "boolean",
22980
+ "description": "Whether the webhook is being sent received mail at all now: it is on, it is not held, and its owner can receive mail. Which messages reach it is still decided per message, by routing, rule grants and its own rules."
22981
+ },
22982
+ "reasons": {
22983
+ "type": "array",
22984
+ "items": {
22985
+ "type": "object",
22986
+ "properties": {
22987
+ "code": { "type": "string" },
22988
+ "message": { "type": "string" }
22989
+ },
22990
+ "required": ["code", "message"],
22991
+ "additionalProperties": {}
22992
+ },
22993
+ "description": "Every reason it is not, in the order delivery checks them; empty when it is. Codes, of which more may be added: disabled: the endpoint is off (disabled_reason on the endpoint says why). held: the endpoint, or the route the email came through, is held pending an owner or admin decision after its creator's access was reduced. acting_owner_revoked, acting_owner_suspended (which covers an expired key) and acting_owner_left: its owner cannot act now. acting_owner_permission: its owner does not read mail. acting_owner_depth: its owner sees headers only. acting_owner_scope: its owner does not read this email's mailbox, or, with no email to judge, reads no received mail at all now. These three name what the owner's access holds, so a caller who may not read API key metadata sees acting_owner_access in their place."
22994
+ },
22995
+ "held_for_owner": {
22996
+ "description": "Rule-granted mail held for this webhook while its owner cannot act; null when none is. Only for a caller with access to every mailbox (held mail can be an earlier owner's), and absent when it could not be read just now.",
22997
+ "type": ["object", "null"],
22998
+ "properties": {
22999
+ "count": {
23000
+ "type": "integer",
23001
+ "minimum": -9007199254740991,
23002
+ "maximum": 9007199254740991,
23003
+ "description": "Rule-granted emails waiting for this webhook because its owner could not act at their last check."
23004
+ },
23005
+ "oldest_queued_at": {
23006
+ "type": "string",
23007
+ "format": "date-time",
23008
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
23009
+ "description": "When the oldest of them was queued."
23010
+ },
23011
+ "expires_at": {
23012
+ "type": "string",
23013
+ "format": "date-time",
23014
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
23015
+ "description": "When the oldest is given up on if its owner still cannot act. Each is checked again periodically, so the count can trail a change of the owner's state."
23016
+ }
23017
+ },
23018
+ "required": [
23019
+ "count",
23020
+ "oldest_queued_at",
23021
+ "expires_at"
23022
+ ],
23023
+ "additionalProperties": {}
23024
+ }
23025
+ },
23026
+ "required": ["delivering", "reasons"],
23027
+ "additionalProperties": {}
23028
+ },
22275
23029
  "signed_with": {
22276
23030
  "description": "Which secret signs deliveries to this webhook: its own (read it from GET /endpoints/{id}/signing-secret), or the organization's webhook secret. Absent for an endpoint that is not a webhook.",
22277
23031
  "type": "string",
@@ -22338,7 +23092,7 @@ const operationManifest = [
22338
23092
  "binaryResponse": false,
22339
23093
  "bodyRequired": false,
22340
23094
  "command": "get-endpoint",
22341
- "description": "One endpoint, as GET /endpoints shows it, to a caller who would see it there. Any other id, a deleted endpoint's included, answers 404.",
23095
+ "description": "One endpoint, as GET /endpoints shows it, to a caller who would see it there. Any other id, a deleted endpoint's included, answers 404. For a webhook, delivery says whether it is being sent received mail at all, why not, and how much rule-granted mail is held while its owner cannot act.",
22342
23096
  "hasJsonBody": false,
22343
23097
  "method": "GET",
22344
23098
  "operationId": "getEndpoint",
@@ -22525,13 +23279,13 @@ const operationManifest = [
22525
23279
  "type": "boolean"
22526
23280
  },
22527
23281
  "owner_key_id": {
22528
- "description": "The API key that owns this webhook. In endpoint responses it is set only for a caller who may read API key metadata, and is null otherwise and when the organization owns the webhook. After a rotation, the key that carries on for the key first named while it can act.",
23282
+ "description": "The API key that owns this webhook. In endpoint responses it is set only for a caller who may read API key metadata, and is null otherwise and when the organization owns the webhook. After a rotation, the key that carries on for the key first named while it can act. When none can, the key delivery stops on: the key first named while it is suspended and not expired, otherwise the newest key of its rotation that is not revoked, otherwise the newest one (only the key first named when nothing may carry on for it).",
22529
23283
  "type": ["string", "null"],
22530
23284
  "format": "uuid",
22531
23285
  "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
22532
23286
  },
22533
23287
  "owner_key_status": {
22534
- "description": "Whether the owning key can act now. In endpoint responses it is set only for a caller who may read API key metadata. Suspended also covers an expired key. A suspended or revoked owner is sent nothing.",
23288
+ "description": "Whether the owning key can act now. In endpoint responses it is set only for a caller who may read API key metadata. It is the state delivery acts on. Suspended also covers an expired key. A suspended or revoked owner is sent nothing. Anyone who may read the endpoint learns that from delivery.reasons, which never names the key.",
22535
23289
  "type": ["string", "null"],
22536
23290
  "enum": [
22537
23291
  "active",
@@ -22540,6 +23294,65 @@ const operationManifest = [
22540
23294
  null
22541
23295
  ]
22542
23296
  },
23297
+ "disabled_reason": {
23298
+ "description": "Why the platform turned this endpoint off, when it names a reason. owner_changed: it was given to a new owner while its URL stayed, and the URL was chosen under the previous owner; turning it on or giving it a new URL starts it again. owner_credential_revoked: the third-party app or agent delegate grant that owned it was revoked; an owner or admin who turns it back on takes it over. Null while it is on and when it was turned off by hand. An access suspension does not set it (a suspension is decided through its own record). Other values may be added.",
23299
+ "type": ["string", "null"]
23300
+ },
23301
+ "delivery": {
23302
+ "description": "For a webhook (kind http) in GET /endpoints and GET /endpoints/{id}: whether it is being sent received mail, and why not. Shown to anyone who may read the endpoint; it names a state, never a key. Null when it could not be checked just now. Absent from other kinds of endpoint and from create and update answers.",
23303
+ "type": ["object", "null"],
23304
+ "properties": {
23305
+ "delivering": {
23306
+ "type": "boolean",
23307
+ "description": "Whether the webhook is being sent received mail at all now: it is on, it is not held, and its owner can receive mail. Which messages reach it is still decided per message, by routing, rule grants and its own rules."
23308
+ },
23309
+ "reasons": {
23310
+ "type": "array",
23311
+ "items": {
23312
+ "type": "object",
23313
+ "properties": {
23314
+ "code": { "type": "string" },
23315
+ "message": { "type": "string" }
23316
+ },
23317
+ "required": ["code", "message"],
23318
+ "additionalProperties": {}
23319
+ },
23320
+ "description": "Every reason it is not, in the order delivery checks them; empty when it is. Codes, of which more may be added: disabled: the endpoint is off (disabled_reason on the endpoint says why). held: the endpoint, or the route the email came through, is held pending an owner or admin decision after its creator's access was reduced. acting_owner_revoked, acting_owner_suspended (which covers an expired key) and acting_owner_left: its owner cannot act now. acting_owner_permission: its owner does not read mail. acting_owner_depth: its owner sees headers only. acting_owner_scope: its owner does not read this email's mailbox, or, with no email to judge, reads no received mail at all now. These three name what the owner's access holds, so a caller who may not read API key metadata sees acting_owner_access in their place."
23321
+ },
23322
+ "held_for_owner": {
23323
+ "description": "Rule-granted mail held for this webhook while its owner cannot act; null when none is. Only for a caller with access to every mailbox (held mail can be an earlier owner's), and absent when it could not be read just now.",
23324
+ "type": ["object", "null"],
23325
+ "properties": {
23326
+ "count": {
23327
+ "type": "integer",
23328
+ "minimum": -9007199254740991,
23329
+ "maximum": 9007199254740991,
23330
+ "description": "Rule-granted emails waiting for this webhook because its owner could not act at their last check."
23331
+ },
23332
+ "oldest_queued_at": {
23333
+ "type": "string",
23334
+ "format": "date-time",
23335
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
23336
+ "description": "When the oldest of them was queued."
23337
+ },
23338
+ "expires_at": {
23339
+ "type": "string",
23340
+ "format": "date-time",
23341
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
23342
+ "description": "When the oldest is given up on if its owner still cannot act. Each is checked again periodically, so the count can trail a change of the owner's state."
23343
+ }
23344
+ },
23345
+ "required": [
23346
+ "count",
23347
+ "oldest_queued_at",
23348
+ "expires_at"
23349
+ ],
23350
+ "additionalProperties": {}
23351
+ }
23352
+ },
23353
+ "required": ["delivering", "reasons"],
23354
+ "additionalProperties": {}
23355
+ },
22543
23356
  "signed_with": {
22544
23357
  "description": "Which secret signs deliveries to this webhook: its own (read it from GET /endpoints/{id}/signing-secret), or the organization's webhook secret. Absent for an endpoint that is not a webhook.",
22545
23358
  "type": "string",
@@ -22786,13 +23599,13 @@ const operationManifest = [
22786
23599
  "type": "boolean"
22787
23600
  },
22788
23601
  "owner_key_id": {
22789
- "description": "The API key that owns this webhook. In endpoint responses it is set only for a caller who may read API key metadata, and is null otherwise and when the organization owns the webhook. After a rotation, the key that carries on for the key first named while it can act.",
23602
+ "description": "The API key that owns this webhook. In endpoint responses it is set only for a caller who may read API key metadata, and is null otherwise and when the organization owns the webhook. After a rotation, the key that carries on for the key first named while it can act. When none can, the key delivery stops on: the key first named while it is suspended and not expired, otherwise the newest key of its rotation that is not revoked, otherwise the newest one (only the key first named when nothing may carry on for it).",
22790
23603
  "type": ["string", "null"],
22791
23604
  "format": "uuid",
22792
23605
  "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
22793
23606
  },
22794
23607
  "owner_key_status": {
22795
- "description": "Whether the owning key can act now. In endpoint responses it is set only for a caller who may read API key metadata. Suspended also covers an expired key. A suspended or revoked owner is sent nothing.",
23608
+ "description": "Whether the owning key can act now. In endpoint responses it is set only for a caller who may read API key metadata. It is the state delivery acts on. Suspended also covers an expired key. A suspended or revoked owner is sent nothing. Anyone who may read the endpoint learns that from delivery.reasons, which never names the key.",
22796
23609
  "type": ["string", "null"],
22797
23610
  "enum": [
22798
23611
  "active",
@@ -22801,6 +23614,65 @@ const operationManifest = [
22801
23614
  null
22802
23615
  ]
22803
23616
  },
23617
+ "disabled_reason": {
23618
+ "description": "Why the platform turned this endpoint off, when it names a reason. owner_changed: it was given to a new owner while its URL stayed, and the URL was chosen under the previous owner; turning it on or giving it a new URL starts it again. owner_credential_revoked: the third-party app or agent delegate grant that owned it was revoked; an owner or admin who turns it back on takes it over. Null while it is on and when it was turned off by hand. An access suspension does not set it (a suspension is decided through its own record). Other values may be added.",
23619
+ "type": ["string", "null"]
23620
+ },
23621
+ "delivery": {
23622
+ "description": "For a webhook (kind http) in GET /endpoints and GET /endpoints/{id}: whether it is being sent received mail, and why not. Shown to anyone who may read the endpoint; it names a state, never a key. Null when it could not be checked just now. Absent from other kinds of endpoint and from create and update answers.",
23623
+ "type": ["object", "null"],
23624
+ "properties": {
23625
+ "delivering": {
23626
+ "type": "boolean",
23627
+ "description": "Whether the webhook is being sent received mail at all now: it is on, it is not held, and its owner can receive mail. Which messages reach it is still decided per message, by routing, rule grants and its own rules."
23628
+ },
23629
+ "reasons": {
23630
+ "type": "array",
23631
+ "items": {
23632
+ "type": "object",
23633
+ "properties": {
23634
+ "code": { "type": "string" },
23635
+ "message": { "type": "string" }
23636
+ },
23637
+ "required": ["code", "message"],
23638
+ "additionalProperties": {}
23639
+ },
23640
+ "description": "Every reason it is not, in the order delivery checks them; empty when it is. Codes, of which more may be added: disabled: the endpoint is off (disabled_reason on the endpoint says why). held: the endpoint, or the route the email came through, is held pending an owner or admin decision after its creator's access was reduced. acting_owner_revoked, acting_owner_suspended (which covers an expired key) and acting_owner_left: its owner cannot act now. acting_owner_permission: its owner does not read mail. acting_owner_depth: its owner sees headers only. acting_owner_scope: its owner does not read this email's mailbox, or, with no email to judge, reads no received mail at all now. These three name what the owner's access holds, so a caller who may not read API key metadata sees acting_owner_access in their place."
23641
+ },
23642
+ "held_for_owner": {
23643
+ "description": "Rule-granted mail held for this webhook while its owner cannot act; null when none is. Only for a caller with access to every mailbox (held mail can be an earlier owner's), and absent when it could not be read just now.",
23644
+ "type": ["object", "null"],
23645
+ "properties": {
23646
+ "count": {
23647
+ "type": "integer",
23648
+ "minimum": -9007199254740991,
23649
+ "maximum": 9007199254740991,
23650
+ "description": "Rule-granted emails waiting for this webhook because its owner could not act at their last check."
23651
+ },
23652
+ "oldest_queued_at": {
23653
+ "type": "string",
23654
+ "format": "date-time",
23655
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
23656
+ "description": "When the oldest of them was queued."
23657
+ },
23658
+ "expires_at": {
23659
+ "type": "string",
23660
+ "format": "date-time",
23661
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
23662
+ "description": "When the oldest is given up on if its owner still cannot act. Each is checked again periodically, so the count can trail a change of the owner's state."
23663
+ }
23664
+ },
23665
+ "required": [
23666
+ "count",
23667
+ "oldest_queued_at",
23668
+ "expires_at"
23669
+ ],
23670
+ "additionalProperties": {}
23671
+ }
23672
+ },
23673
+ "required": ["delivering", "reasons"],
23674
+ "additionalProperties": {}
23675
+ },
22804
23676
  "signed_with": {
22805
23677
  "description": "Which secret signs deliveries to this webhook: its own (read it from GET /endpoints/{id}/signing-secret), or the organization's webhook secret. Absent for an endpoint that is not a webhook.",
22806
23678
  "type": "string",
@@ -22994,7 +23866,7 @@ const operationManifest = [
22994
23866
  "binaryResponse": false,
22995
23867
  "bodyRequired": false,
22996
23868
  "command": "test-endpoint",
22997
- "description": "Sends a sample `email.received` event to the endpoint. The request\nincludes SSRF protection (private IP rejection and DNS pinning).\nRate limited to 4 per minute and 30 per hour (non-exempt).\nSuccessful deliveries and verified-domain endpoints are exempt\nfrom the rate limit.\n",
23869
+ "description": "Sends a sample `email.received` event to the endpoint. The request\nincludes SSRF protection (private IP rejection and DNS pinning).\nRate limited to 4 per minute and 30 per hour (non-exempt).\nSuccessful deliveries and verified-domain endpoints are exempt\nfrom the rate limit.\n\n\nThe test is sent whether or not real mail would be. reachable says whether the endpoint answered it with a 2xx; delivery says, apart from that, whether real email.received events would be sent to the endpoint by its own conditions (on, not held, an owner that can receive mail, subscribed to the event), with every reason it would not be. A 200 from the test alone does not mean mail arrives.",
22998
23870
  "hasJsonBody": false,
22999
23871
  "method": "POST",
23000
23872
  "operationId": "testEndpoint",
@@ -23031,9 +23903,58 @@ const operationManifest = [
23031
23903
  "signature": {
23032
23904
  "type": "string",
23033
23905
  "description": "The signature header value sent, answered to a caller who may see it: for a webhook signed with the organization's secret, a caller who may read that secret; for one signed with its own, the person who owns it, calling as themselves."
23906
+ },
23907
+ "reachable": {
23908
+ "type": "boolean",
23909
+ "description": "Whether the endpoint answered the test with a 2xx status: the request reached it and its handler accepted it. It says nothing about whether real mail is sent to it: see delivery."
23910
+ },
23911
+ "delivery": {
23912
+ "description": "Whether real email.received events would be sent to this endpoint, judged by the endpoint's own conditions as delivery judges them. Null when that could not be checked just now.",
23913
+ "type": ["object", "null"],
23914
+ "properties": {
23915
+ "event_type": {
23916
+ "type": "string",
23917
+ "description": "The event type checked."
23918
+ },
23919
+ "would_deliver": {
23920
+ "type": "boolean",
23921
+ "description": "Whether this endpoint's own conditions let the event through: it is on, it is not held, its owner can receive it, and it subscribes to the event type. Which endpoint routing or a rule grant sends a message to is decided separately, per message: POST /routes/simulate answers that, and a rule grant can deliver an email outside its owner's mailboxes. A Function endpoint is also judged at delivery on its Function's run state and key."
23922
+ },
23923
+ "reasons": {
23924
+ "type": "array",
23925
+ "items": {
23926
+ "type": "object",
23927
+ "properties": {
23928
+ "code": { "type": "string" },
23929
+ "message": { "type": "string" }
23930
+ },
23931
+ "required": ["code", "message"],
23932
+ "additionalProperties": {}
23933
+ },
23934
+ "description": "Every reason it would not be sent the event, in the order delivery checks them; empty when it would. Codes, of which more may be added: disabled: the endpoint is off (disabled_reason on the endpoint says why). held: the endpoint, or the route the email came through, is held pending an owner or admin decision after its creator's access was reduced. acting_owner_revoked, acting_owner_suspended (which covers an expired key) and acting_owner_left: its owner cannot act now. acting_owner_permission: its owner does not read mail. acting_owner_depth: its owner sees headers only. acting_owner_scope: its owner does not read this email's mailbox, or, with no email to judge, reads no received mail at all now. These three name what the owner's access holds, so a caller who may not read API key metadata sees acting_owner_access in their place. event_type_not_subscribed: rules.event_types leaves the event out."
23935
+ },
23936
+ "events_paused_until": {
23937
+ "description": "While repeated failures have paused this webhook (its circuit breaker is open): when webhook events other than mail (interaction, sent mail and credential events) are next attempted. Received mail is attempted regardless, so this does not change would_deliver. Null when nothing is paused, and for a Function endpoint.",
23938
+ "type": ["string", "null"],
23939
+ "format": "date-time",
23940
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
23941
+ }
23942
+ },
23943
+ "required": [
23944
+ "event_type",
23945
+ "would_deliver",
23946
+ "reasons",
23947
+ "events_paused_until"
23948
+ ],
23949
+ "additionalProperties": {}
23034
23950
  }
23035
23951
  },
23036
- "required": ["status", "body"]
23952
+ "required": [
23953
+ "status",
23954
+ "body",
23955
+ "reachable",
23956
+ "delivery"
23957
+ ]
23037
23958
  },
23038
23959
  "sdkName": "testEndpoint",
23039
23960
  "summary": "Send a test webhook",
@@ -23044,7 +23965,7 @@ const operationManifest = [
23044
23965
  "binaryResponse": false,
23045
23966
  "bodyRequired": true,
23046
23967
  "command": "test-endpoint-rules",
23047
- "description": "Evaluates the endpoint's filtering rules against an\nalready-received email WITHOUT delivering anything. The same\nshared matcher the live delivery paths use produces the\nverdict, so the response explains exactly why a webhook fired\nor was suppressed for that message.\n\nWhen delivery would be suppressed, `rule` names the failing\nrule and `reason` carries a human-readable explanation; both\nare null when the message matches. `evaluated` echoes the\nmessage metadata the matcher compared (size, attachments, and\nthe authenticated From identity versus the raw envelope\nsender), so a surprising verdict can be traced to its inputs.\n\nTwo independent gates are surfaced separately:\n`subscribed_to_event` reports the endpoint's event-type\nsubscription (checked before message matching), and\n`rules_valid` reports whether the stored rules blob parsed at\nall. Delivery fails OPEN on an invalid blob (the message is\ndelivered as if unfiltered), so `rules_valid: false` exposes a\nmisconfiguration that is otherwise silent.\n",
23968
+ "description": "Evaluates the endpoint's filtering rules against an\nalready-received email WITHOUT delivering anything. The same\nshared matcher the live delivery paths use produces the\nverdict, so the response explains exactly why a webhook fired\nor was suppressed for that message.\n\nWhen delivery would be suppressed, `rule` names the failing\nrule and `reason` carries a human-readable explanation; both\nare null when the message matches. `evaluated` echoes the\nmessage metadata the matcher compared (size, attachments, and\nthe authenticated From identity versus the raw envelope\nsender), so a surprising verdict can be traced to its inputs.\n\nTwo independent gates are surfaced separately:\n`subscribed_to_event` reports the endpoint's event-type\nsubscription (checked before message matching), and\n`rules_valid` reports whether the stored rules blob parsed at\nall. Delivery fails OPEN on an invalid blob (the message is\ndelivered as if unfiltered), so `rules_valid: false` exposes a\nmisconfiguration that is otherwise silent.\n\n\nwould_deliver also covers the endpoint's own delivery conditions for this email (delivery): it is on, neither it nor the email's route is held, its owner may receive this email, and it subscribes to the event. Which endpoint routing sends the email to is decided separately: POST /routes/simulate answers that. A 503 means those conditions could not be read just now; retry.",
23048
23969
  "hasJsonBody": true,
23049
23970
  "method": "POST",
23050
23971
  "operationId": "testEndpointRules",
@@ -23082,15 +24003,15 @@ const operationManifest = [
23082
24003
  "properties": {
23083
24004
  "would_deliver": {
23084
24005
  "type": "boolean",
23085
- "description": "Whether the delivery path would deliver this email to the endpoint."
24006
+ "description": "Whether this email would be sent to this endpoint by the endpoint's own conditions: it is on, neither it nor the email's route is held, its owner may receive this email, it subscribes to the event, and its rules match. Which endpoint routing sends the email to is decided separately: POST /routes/simulate answers that. A rule grant can still deliver an email whose mailbox the owner does not read, and a Function endpoint is also judged at delivery on its Function's run state and key."
23086
24007
  },
23087
24008
  "rule": {
23088
- "type": ["string", "null"],
23089
- "description": "Name of the failing rule when delivery would be\nsuppressed; null when the message matches and the\nendpoint is subscribed to the event type.\n"
24009
+ "description": "The first check that stops this email, in the order delivery makes them: disabled, held or an acting_owner_* code (see delivery.reasons), then event_types when the endpoint does not subscribe, then the name of the rule that does not match. Null when it would deliver.",
24010
+ "type": ["string", "null"]
23090
24011
  },
23091
24012
  "reason": {
23092
- "type": ["string", "null"],
23093
- "description": "Human-readable explanation of the failing rule; null when the message matches."
24013
+ "description": "Why, in words. Null when it would deliver.",
24014
+ "type": ["string", "null"]
23094
24015
  },
23095
24016
  "rules_valid": {
23096
24017
  "type": "boolean",
@@ -23137,6 +24058,46 @@ const operationManifest = [
23137
24058
  "sender_authenticated",
23138
24059
  "sender_trust_basis"
23139
24060
  ]
24061
+ },
24062
+ "delivery": {
24063
+ "description": "The endpoint's own conditions for this email, as delivery judges them, the owner judged on this email's mailbox and time. Null for a pull destination, which nothing pushes to.",
24064
+ "type": ["object", "null"],
24065
+ "properties": {
24066
+ "event_type": {
24067
+ "type": "string",
24068
+ "description": "The event type checked."
24069
+ },
24070
+ "would_deliver": {
24071
+ "type": "boolean",
24072
+ "description": "Whether this endpoint's own conditions let the event through: it is on, it is not held, its owner can receive it, and it subscribes to the event type. Which endpoint routing or a rule grant sends a message to is decided separately, per message: POST /routes/simulate answers that, and a rule grant can deliver an email outside its owner's mailboxes. A Function endpoint is also judged at delivery on its Function's run state and key."
24073
+ },
24074
+ "reasons": {
24075
+ "type": "array",
24076
+ "items": {
24077
+ "type": "object",
24078
+ "properties": {
24079
+ "code": { "type": "string" },
24080
+ "message": { "type": "string" }
24081
+ },
24082
+ "required": ["code", "message"],
24083
+ "additionalProperties": {}
24084
+ },
24085
+ "description": "Every reason it would not be sent the event, in the order delivery checks them; empty when it would. Codes, of which more may be added: disabled: the endpoint is off (disabled_reason on the endpoint says why). held: the endpoint, or the route the email came through, is held pending an owner or admin decision after its creator's access was reduced. acting_owner_revoked, acting_owner_suspended (which covers an expired key) and acting_owner_left: its owner cannot act now. acting_owner_permission: its owner does not read mail. acting_owner_depth: its owner sees headers only. acting_owner_scope: its owner does not read this email's mailbox, or, with no email to judge, reads no received mail at all now. These three name what the owner's access holds, so a caller who may not read API key metadata sees acting_owner_access in their place. event_type_not_subscribed: rules.event_types leaves the event out."
24086
+ },
24087
+ "events_paused_until": {
24088
+ "description": "While repeated failures have paused this webhook (its circuit breaker is open): when webhook events other than mail (interaction, sent mail and credential events) are next attempted. Received mail is attempted regardless, so this does not change would_deliver. Null when nothing is paused, and for a Function endpoint.",
24089
+ "type": ["string", "null"],
24090
+ "format": "date-time",
24091
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
24092
+ }
24093
+ },
24094
+ "required": [
24095
+ "event_type",
24096
+ "would_deliver",
24097
+ "reasons",
24098
+ "events_paused_until"
24099
+ ],
24100
+ "additionalProperties": {}
23140
24101
  }
23141
24102
  },
23142
24103
  "required": [
@@ -23146,7 +24107,8 @@ const operationManifest = [
23146
24107
  "rules_valid",
23147
24108
  "subscribed_to_event",
23148
24109
  "event_type",
23149
- "evaluated"
24110
+ "evaluated",
24111
+ "delivery"
23150
24112
  ]
23151
24113
  },
23152
24114
  "sdkName": "testEndpointRules",
@@ -23158,7 +24120,7 @@ const operationManifest = [
23158
24120
  "binaryResponse": false,
23159
24121
  "bodyRequired": true,
23160
24122
  "command": "update-endpoint",
23161
- "description": "Updates an active webhook endpoint. Changing the URL keeps the endpoint: its id, recipient routes, signing secret, delivery history and any deliveries still owed to it stay with it, and owed deliveries go to the new URL. Another active endpoint at the new URL is refused.\n\n\nPull destinations cannot be assigned a URL, function or recipient-routing domain. Existing endpoint deletion disconnects the destination; creating a new destination does not backfill historical events.",
24123
+ "description": "Updates an active webhook endpoint. Changing the URL keeps the endpoint: its id, recipient routes, signing secret, delivery history and any deliveries still owed to it stay with it, and owed deliveries go to the new URL. A URL can have one endpoint per domain, route targets included (unlike the one default destination per domain), and endpoints with no domain_id count as one domain. A disabled endpoint still holds its URL; a deleted one does not. A change onto a URL another endpoint has on the same domain is refused (400), and a move onto a domain where another endpoint has this URL is refused (409).\n\n\nPull destinations cannot be assigned a URL, function or recipient-routing domain. Existing endpoint deletion disconnects the destination; creating a new destination does not backfill historical events.\n\nGiving a webhook the organization does not own to a different key (owner_key_id), or a key's webhook back to the organization (owner_key_id null), turns it off unless the request also sets enabled: its URL was chosen under its previous owner. It then reads enabled false with disabled_reason owner_changed, and turning it on or giving it a new URL starts it again. A key that carries on for the owning key through rotation (its successor, at any depth up to 16 rotations) is not a different key: the webhook stays as it was, with the same signing secret. That does not hold when the owning key was made from a key a transfer moved or rotated away, or from one that has since been deleted: no successor carries on for it. The answer's warnings name each such change (turned_off_owner_changed, turned_on_new_url), so a client can say so.",
23162
24124
  "hasJsonBody": true,
23163
24125
  "method": "PATCH",
23164
24126
  "operationId": "updateEndpoint",
@@ -23254,7 +24216,7 @@ const operationManifest = [
23254
24216
  "description": "For a move to another domain_id whose slot already has an active endpoint: false (the default) answers 409 conflict with details.holder naming it; true retires that endpoint in the same request, after the checks deleting it would need."
23255
24217
  },
23256
24218
  "owner_key_id": {
23257
- "description": "Give this webhook to an API key of the organization. Only a route target can be given to a key; a default endpoint stays the organization's, and takeover is refused with it. null gives a key-owned webhook back to the organization (a caller with access to every mailbox only); it stays a route target.",
24219
+ "description": "Give this webhook to an API key of the organization. Only a route target can be given to a key; a default endpoint stays the organization's, and takeover is refused with it. null gives a key-owned webhook back to the organization (a caller with access to every mailbox only); it stays a route target. Giving a webhook the organization does not own to a different key, or a key's webhook back to the organization, turns it off (enabled false, disabled_reason owner_changed) unless the same request sets enabled: its URL was chosen under its previous owner. A key that carries on for the key that owns it through rotation (its successor, at any depth up to 16 rotations) is not a different key: the webhook stays as it was, with the same signing secret. That does not hold for a webhook whose owning key was made from a key a transfer moved or rotated away, or from one that has since been deleted: no successor carries on for it, so giving the webhook to one is a change of owner. Turning it on, or a new URL, starts it again. A webhook that signs with its own secret keeps signing with one: the same one when it stays with the same key, or was the organization's (giving away one the organization owns needs webhook_secret:read, since the key and its holder can then read its secret), and a new one for any other owner's. The response's warnings say when an update turned the webhook off or on.",
23258
24220
  "type": ["string", "null"],
23259
24221
  "format": "uuid",
23260
24222
  "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
@@ -23267,7 +24229,7 @@ const operationManifest = [
23267
24229
  },
23268
24230
  "minProperties": 1
23269
24231
  },
23270
- "responseSchema": {
24232
+ "responseSchema": { "allOf": [{
23271
24233
  "type": "object",
23272
24234
  "properties": {
23273
24235
  "recipient": {
@@ -23437,13 +24399,13 @@ const operationManifest = [
23437
24399
  "type": "boolean"
23438
24400
  },
23439
24401
  "owner_key_id": {
23440
- "description": "The API key that owns this webhook. In endpoint responses it is set only for a caller who may read API key metadata, and is null otherwise and when the organization owns the webhook. After a rotation, the key that carries on for the key first named while it can act.",
24402
+ "description": "The API key that owns this webhook. In endpoint responses it is set only for a caller who may read API key metadata, and is null otherwise and when the organization owns the webhook. After a rotation, the key that carries on for the key first named while it can act. When none can, the key delivery stops on: the key first named while it is suspended and not expired, otherwise the newest key of its rotation that is not revoked, otherwise the newest one (only the key first named when nothing may carry on for it).",
23441
24403
  "type": ["string", "null"],
23442
24404
  "format": "uuid",
23443
24405
  "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
23444
24406
  },
23445
24407
  "owner_key_status": {
23446
- "description": "Whether the owning key can act now. In endpoint responses it is set only for a caller who may read API key metadata. Suspended also covers an expired key. A suspended or revoked owner is sent nothing.",
24408
+ "description": "Whether the owning key can act now. In endpoint responses it is set only for a caller who may read API key metadata. It is the state delivery acts on. Suspended also covers an expired key. A suspended or revoked owner is sent nothing. Anyone who may read the endpoint learns that from delivery.reasons, which never names the key.",
23447
24409
  "type": ["string", "null"],
23448
24410
  "enum": [
23449
24411
  "active",
@@ -23452,6 +24414,65 @@ const operationManifest = [
23452
24414
  null
23453
24415
  ]
23454
24416
  },
24417
+ "disabled_reason": {
24418
+ "description": "Why the platform turned this endpoint off, when it names a reason. owner_changed: it was given to a new owner while its URL stayed, and the URL was chosen under the previous owner; turning it on or giving it a new URL starts it again. owner_credential_revoked: the third-party app or agent delegate grant that owned it was revoked; an owner or admin who turns it back on takes it over. Null while it is on and when it was turned off by hand. An access suspension does not set it (a suspension is decided through its own record). Other values may be added.",
24419
+ "type": ["string", "null"]
24420
+ },
24421
+ "delivery": {
24422
+ "description": "For a webhook (kind http) in GET /endpoints and GET /endpoints/{id}: whether it is being sent received mail, and why not. Shown to anyone who may read the endpoint; it names a state, never a key. Null when it could not be checked just now. Absent from other kinds of endpoint and from create and update answers.",
24423
+ "type": ["object", "null"],
24424
+ "properties": {
24425
+ "delivering": {
24426
+ "type": "boolean",
24427
+ "description": "Whether the webhook is being sent received mail at all now: it is on, it is not held, and its owner can receive mail. Which messages reach it is still decided per message, by routing, rule grants and its own rules."
24428
+ },
24429
+ "reasons": {
24430
+ "type": "array",
24431
+ "items": {
24432
+ "type": "object",
24433
+ "properties": {
24434
+ "code": { "type": "string" },
24435
+ "message": { "type": "string" }
24436
+ },
24437
+ "required": ["code", "message"],
24438
+ "additionalProperties": {}
24439
+ },
24440
+ "description": "Every reason it is not, in the order delivery checks them; empty when it is. Codes, of which more may be added: disabled: the endpoint is off (disabled_reason on the endpoint says why). held: the endpoint, or the route the email came through, is held pending an owner or admin decision after its creator's access was reduced. acting_owner_revoked, acting_owner_suspended (which covers an expired key) and acting_owner_left: its owner cannot act now. acting_owner_permission: its owner does not read mail. acting_owner_depth: its owner sees headers only. acting_owner_scope: its owner does not read this email's mailbox, or, with no email to judge, reads no received mail at all now. These three name what the owner's access holds, so a caller who may not read API key metadata sees acting_owner_access in their place."
24441
+ },
24442
+ "held_for_owner": {
24443
+ "description": "Rule-granted mail held for this webhook while its owner cannot act; null when none is. Only for a caller with access to every mailbox (held mail can be an earlier owner's), and absent when it could not be read just now.",
24444
+ "type": ["object", "null"],
24445
+ "properties": {
24446
+ "count": {
24447
+ "type": "integer",
24448
+ "minimum": -9007199254740991,
24449
+ "maximum": 9007199254740991,
24450
+ "description": "Rule-granted emails waiting for this webhook because its owner could not act at their last check."
24451
+ },
24452
+ "oldest_queued_at": {
24453
+ "type": "string",
24454
+ "format": "date-time",
24455
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
24456
+ "description": "When the oldest of them was queued."
24457
+ },
24458
+ "expires_at": {
24459
+ "type": "string",
24460
+ "format": "date-time",
24461
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
24462
+ "description": "When the oldest is given up on if its owner still cannot act. Each is checked again periodically, so the count can trail a change of the owner's state."
24463
+ }
24464
+ },
24465
+ "required": [
24466
+ "count",
24467
+ "oldest_queued_at",
24468
+ "expires_at"
24469
+ ],
24470
+ "additionalProperties": {}
24471
+ }
24472
+ },
24473
+ "required": ["delivering", "reasons"],
24474
+ "additionalProperties": {}
24475
+ },
23455
24476
  "signed_with": {
23456
24477
  "description": "Which secret signs deliveries to this webhook: its own (read it from GET /endpoints/{id}/signing-secret), or the organization's webhook secret. Absent for an endpoint that is not a webhook.",
23457
24478
  "type": "string",
@@ -23474,7 +24495,24 @@ const operationManifest = [
23474
24495
  "failure_count",
23475
24496
  "consecutive_fails"
23476
24497
  ]
23477
- },
24498
+ }, {
24499
+ "type": "object",
24500
+ "required": ["warnings"],
24501
+ "properties": { "warnings": {
24502
+ "default": [],
24503
+ "description": "When this update turned the webhook off or on without the request setting enabled. turned_off_owner_changed: the webhook was given to a new owner, so it is now off (disabled_reason owner_changed) until it is turned on or given a new URL. turned_on_new_url: it was off because its owner changed, and the new URL turned it back on. Empty when the update did neither. Other codes may be added.",
24504
+ "type": "array",
24505
+ "items": {
24506
+ "type": "object",
24507
+ "properties": {
24508
+ "code": { "type": "string" },
24509
+ "message": { "type": "string" }
24510
+ },
24511
+ "required": ["code", "message"],
24512
+ "additionalProperties": {}
24513
+ }
24514
+ } }
24515
+ }] },
23478
24516
  "sdkName": "updateEndpoint",
23479
24517
  "summary": "Update a webhook endpoint",
23480
24518
  "tag": "Endpoints",
@@ -45670,8 +46708,12 @@ const operationManifest = [
45670
46708
  "type": "boolean",
45671
46709
  "description": "Whether this is the latest attempt of its delivery: the one recorded last, where a record of no attempt (a rule skip or a held send, attempt_count 0) is latest only when no attempt was made. A retry is recorded as a new attempt with its own id and attempt_count (not always higher than the one before), so an earlier attempt never changes: follow the latest one, or read attempts on any of them."
45672
46710
  },
46711
+ "retry_scheduled": {
46712
+ "description": "Whether an automatic retry will try this delivery again. A retry is a delivery pass for the email: it sends again to the endpoint the email's routing resolves then (its recorded route, or the current endpoint for its domain), unless the email already reached it, deciding it afresh (its rules, its owner's access, whether it is still on), so true promises a pass, not a delivery to this endpoint. A rule-granted delivery is retried on a schedule of its own, to the same endpoint, deciding it afresh: on its latest attempt true while another attempt is to come (the delivery's next_attempt_at says when); null while an attempt is under way (read it again shortly), while it waits on an owner who cannot receive mail or on a held endpoint (checked again hourly, and sent once that clears), when it is past its 7-day limit, or when that could not be read just now; and false otherwise, including once the endpoint is off or already has the email. Null on every attempt of a delivery retried or settled on a schedule of its own that this does not report: a held one (its release settles it, including the attempts it makes itself), a pull attempt, and a sent email's or other event's delivery. On the latest attempt that failed, is pending or was skipped in a way still owed, of an email that was not rejected and still has its content, to an endpoint that is still on, takes the event, has an owner the pass would deliver to (a revoked or suspended key, or an owner that may not receive the message, reads false) and has not received the email by another delivery: null while a delivery pass for the email is running (whether another follows is decided when it ends), true when the email has another automatic pass to come (a scheduled retry, the reclaim of a pass that stopped, or the recovery of a first pass that did not finish), and false otherwise. False on every other attempt, including every attempt a later one followed: no automatic retry of the email's delivery will try it again, which does not mean nothing else ever will.",
46713
+ "type": ["boolean", "null"]
46714
+ },
45673
46715
  "next_attempt_at": {
45674
- "description": "When the next automatic retry of this delivery is due. Not reported yet: always null.",
46716
+ "description": "When the next automatic attempt of a rule-granted delivery is due: set on its latest attempt while retry_scheduled is true. The attempt runs at the first retry pass at or after this time (one runs every few minutes), so a time already past means it is due now. Null otherwise, and on every delivery that is not rule-granted, whose retries are passes for the email with no time reported (whether one is coming is retry_scheduled).",
45675
46717
  "type": ["string", "null"]
45676
46718
  },
45677
46719
  "attempts": {
@@ -45699,6 +46741,10 @@ const operationManifest = [
45699
46741
  "response_body": {
45700
46742
  "description": "The first 2 KB of the body the endpoint answered this attempt with, so a handler that answers 200 and ignores a delivery can be seen doing so. It is whatever came back, which may echo what was sent. Null when no answer came back, and cleared when the message's content is discarded or the sent email is deleted. Withheld from a caller who cannot read message content, since an answer can echo what was sent.",
45701
46743
  "type": ["string", "null"]
46744
+ },
46745
+ "retry_scheduled": {
46746
+ "description": "Whether an automatic retry will try this delivery again. A retry is a delivery pass for the email: it sends again to the endpoint the email's routing resolves then (its recorded route, or the current endpoint for its domain), unless the email already reached it, deciding it afresh (its rules, its owner's access, whether it is still on), so true promises a pass, not a delivery to this endpoint. A rule-granted delivery is retried on a schedule of its own, to the same endpoint, deciding it afresh: on its latest attempt true while another attempt is to come (the delivery's next_attempt_at says when); null while an attempt is under way (read it again shortly), while it waits on an owner who cannot receive mail or on a held endpoint (checked again hourly, and sent once that clears), when it is past its 7-day limit, or when that could not be read just now; and false otherwise, including once the endpoint is off or already has the email. Null on every attempt of a delivery retried or settled on a schedule of its own that this does not report: a held one (its release settles it, including the attempts it makes itself), a pull attempt, and a sent email's or other event's delivery. On the latest attempt that failed, is pending or was skipped in a way still owed, of an email that was not rejected and still has its content, to an endpoint that is still on, takes the event, has an owner the pass would deliver to (a revoked or suspended key, or an owner that may not receive the message, reads false) and has not received the email by another delivery: null while a delivery pass for the email is running (whether another follows is decided when it ends), true when the email has another automatic pass to come (a scheduled retry, the reclaim of a pass that stopped, or the recovery of a first pass that did not finish), and false otherwise. False on every other attempt, including every attempt a later one followed: no automatic retry of the email's delivery will try it again, which does not mean nothing else ever will.",
46747
+ "type": ["boolean", "null"]
45702
46748
  }
45703
46749
  },
45704
46750
  "required": [
@@ -45709,7 +46755,8 @@ const operationManifest = [
45709
46755
  "duration_ms",
45710
46756
  "created_at",
45711
46757
  "response_status",
45712
- "response_body"
46758
+ "response_body",
46759
+ "retry_scheduled"
45713
46760
  ]
45714
46761
  },
45715
46762
  "description": "Every attempt of this delivery in the order recorded, oldest first, this one included: the same endpoint, and the same event (or, for an attempt recorded without one, the same email). Each carries what the endpoint answered that attempt (response_status, response_body). Records of no attempt (attempt_count 0) are listed where they were recorded, so the last entry is not always the latest attempt (latest_attempt says which is). The newest 100 when there are more."
@@ -45734,6 +46781,7 @@ const operationManifest = [
45734
46781
  "updated_at",
45735
46782
  "email",
45736
46783
  "latest_attempt",
46784
+ "retry_scheduled",
45737
46785
  "next_attempt_at",
45738
46786
  "attempts"
45739
46787
  ]
@@ -45907,10 +46955,14 @@ const operationManifest = [
45907
46955
  "type": "boolean",
45908
46956
  "description": "Whether this is the latest attempt of its delivery: the one recorded last, where a record of no attempt (a rule skip or a held send, attempt_count 0) is latest only when no attempt was made. A retry is recorded as a new attempt with its own id and attempt_count (not always higher than the one before), so an earlier attempt never changes: follow the latest one, or read attempts on any of them."
45909
46957
  },
46958
+ "retry_scheduled": {
46959
+ "type": ["boolean", "null"],
46960
+ "description": "Whether an automatic retry will try this delivery again. A retry is a delivery pass for the email: it sends again to the endpoint the email's routing resolves then (its recorded route, or the current endpoint for its domain), unless the email already reached it, deciding it afresh (its rules, its owner's access, whether it is still on), so true promises a pass, not a delivery to this endpoint. A rule-granted delivery is retried on a schedule of its own, to the same endpoint, deciding it afresh: on its latest attempt true while another attempt is to come (the delivery's next_attempt_at says when); null while an attempt is under way (read it again shortly), while it waits on an owner who cannot receive mail or on a held endpoint (checked again hourly, and sent once that clears), when it is past its 7-day limit, or when that could not be read just now; and false otherwise, including once the endpoint is off or already has the email. Null on every attempt of a delivery retried or settled on a schedule of its own that this does not report: a held one (its release settles it, including the attempts it makes itself), a pull attempt, and a sent email's or other event's delivery. On the latest attempt that failed, is pending or was skipped in a way still owed, of an email that was not rejected and still has its content, to an endpoint that is still on, takes the event, has an owner the pass would deliver to (a revoked or suspended key, or an owner that may not receive the message, reads false) and has not received the email by another delivery: null while a delivery pass for the email is running (whether another follows is decided when it ends), true when the email has another automatic pass to come (a scheduled retry, the reclaim of a pass that stopped, or the recovery of a first pass that did not finish), and false otherwise. False on every other attempt, including every attempt a later one followed: no automatic retry of the email's delivery will try it again, which does not mean nothing else ever will."
46961
+ },
45910
46962
  "next_attempt_at": {
45911
46963
  "type": ["string", "null"],
45912
46964
  "format": "date-time",
45913
- "description": "When the next automatic retry of this delivery is due. Not reported yet: always null."
46965
+ "description": "When the next automatic attempt of a rule-granted delivery is due: set on its latest attempt while retry_scheduled is true. The attempt runs at the first retry pass at or after this time (one runs every few minutes), so a time already past means it is due now. Null otherwise, and on every delivery that is not rule-granted, whose retries are passes for the email with no time reported (whether one is coming is retry_scheduled)."
45914
46966
  }
45915
46967
  },
45916
46968
  "required": [
@@ -45929,6 +46981,7 @@ const operationManifest = [
45929
46981
  "response_status",
45930
46982
  "response_body",
45931
46983
  "latest_attempt",
46984
+ "retry_scheduled",
45932
46985
  "next_attempt_at"
45933
46986
  ]
45934
46987
  }
@@ -45985,6 +47038,16 @@ const operationManifest = [
45985
47038
  "tagCommand": "webhook-deliveries"
45986
47039
  }
45987
47040
  ];
47041
+ // ../packages/api-core/src/openapi/permissions.generated.ts
47042
+ /**
47043
+ * The keys of the built-in roles, from the spec's `BuiltinRoleKey`.
47044
+ * Template and custom roles use other keys.
47045
+ */
47046
+ const BUILTIN_ROLE_KEYS = [
47047
+ "owner",
47048
+ "admin",
47049
+ "member"
47050
+ ];
45988
47051
  // src/oclif/api-path-commands.ts
45989
47052
  /**
45990
47053
  * The hand-rolled command that covers an operation, where the CLI offers one
@@ -46205,6 +47268,27 @@ const TOKEN_SKEW_MS = 6e4;
46205
47268
  const MAX_CACHED_ORG_TOKENS = 20;
46206
47269
  const USER_LOGIN_EXPIRED_MESSAGE = "Saved Primitive CLI login expired or was revoked. Run `primitive login` to sign in again.";
46207
47270
  const NO_CURRENT_ORG_MESSAGE = "No organization is selected for this login. Run `primitive orgs` to see where you can use the CLI, then `primitive org use <org>`.";
47271
+ /**
47272
+ * The CLI login and the organization list report a person's stored role:
47273
+ * the largest built-in role (owner, admin or member) their access fully
47274
+ * includes, or `restricted` when it includes none or they hold no role at
47275
+ * all. The role's own name is not in these answers.
47276
+ */
47277
+ const BUILT_IN_ROLE_KEYS = new Set(BUILTIN_ROLE_KEYS);
47278
+ /** Whether a role key names one of the built-in roles. */
47279
+ function isBuiltInRoleKey(role) {
47280
+ return BUILT_IN_ROLE_KEYS.has(role);
47281
+ }
47282
+ /** What the role key `restricted` stands for. */
47283
+ const RESTRICTED_ROLE_NOTE = "a template or custom role that does not fully include member, because it lacks some member permission (as Operator does) or reaches only some mailboxes";
47284
+ /** A stored role key, labelled for what it is: "built-in role admin", "role restricted". */
47285
+ function roleKeyLabel(role) {
47286
+ return isBuiltInRoleKey(role) ? `built-in role ${role}` : `role ${role}`;
47287
+ }
47288
+ /** A stored role key for a sentence, saying what `restricted` stands for. */
47289
+ function roleKeyText(role) {
47290
+ return role === "restricted" ? `role restricted (${RESTRICTED_ROLE_NOTE})` : roleKeyLabel(role);
47291
+ }
46208
47292
  const REFUSAL_FALLBACK_MESSAGES = {
46209
47293
  not_a_member: "You are not a member of this organization.",
46210
47294
  cli_access_disabled_by_org: "This organization does not allow CLI access.",
@@ -47104,7 +48188,7 @@ function errorDetail(error) {
47104
48188
  return error instanceof Error ? error.message : String(error);
47105
48189
  }
47106
48190
  /** `count` and the word for it: "1 expectation", "2 expectations". */
47107
- function plural(count, one, many) {
48191
+ function plural$1(count, one, many) {
47108
48192
  return `${count} ${count === 1 ? one : many}`;
47109
48193
  }
47110
48194
  function checkEmailId(id, label) {
@@ -47423,21 +48507,21 @@ function expectationsOutcome(expectations, testResults = []) {
47423
48507
  exitCode: 1,
47424
48508
  error: {
47425
48509
  code: "expectations_failed",
47426
- message: `${plural(failedCount, "expectation", "expectations")} failed${failed.length > 0 ? `: ${failed.join(", ")}` : ""}.`
48510
+ message: `${plural$1(failedCount, "expectation", "expectations")} failed${failed.length > 0 ? `: ${failed.join(", ")}` : ""}.`
47427
48511
  }
47428
48512
  };
47429
48513
  if (unresolvable.length > 0) return {
47430
48514
  exitCode: 1,
47431
48515
  error: {
47432
48516
  code: "expectations_unresolvable",
47433
- message: `${plural(unresolvable.length, "expectation is", "expectations are")} undecided, and testing again will not decide ${unresolvable.length === 1 ? "it" : "them"}: ${why(unresolvable)}.`
48517
+ message: `${plural$1(unresolvable.length, "expectation is", "expectations are")} undecided, and testing again will not decide ${unresolvable.length === 1 ? "it" : "them"}: ${why(unresolvable)}.`
47434
48518
  }
47435
48519
  };
47436
48520
  if (undecidedCount > 0) return {
47437
48521
  exitCode: 3,
47438
48522
  error: {
47439
48523
  code: "expectations_undecided",
47440
- message: `${plural(undecidedCount, "expectation is", "expectations are")} undecided${undecided.length > 0 ? `: ${why(undecided)}` : ""}. Test again later.`
48524
+ message: `${plural$1(undecidedCount, "expectation is", "expectations are")} undecided${undecided.length > 0 ? `: ${why(undecided)}` : ""}. Test again later.`
47441
48525
  }
47442
48526
  };
47443
48527
  return {};
@@ -47471,7 +48555,8 @@ function expectationsCheckLine(check, ruleId) {
47471
48555
  else if (undecided.length > 0) parts.push("none failed");
47472
48556
  if (undecided.length > 0) parts.push(`${undecided.length} undecided (${undecided.join(", ")})`);
47473
48557
  const rest = undecided.length > 0 ? ` To decide the rest, run \`primitive email-rules test-email-rule --rule-id ${ruleId}\`.` : "";
47474
- return `Expectations on ${on}: ${parts.length > 0 ? parts.join(", ") : "not passed"}.${rest}`;
48558
+ const note = typeof check.note === "string" && check.note.trim() ? ` ${check.note.trim()}` : "";
48559
+ return `Expectations on ${on}: ${parts.length > 0 ? parts.join(", ") : "not passed"}.${rest}${note}`;
47475
48560
  }
47476
48561
  /** The flag that makes a rule write's exit code follow its expectations_check. */
47477
48562
  const FAIL_ON_EXPECTATIONS_FLAG = "fail-on-expectations";
@@ -47483,7 +48568,7 @@ const FAIL_ON_EXPECTATIONS_FLAG = "fail-on-expectations";
47483
48568
  * `passed` as false or as null when nothing failed and something is
47484
48569
  * undecided; any other check that did not pass exits 1.
47485
48570
  */
47486
- function writtenExpectationsOutcome(check, sentExpectations, ruleId) {
48571
+ function writtenExpectationsOutcome(check, sentExpectations, ruleId, saved = "") {
47487
48572
  const test = `\`primitive email-rules test-email-rule --rule-id ${ruleId}\``;
47488
48573
  if (!isRecord(check)) {
47489
48574
  if (!sentExpectations) return {};
@@ -47491,7 +48576,7 @@ function writtenExpectationsOutcome(check, sentExpectations, ruleId) {
47491
48576
  exitCode: 3,
47492
48577
  error: {
47493
48578
  code: "expectations_unchecked",
47494
- message: `The expectations this write sent were not checked on it. Test them with ${test}.`
48579
+ message: `${saved}The expectations this write sent were not checked on it. Test them with ${test}.`
47495
48580
  }
47496
48581
  };
47497
48582
  }
@@ -47501,25 +48586,61 @@ function writtenExpectationsOutcome(check, sentExpectations, ruleId) {
47501
48586
  exitCode: 1,
47502
48587
  error: {
47503
48588
  code: "expectations_failed",
47504
- message: `${plural(failed.length, "expectation", "expectations")} failed on this write: ${failed.join(", ")}.`
48589
+ message: `${saved}${plural$1(failed.length, "expectation", "expectations")} failed on this write: ${failed.join(", ")}.`
47505
48590
  }
47506
48591
  };
47507
48592
  if (undecided.length > 0) return {
47508
48593
  exitCode: 3,
47509
48594
  error: {
47510
48595
  code: "expectations_undecided",
47511
- message: `${plural(undecided.length, "expectation is", "expectations are")} undecided on this write, which judges them without classifying mail or asking the rule's questions: ${undecided.join(", ")}. Test them with ${test}.`
48596
+ message: `${saved}${plural$1(undecided.length, "expectation is", "expectations are")} undecided on this write, which judges them without classifying mail or asking the rule's questions: ${undecided.join(", ")}. Test them with ${test}.`
47512
48597
  }
47513
48598
  };
47514
48599
  if (check.passed !== true) return {
47515
48600
  exitCode: 1,
47516
48601
  error: {
47517
48602
  code: "expectations_failed",
47518
- message: "The write's expectations check did not pass."
48603
+ message: `${saved}The write's expectations check did not pass.`
47519
48604
  }
47520
48605
  };
47521
48606
  return {};
47522
48607
  }
48608
+ /**
48609
+ * update-email-rule's post-save outcome says the write was saved: the server
48610
+ * judges a write before saving only when it saves a version or turns the
48611
+ * rule active, and refuses with these same codes, saving nothing.
48612
+ */
48613
+ const SAVED_UNJUDGED = "The write was saved: the server judges a write before saving only when it saves a version or turns the rule active, so not this one. ";
48614
+ /** What a restore_version write saved, as its answer says. */
48615
+ function restoredLine(restored, currentVersion) {
48616
+ if (!isRecord(restored) || typeof restored.version !== "number") return void 0;
48617
+ const note = typeof restored.note === "string" && restored.note.trim() ? ` ${restored.note.trim()}` : "";
48618
+ const as = typeof currentVersion === "number" ? `as version ${currentVersion}, now current` : "as the current version";
48619
+ return `Saved version ${restored.version} again ${as}.${note}`;
48620
+ }
48621
+ /**
48622
+ * Who an update that saved a version reaches, as its answer counts them: the
48623
+ * keys listing the rule, the webhooks those keys own, and the shares the
48624
+ * write ended.
48625
+ */
48626
+ function affectsLine(affects) {
48627
+ if (!isRecord(affects)) return void 0;
48628
+ const count = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
48629
+ const keys = count(affects.keys);
48630
+ const endpoints = count(affects.endpoints);
48631
+ const shares = count(affects.shares_ended);
48632
+ if (keys === void 0 || endpoints === void 0 || shares === void 0) return void 0;
48633
+ return `This version reaches ${plural$1(keys, "API key", "API keys")} listing the rule and ${plural$1(endpoints, "webhook", "webhooks")} those keys own; it ended ${plural$1(shares, "share", "shares")}. The rule keeps its state.`;
48634
+ }
48635
+ /** The rule's author, by name and email where the answer gives them. */
48636
+ function authorLine(author) {
48637
+ if (!isRecord(author) || typeof author.user_id !== "string") return void 0;
48638
+ const name = typeof author.name === "string" && author.name.trim() ? author.name.trim() : null;
48639
+ const email = typeof author.email === "string" && author.email.trim() ? author.email.trim() : null;
48640
+ if (name && email) return `Author: ${name} <${email}>`;
48641
+ if (name || email) return `Author: ${name ?? email}`;
48642
+ return `Author: user ${author.user_id} (name and email not shown to you)`;
48643
+ }
47523
48644
  /** The stderr report of a rule written by create-email-rule or update-email-rule. */
47524
48645
  function writtenEmailRuleReport(data, request) {
47525
48646
  if (!isRecord(data)) return { lines: [] };
@@ -47527,6 +48648,10 @@ function writtenEmailRuleReport(data, request) {
47527
48648
  const ruleId = typeof rule.id === "string" ? rule.id : "<rule-id>";
47528
48649
  const lines = scopeWarningLines(rule.scope_warnings);
47529
48650
  if (rule.kind === "do_not_share") lines.unshift(`This rule blocks sharing (kind do_not_share): while it is active, no rule shares an email it matches.${typeof rule.state === "string" && rule.state !== "active" ? " It blocks nothing until it is active." : ""}`);
48651
+ const restored = restoredLine(rule.restored_from, rule.current_version);
48652
+ if (restored) lines.push(restored);
48653
+ const reach = affectsLine(rule.affects);
48654
+ if (reach) lines.push(reach);
47530
48655
  const sentExpectations = isRecord(request.body) && isRecord(request.body.expectations);
47531
48656
  const checked = expectationsCheckLine(rule.expectations_check, ruleId);
47532
48657
  if (checked) lines.push(checked);
@@ -47534,9 +48659,13 @@ function writtenEmailRuleReport(data, request) {
47534
48659
  if (request.flags?.["fail-on-expectations"] !== true) return { lines };
47535
48660
  return {
47536
48661
  lines,
47537
- ...writtenExpectationsOutcome(rule.expectations_check, sentExpectations, ruleId)
48662
+ ...writtenExpectationsOutcome(rule.expectations_check, sentExpectations, ruleId, isUpdate(request) ? SAVED_UNJUDGED : "")
47538
48663
  };
47539
48664
  }
48665
+ /** Whether a rule write is an update: it names the rule in its path. */
48666
+ function isUpdate(request) {
48667
+ return typeof request.path?.id === "string";
48668
+ }
47540
48669
  /**
47541
48670
  * A rule write's exit code when its report cannot be built, under
47542
48671
  * --fail-on-expectations: 1 for a check that is there and did not pass, and 3
@@ -47557,6 +48686,33 @@ function emailRuleScopeReport(data) {
47557
48686
  }
47558
48687
  return { lines };
47559
48688
  }
48689
+ /** The delete-email-rule question, naming the rule. */
48690
+ function deleteEmailRuleQuestion(flags) {
48691
+ return `Delete email rule ${typeof flags.id === "string" ? flags.id : "this rule"}? Its versions and decisions are deleted with it. [y/N] `;
48692
+ }
48693
+ /**
48694
+ * What a rule's delete left behind, read from the answer: the shares it
48695
+ * ended, and the keys that still list the rule and now read nothing through
48696
+ * it. An answer without these fields (an older server) prints nothing.
48697
+ */
48698
+ function deleteEmailRuleReport(data, request) {
48699
+ if (!isRecord(data)) return { lines: [] };
48700
+ const answer = data;
48701
+ const lines = [];
48702
+ const ended = Array.isArray(answer.ended_shares) ? answer.ended_shares.length : 0;
48703
+ if (ended > 0) lines.push(`Ended ${ended === 1 ? "1 share" : `${ended} shares`} of this rule (end_reason rule_deleted). ${ended === 1 ? "It keeps its record" : "Each keeps its record"}.`);
48704
+ const keys = Array.isArray(answer.keys) ? answer.keys : [];
48705
+ const others = typeof answer.other_keys === "number" && answer.other_keys > 0 ? answer.other_keys : 0;
48706
+ const ruleId = typeof request.path.id === "string" ? request.path.id : "<rule-id>";
48707
+ if (keys.length > 0) {
48708
+ lines.push(`${keys.length === 1 ? "1 key still lists" : `${keys.length} keys still list`} this rule and now ${keys.length === 1 ? "reads" : "read"} nothing through it:`);
48709
+ for (const key of keys) lines.push(` ${key.id} ${key.name ?? "(no name)"} ${key.key_prefix}`);
48710
+ lines.push(keys.length === 1 ? `Take the rule off with \`primitive keys update ${keys[0]?.id} --remove-rule ${ruleId}\`, or revoke the key.` : `Take the rule off each with \`primitive keys update <key-id> --remove-rule ${ruleId}\`, or revoke them.`);
48711
+ }
48712
+ if (others > 0) lines.push(keys.length > 0 ? `${others === 1 ? "1 more key" : `${others} more keys`} you cannot read ${others === 1 ? "lists" : "list"} this rule too.` : `${others === 1 ? "1 key" : `${others} keys`} you cannot read ${others === 1 ? "lists" : "list"} this rule and now ${others === 1 ? "reads" : "read"} nothing through it.`);
48713
+ if (Array.isArray(answer.ended_shares) && answer.keys === void 0 && answer.other_keys === void 0) lines.push("Could not check which keys list this rule.");
48714
+ return { lines };
48715
+ }
47560
48716
  function errorDetails(payload) {
47561
48717
  if (!isRecord(payload)) return {};
47562
48718
  if (isRecord(payload.details)) return payload.details;
@@ -47576,7 +48732,93 @@ function versionMovedHint(payload, ruleId) {
47576
48732
  const read = `Read it again with \`primitive email-rules get-email-rule ${id}\` and reapply your change to what it shows`;
47577
48733
  return current === void 0 ? `Hint: the rule's version is not the if_version you sent, so nothing was saved, and the answer did not give its current version. ${read}.` : `Hint: the rule's current version is ${current}, not the if_version you sent, so nothing was saved. ${read}, then retry with --if-version ${current}.`;
47578
48734
  }
47579
- const EXPECTATION_FLAG_NOTE = "Replaces any expect in --body-file or --raw-body. The exit code follows the answer's expectations; see --help-full.";
48735
+ /** The read-again advice every conflict ends with. */
48736
+ function readAgain(id) {
48737
+ return `Read it again with \`primitive email-rules get-email-rule ${id}\` and send the write again`;
48738
+ }
48739
+ /**
48740
+ * The hint for update-email-rule's refusals: an email_rule_conflict by its
48741
+ * details.reason (version_moved, revision_moved, expectations_moved,
48742
+ * rule_changed), and the expectations guard's expectations_failed,
48743
+ * expectations_undecided and no_expectations. Nothing was saved in any of
48744
+ * them.
48745
+ */
48746
+ function updateEmailRuleErrorHint(code, payload, ruleId, body) {
48747
+ const id = typeof ruleId === "string" && ruleId ? ruleId : "<rule-id>";
48748
+ const details = errorDetails(payload);
48749
+ const guarded = isRecord(body) && body.if_expectations_pass === true;
48750
+ if (code === "email_rule_conflict") switch (details.reason) {
48751
+ case "version_moved": return versionMovedHint(payload, ruleId);
48752
+ case "revision_moved": {
48753
+ const current = typeof details.current_revision === "number" ? details.current_revision : void 0;
48754
+ return current === void 0 ? `Hint: the rule was updated since the if_revision you sent, so nothing was saved. ${readAgain(id)} with --if-revision set to its revision.` : `Hint: the rule is now at revision ${current}, not the if_revision you sent, so nothing was saved. ${readAgain(id)}, reapplying your change to what it shows, with --if-revision ${current}.`;
48755
+ }
48756
+ case "expectations_moved": return `Hint: the rule's saved expectations changed while this write was checked against them, so nothing was saved. ${readAgain(id)}.`;
48757
+ case "rule_changed": return guarded ? `Hint: the rule changed after this write was judged against its expectations and before it was saved, so saving it would have gone unchecked; nothing was saved. ${readAgain(id)}.` : `Hint: the rule changed while this write was being saved, so nothing was saved. ${readAgain(id)}.`;
48758
+ default: return;
48759
+ }
48760
+ if (code === "expectations_failed" || code === "expectations_undecided") {
48761
+ const check = isRecord(details.expectations_check) ? details.expectations_check : {};
48762
+ const failed = Array.isArray(check.failed) ? check.failed : [];
48763
+ const undecided = Array.isArray(check.undecided) ? check.undecided : [];
48764
+ const parts = [...failed.length > 0 ? [`failed: ${failed.join(", ")}`] : [], ...undecided.length > 0 ? [`undecided: ${undecided.join(", ")}`] : []];
48765
+ return `Hint: the rule, as this write would leave it, was judged against its expectations before saving (if_expectations_pass), and nothing was saved.${parts.length > 0 ? ` Expectations ${parts.join("; ")}.` : ""}${code === "expectations_failed" ? "" : ` ${UNDECIDED_NEXT_STEP[String(details.reason)] ?? UNDECIDED_NEXT_STEP.none}`} The command exits ${updateEmailRuleErrorExitCode(payload) ?? 1}.`;
48766
+ }
48767
+ if (code === "no_expectations") return "Hint: nothing was saved. Give the rule expectations with --expectations-file (in this write or before it), or send the write without if_expectations_pass (--fail-on-expectations or --if-expectations-pass).";
48768
+ }
48769
+ /** What to do about an undecided judgment, by details.reason ("none": no reason given). */
48770
+ const UNDECIDED_NEXT_STEP = {
48771
+ none: "Some could not be decided yet (mail not classified yet, or a question not answered yet); send the write again later.",
48772
+ deadline: "The judgment ran out of time; send the write again.",
48773
+ error: "The judgment could not finish; send the write again.",
48774
+ client_gone: "The request ended before the write was saved; send it again.",
48775
+ author_unresolvable: "The rule cannot run as its author over the mail it would cover (details.author_reason), so sending the write again will not decide them."
48776
+ };
48777
+ /**
48778
+ * update-email-rule's exit code for the expectations guard's refusals: 1 when
48779
+ * one failed, or when they cannot be decided by trying again (the rule cannot
48780
+ * run as its author); 3 when trying again later can decide them.
48781
+ */
48782
+ function updateEmailRuleErrorExitCode(payload) {
48783
+ const code = isRecord(payload) ? payload.code : void 0;
48784
+ if (code === "expectations_failed") return 1;
48785
+ if (code !== "expectations_undecided") return void 0;
48786
+ return errorDetails(payload).reason === "author_unresolvable" ? 1 : 3;
48787
+ }
48788
+ /** The stderr report of compile-email-rule: how many recent emails it holds for. */
48789
+ function compileEmailRuleReport(data) {
48790
+ if (!isRecord(data)) return { lines: [] };
48791
+ const breadth = data.breadth;
48792
+ if (!isRecord(breadth) || typeof breadth.checked !== "number" || typeof breadth.matched !== "number") return { lines: [] };
48793
+ return { lines: [`Breadth: its conditions hold for ${breadth.matched} of ${plural$1(breadth.checked, "recent email", "recent emails")} checked in your own mailboxes, whatever scope you save it with.`] };
48794
+ }
48795
+ /** The stderr report of get-email-rule: who wrote it, and its revision. */
48796
+ function getEmailRuleReport(data) {
48797
+ if (!isRecord(data)) return { lines: [] };
48798
+ const lines = [];
48799
+ const author = authorLine(data.author);
48800
+ if (author) lines.push(author);
48801
+ if (typeof data.revision === "number") lines.push(`Revision ${data.revision}: pass --if-revision ${data.revision} to update-email-rule so a change made since is never replaced unseen.`);
48802
+ return { lines };
48803
+ }
48804
+ /** create-email-rule's --fail-on-expectations: the exit code follows the check made after saving. */
48805
+ const FAIL_ON_EXPECTATIONS_HELP = "Exit 1 when an expectation in the answer's expectations_check failed, and 3 when none failed but one is undecided or the expectations this write sent were not checked (test the rule to decide). Without it the command exits 0 whatever the check says.";
48806
+ /** update-email-rule's --fail-on-expectations: the server refuses the write. */
48807
+ const UPDATE_FAIL_ON_EXPECTATIONS_HELP = "Refuse the write unless the rule, as this write would leave it, passes its expectations. It sends if_expectations_pass (over any in --body-file or --raw-body), so the server judges a new version, a restored one, a new scope or turning the rule active before saving, and saves nothing when one failed (exit 1), one is undecided (exit 3, or 1 when the rule cannot run as its author) or the rule has none (exit 1). It can take as long as a test of the rule; the command waits for it. Judging needs a credential that reads the mail's content. A write the server does not judge (one changing only expectations, say) is saved, and the exit code then follows the answer's expectations_check, as on create. With --json, a refused write's document holds the API's error alone; a saved write's holds the rule.";
48808
+ const EXPECTATION_FLAG_NOTE = "Replaces any expect in --body-file or --raw-body. The answer reports them in expectations (passed, counts, results) and the exit code follows it; see --help-full.";
48809
+ /**
48810
+ * update-email-rule's --fail-on-expectations sends if_expectations_pass, over
48811
+ * any in the body. Asking for it while turning it off is refused.
48812
+ */
48813
+ function expectationsGuardBodyFields(flags) {
48814
+ if (flags["fail-on-expectations"] !== true) return [];
48815
+ if (flags["if-expectations-pass"] === false) throw usageError("--fail-on-expectations asks the server to judge the write before saving it; it cannot be combined with --no-if-expectations-pass.");
48816
+ return [{
48817
+ flag: `--${FAIL_ON_EXPECTATIONS_FLAG}`,
48818
+ property: "if_expectations_pass",
48819
+ value: true
48820
+ }];
48821
+ }
47580
48822
  const EMAIL_RULE_OPERATION_EXTENSIONS = {
47581
48823
  testEmailRule: {
47582
48824
  flags: {
@@ -47605,8 +48847,8 @@ const EMAIL_RULE_OPERATION_EXTENSIONS = {
47605
48847
  },
47606
48848
  createEmailRule: {
47607
48849
  flags: {
47608
- "expectations-file": Flags.string({ description: "A JSON file holding the emails the rule must match and must not, such as {\"match\": [\"<email-id>\"], \"no_match\": [\"<email-id>\"]}. Sets expectations, over any in --body-file or --raw-body; the answer judges them in expectations_check." }),
47609
- [FAIL_ON_EXPECTATIONS_FLAG]: Flags.boolean({ description: "Exit 1 when an expectation in the answer's expectations_check failed, and 3 when none failed but one is undecided or the expectations this write sent were not checked (test the rule to decide). Without it the command exits 0 whatever the check says." })
48850
+ "expectations-file": Flags.string({ description: "A JSON file holding the emails the rule must match and must not, such as {\"match\": [\"<email-id>\"], \"no_match\": [\"<email-id>\"]}. Sets expectations, over any in --body-file or --raw-body; the answer judges them in expectations_check (passed, decided, failed, undecided); the report test-email-rule answers with is named expectations instead." }),
48851
+ [FAIL_ON_EXPECTATIONS_FLAG]: Flags.boolean({ description: FAIL_ON_EXPECTATIONS_HELP })
47610
48852
  },
47611
48853
  bodyFields: expectationsBodyFields(false),
47612
48854
  bodyProperties: ["expectations"],
@@ -47615,17 +48857,38 @@ const EMAIL_RULE_OPERATION_EXTENSIONS = {
47615
48857
  },
47616
48858
  updateEmailRule: {
47617
48859
  flags: {
47618
- "expectations-file": Flags.string({ description: "A JSON file holding the emails the rule must match and must not, such as {\"match\": [\"<email-id>\"], \"no_match\": [\"<email-id>\"]}, replacing its saved expectations; a file holding null removes them. Sets expectations, over any in --body-file or --raw-body; the answer judges them in expectations_check." }),
47619
- [FAIL_ON_EXPECTATIONS_FLAG]: Flags.boolean({ description: "Exit 1 when an expectation in the answer's expectations_check failed, and 3 when none failed but one is undecided or the expectations this write sent were not checked (test the rule to decide). Without it the command exits 0 whatever the check says." })
48860
+ "expectations-file": Flags.string({ description: "A JSON file holding the emails the rule must match and must not, such as {\"match\": [\"<email-id>\"], \"no_match\": [\"<email-id>\"]}, replacing its saved expectations; a file holding null removes them. Sets expectations, over any in --body-file or --raw-body; the answer judges them in expectations_check (passed, decided, failed, undecided); the report test-email-rule answers with is named expectations instead." }),
48861
+ [FAIL_ON_EXPECTATIONS_FLAG]: Flags.boolean({ description: UPDATE_FAIL_ON_EXPECTATIONS_HELP })
47620
48862
  },
47621
- bodyFields: expectationsBodyFields(true),
48863
+ bodyFields: (flags) => [...expectationsBodyFields(true)(flags), ...expectationsGuardBodyFields(flags)],
47622
48864
  bodyProperties: ["expectations"],
48865
+ errorExitCode: updateEmailRuleErrorExitCode,
47623
48866
  fallbackExitCode: writtenFallbackExitCode,
47624
48867
  report: writtenEmailRuleReport
47625
48868
  },
48869
+ compileEmailRule: {
48870
+ flags: {},
48871
+ report: compileEmailRuleReport
48872
+ },
48873
+ getEmailRule: {
48874
+ flags: {},
48875
+ report: getEmailRuleReport
48876
+ },
47626
48877
  getEmailRuleScope: {
47627
48878
  flags: {},
47628
48879
  report: emailRuleScopeReport
48880
+ },
48881
+ deleteEmailRule: {
48882
+ flags: { yes: Flags.boolean({
48883
+ char: "y",
48884
+ description: "Delete without asking for confirmation. Needed without a terminal, and with --json."
48885
+ }) },
48886
+ confirm: {
48887
+ question: deleteEmailRuleQuestion,
48888
+ noTerminal: "Not deleting: no terminal to confirm in. Pass --yes to delete the rule without a prompt.",
48889
+ cancelled: "Cancelled. The rule was not deleted."
48890
+ },
48891
+ report: deleteEmailRuleReport
47629
48892
  }
47630
48893
  };
47631
48894
  /** The classification in a listEmailRuleResults answer, or null. */
@@ -47642,15 +48905,208 @@ function noFactsMessage(data) {
47642
48905
  if (classificationOf(data)) return "This email's classification has no facts.";
47643
48906
  return (isRecord(data) && Array.isArray(data.rules) ? data.rules : []).some((rule) => isRecord(rule) && isRecord(rule.decision) && rule.decision.detail_withheld === true) ? "No classification to show: this email's content cannot be read (withheld, failed to parse or discarded)." : "No classification for this email yet. It has not been classified (recent mail can take a while, so try again later), it will not be, or its content cannot be read.";
47644
48907
  }
48908
+ // src/oclif/empty-mail-hint.ts
48909
+ /**
48910
+ * Query parameters that page, shape or modify a list without narrowing which
48911
+ * emails it holds on their own. Any other one the caller set is a filter.
48912
+ */
48913
+ const NOT_FILTERS = /* @__PURE__ */ new Set([
48914
+ "cursor",
48915
+ "since",
48916
+ "limit",
48917
+ "include",
48918
+ "wait",
48919
+ "group",
48920
+ "search_regex",
48921
+ "search_case_sensitive",
48922
+ "search_negate"
48923
+ ]);
48924
+ /** How many addresses, domains or rules the hint names before "and N more". */
48925
+ const NAMED_MAX = 3;
48926
+ /** How long the hint waits for the grant before leaving it out. */
48927
+ const GRANT_TIMEOUT_MS = 2e3;
48928
+ /**
48929
+ * The caller's grant from GET /whoami/grant, or undefined when it cannot be
48930
+ * read in time: the hint then says only that the credential may be narrowed.
48931
+ * Reading mail follows `mail:read`'s own scope where it reaches fewer
48932
+ * mailboxes than the grant's.
48933
+ */
48934
+ async function readCallerMail(client, timeoutMs = GRANT_TIMEOUT_MS) {
48935
+ let timer;
48936
+ const late = new Promise((resolve) => {
48937
+ timer = setTimeout(() => resolve(void 0), timeoutMs);
48938
+ });
48939
+ try {
48940
+ return await Promise.race([readGrant(client, timeoutMs), late]);
48941
+ } finally {
48942
+ clearTimeout(timer);
48943
+ }
48944
+ }
48945
+ async function readGrant(client, timeoutMs) {
48946
+ try {
48947
+ const result = await getOwnGrant({
48948
+ client,
48949
+ fetch: (input, init) => globalThis.fetch(input, init),
48950
+ responseStyle: "fields",
48951
+ signal: AbortSignal.timeout(timeoutMs)
48952
+ });
48953
+ if (result.error) return void 0;
48954
+ const grant = result.data?.data?.grant;
48955
+ if (!grant || typeof grant !== "object") return void 0;
48956
+ const g = grant;
48957
+ const scoped = g.scoped_permissions && typeof g.scoped_permissions === "object" ? g.scoped_permissions["mail:read"] : void 0;
48958
+ return {
48959
+ credentialKind: g.credential_kind,
48960
+ mailScope: scoped ?? g.mail_scope
48961
+ };
48962
+ } catch {
48963
+ return;
48964
+ }
48965
+ }
48966
+ /**
48967
+ * What the caller reads, for the hint: a connected agent's key from the
48968
+ * address it was connected for (it may not ask GET /whoami/grant), any other
48969
+ * credential from GET /whoami/grant.
48970
+ */
48971
+ async function readCallerMailFor(auth, client) {
48972
+ const address = auth.connectedAgent?.agentAddress;
48973
+ if (address) return {
48974
+ credentialKind: "connected_key",
48975
+ mailScope: {
48976
+ addresses: [address],
48977
+ domains: []
48978
+ }
48979
+ };
48980
+ if (auth.apiKey?.startsWith("pconn_")) return {
48981
+ credentialKind: "connected_key",
48982
+ mailScope: void 0
48983
+ };
48984
+ return readCallerMail(client);
48985
+ }
48986
+ /** The query parameters the caller set that narrow which emails are listed. */
48987
+ function listFilters(query) {
48988
+ return Object.keys(query).filter((name) => {
48989
+ const value = query[name];
48990
+ if (NOT_FILTERS.has(name) || value === void 0) return false;
48991
+ const off = value === false || value === "false";
48992
+ if (name.startsWith("exclude_")) return !off;
48993
+ if (name === "include_presence") return off;
48994
+ return true;
48995
+ });
48996
+ }
48997
+ function strings(value) {
48998
+ return Array.isArray(value) && value.every((v) => typeof v === "string") ? value : null;
48999
+ }
49000
+ /** What a mail scope from GET /whoami/grant reaches, as far as the hint needs. */
49001
+ function mailReach(scope) {
49002
+ if (scope === "org") return { kind: "org" };
49003
+ if (scope === "unreadable") return { kind: "unreadable" };
49004
+ if (!scope || typeof scope !== "object" || Array.isArray(scope)) return { kind: "unknown" };
49005
+ const record = scope;
49006
+ const addresses = strings(record.addresses ?? []);
49007
+ const domains = strings(record.domains ?? []);
49008
+ const rules = strings(record.rules ?? []);
49009
+ if (!addresses || !domains || !rules) return { kind: "unknown" };
49010
+ if (record.direction === "outbound" && rules.length === 0) return { kind: "outbound_only" };
49011
+ if (addresses.length === 0 && domains.length === 0) return rules.length > 0 ? {
49012
+ kind: "rules_only",
49013
+ rules
49014
+ } : { kind: "none" };
49015
+ const exceptPersonal = typeof record.excluded_personal_mailboxes === "number" && record.excluded_personal_mailboxes > 0;
49016
+ return {
49017
+ kind: "mailboxes",
49018
+ names: [...addresses, ...domains.map((d) => exceptPersonal ? `addresses at ${d} other than other members' personal mailboxes` : `any address at ${d}`)],
49019
+ rules
49020
+ };
49021
+ }
49022
+ function named(names) {
49023
+ const shown = names.slice(0, NAMED_MAX);
49024
+ const rest = names.length - shown.length;
49025
+ const all = rest > 0 ? [...shown, `${rest} more`] : shown;
49026
+ return all.length <= 1 ? all[0] ?? "" : `${all.slice(0, -1).join(", ")} and ${all[all.length - 1]}`;
49027
+ }
49028
+ const plural = (n, one) => `${n} ${one}${n === 1 ? "" : "s"}`;
49029
+ const GRANT_POINTER = "`primitive whoami --json --grant` shows which mail it reaches.";
49030
+ /**
49031
+ * How a rule grants mail, and who can look at it: a key cannot read rules
49032
+ * itself. An id a key lists is an email rule, or a rule shared with the key's
49033
+ * holder, which its holder checks as a share.
49034
+ */
49035
+ function rulesSentence(rules) {
49036
+ return `A rule grants an email it matches when the email is evaluated, shortly after it arrives, while the rule is active, and the key reads that mail only while the rule stays active. Each id (${named(rules)}) is an email rule or a rule shared with the key's holder: a rule's author, signed in as themselves, can check it with \`primitive email-rules get-email-rule --id <id>\`, and a share's holder with \`primitive email-rules get-email-rule-share --id <id>\`.`;
49037
+ }
49038
+ function holderOf(kind) {
49039
+ if (kind === "org_key" || kind === "function_key") return "key";
49040
+ if (kind === "connected_key") return "connection";
49041
+ return typeof kind === "string" ? "person" : "unknown";
49042
+ }
49043
+ const NOUN = {
49044
+ key: "This key",
49045
+ connection: "This connection key",
49046
+ person: "This login",
49047
+ unknown: "This credential"
49048
+ };
49049
+ /** A person also reads mail through rules colleagues shared with them, which sit beside the grant, not in it. */
49050
+ const SHARED = "mail from email rules colleagues shared with you (`primitive email-rules list-email-rule-shares` lists them)";
49051
+ /** A connection key also reads member mailboxes its owner gave it, asked for by name. */
49052
+ const GIVEN = "any member mailbox its owner gave it (pass that mailbox with --recipient)";
49053
+ /** What the credential reads, as sentences for the hint, or null when it reads every mailbox. */
49054
+ function reachSentence(reach, holder) {
49055
+ const noun = NOUN[holder];
49056
+ switch (reach.kind) {
49057
+ case "org": return null;
49058
+ case "unknown": return holder === "connection" ? `${noun} reads the address it was connected for, and ${GIVEN}.` : `${noun} may not read every mailbox; ${GRANT_POINTER}`;
49059
+ case "outbound_only": return holder === "person" ? `${noun} reads no received mail through its grant, which covers sent mail only, but does read ${SHARED}. ${GRANT_POINTER}` : `${noun} reads no received mail: its grant covers sent mail only. ${GRANT_POINTER}`;
49060
+ case "unreadable": return `The server could not read this credential's stored mail scope, so it reaches no mail through it. ${GRANT_POINTER}`;
49061
+ case "none": return holder === "person" ? `${noun} reads no mailbox through its grant, only ${SHARED}. ${GRANT_POINTER}` : `${noun} reads no mail: its grant names no mailbox or domain, and no email rule or live share. ${GRANT_POINTER}`;
49062
+ case "rules_only": return `${noun} reads only mail its ${plural(reach.rules.length, "email rule")} granted. ${rulesSentence(reach.rules)}`;
49063
+ case "mailboxes": {
49064
+ const parts = [
49065
+ named(reach.names),
49066
+ ...reach.rules.length > 0 ? [`mail its ${plural(reach.rules.length, "email rule")} granted`] : [],
49067
+ ...holder === "connection" ? [GIVEN] : [],
49068
+ ...holder === "person" ? [SHARED] : []
49069
+ ];
49070
+ const all = parts.length === 1 ? parts[0] : `${parts.slice(0, -1).join(", ")}, and ${parts[parts.length - 1]}`;
49071
+ return holder === "connection" ? `${noun} reads only ${all}.` : `${noun} reads ${holder === "person" ? "" : "only "}${all}; other mail is not listed.`;
49072
+ }
49073
+ }
49074
+ }
49075
+ /**
49076
+ * The hint for an empty inbound email list: what was asked (the filter flags
49077
+ * given, or a page or tail past the last email), and what this credential
49078
+ * reads. `caller` is undefined when the grant could not be read, and null
49079
+ * when it was not asked for (a tail poll, which should not double its
49080
+ * requests, or a page with a next cursor, whose hint does not need it).
49081
+ */
49082
+ function emptyEmailListHint(input) {
49083
+ if (input.nextCursor) return `(no results) This page came back empty, but the list goes on: continue with --cursor ${input.nextCursor}.`;
49084
+ const reach = input.caller === null ? { kind: "org" } : mailReach(input.caller?.mailScope);
49085
+ const holder = holderOf(input.caller?.credentialKind);
49086
+ const scope = reachSentence(reach, holder);
49087
+ const withScope = (lead) => `${lead}${scope ? ` ${scope}` : ""}`;
49088
+ const filters = holder === "connection" ? input.filterFlags.filter((f) => !f.startsWith("--include-presence")) : input.filterFlags;
49089
+ if (filters.length > 0) return withScope(`(no results) No inbound emails match the filters given (${filters.join(", ")}).`);
49090
+ if (input.pastFlag) return withScope(input.pastFlag === "--cursor" ? "(no results) No more inbound emails past the given --cursor." : "(no results) No inbound emails after the given --since.");
49091
+ if (!scope) return input.orgHint ?? "(no results) No inbound emails received yet on this account. Send one to a verified domain to populate this list. For a compact view, prefer `primitive emails latest`.";
49092
+ const noun = NOUN[holder].replace(/^This/, "this");
49093
+ if (reach.kind === "rules_only") return `(no results) No mail ${noun}'s rules granted is readable now. ${scope}`;
49094
+ if (reach.kind === "mailboxes") return `(no results) No inbound emails yet in the mail ${noun} reads. ${scope}`;
49095
+ return `(no results) No inbound emails to show. ${scope}`;
49096
+ }
47645
49097
  // src/oclif/endpoint-event-types.ts
47646
49098
  const EVENT_TYPES_FLAG_OPERATIONS = /* @__PURE__ */ new Set(["createEndpoint", "updateEndpoint"]);
47647
- const EVENT_TYPES_FLAG_DESCRIPTION = "Event types this endpoint subscribes to, comma separated or repeated (sets rules.event_types). Omitting the subscription means every event type except the opt-in sent_email.* events, which an endpoint receives only when it lists them; sent_email.* expands to all four. Keep email.received in the list if the endpoint also handles inbound mail. On update, the endpoint's other rules are kept.";
47648
- const SENT_EMAIL_WILDCARD = "sent_email.*";
49099
+ const EVENT_TYPES_FLAG_DESCRIPTION = "Event types this endpoint subscribes to, comma separated or repeated (sets rules.event_types). Omitting the subscription means every event type except the opt-in sent_email.* and email_rule_request.* events, which an endpoint receives only when it lists them; sent_email.* and email_rule_request.* each expand to all four. Rule request events can be listed only when the webhook is created, signed in as yourself; such a webhook is yours and always a route target. Keep email.received in the list if the endpoint also handles inbound mail. On update, the endpoint's other rules are kept.";
49100
+ /** Shorthands the CLI expands, since the server would store them as literals that match nothing. */
49101
+ const WILDCARDS = {
49102
+ "sent_email.*": SENT_EMAIL_EVENT_TYPES,
49103
+ "email_rule_request.*": EMAIL_RULE_REQUEST_EVENT_TYPES
49104
+ };
47649
49105
  const KNOWN_EVENT_TYPES = new Set(WEBHOOK_EVENT_TYPES);
47650
49106
  var EventTypesFlagError = class extends Error {};
47651
49107
  /**
47652
49108
  * Turn the raw `--event-types` values into the list to store: split on
47653
- * commas, trim, expand `sent_email.*`, and drop duplicates while keeping the
49109
+ * commas, trim, expand `sent_email.*` and `email_rule_request.*`, and drop duplicates while keeping the
47654
49110
  * order the caller gave.
47655
49111
  */
47656
49112
  function parseEventTypesFlag(values) {
@@ -47659,14 +49115,14 @@ function parseEventTypesFlag(values) {
47659
49115
  for (const value of values) for (const raw of value.split(",")) {
47660
49116
  const name = raw.trim();
47661
49117
  if (name === "") continue;
47662
- const names = name === SENT_EMAIL_WILDCARD ? [...SENT_EMAIL_EVENT_TYPES] : [name];
49118
+ const names = Object.hasOwn(WILDCARDS, name) ? [...WILDCARDS[name] ?? []] : [name];
47663
49119
  for (const entry of names) {
47664
49120
  if (seen.has(entry)) continue;
47665
49121
  seen.add(entry);
47666
49122
  out.push(entry);
47667
49123
  }
47668
49124
  }
47669
- if (out.length === 0) throw new EventTypesFlagError("--event-types needs at least one event type. To receive every event type except the opt-in sent_email.* events, omit the subscription instead.");
49125
+ if (out.length === 0) throw new EventTypesFlagError("--event-types needs at least one event type. To receive every event type except the opt-in sent_email.* and email_rule_request.* events, omit the subscription instead.");
47670
49126
  if (out.length > 50) throw new EventTypesFlagError(`--event-types accepts at most 50 event types (got ${out.length}).`);
47671
49127
  return out;
47672
49128
  }
@@ -47975,6 +49431,92 @@ function formatAlreadySentNotice(sent, options = {}) {
47975
49431
  if (deliveryStatus && deliveryStatus !== status) details.push(`delivery status ${deliveryStatus}`);
47976
49432
  return `Already sent: this exact message went out earlier${details.length > 0 ? ` (${details.join(", ")})` : ""}. Nothing new was sent.`;
47977
49433
  }
49434
+ // src/oclif/member-role-mail.ts
49435
+ /**
49436
+ * Why an invitation for Member was refused, for the refusal the server gives
49437
+ * when every Member reads all of the organization's mail: offering Member is
49438
+ * then giving that, which only an invitation sent from the dashboard by
49439
+ * someone who can assign roles may do.
49440
+ */
49441
+ const MEMBER_INVITE_READS_ALL_MAIL = "In this organization every Member reads all of its mail, so inviting someone as Member gives them every mailbox. Only an invitation sent from the dashboard itself, by someone who can assign roles, can offer that; a CLI request cannot, even with a step-up token. From the CLI you can invite them with --role none, where the organization can give limited roles, and once they have joined give them a role with `primitive roles-and-members set-member-role`, which needs roles:assign and an approval in the browser. Or invite them from the dashboard's Roles settings, which says what each role reads.";
49442
+ /** The same refusal for any role above Member, which is assigning a role: the same advice, without the mail clause. */
49443
+ const ROLE_INVITE_NEEDS_DASHBOARD = "Inviting someone with a role other than Member or none is assigning that role, and only an invitation sent from the dashboard itself, by someone who can assign roles, can do it; a CLI request cannot, even with a step-up token. From the CLI you can invite them with --role none, where the organization can give limited roles, and once they have joined give them the role with `primitive roles-and-members set-member-role`, which needs roles:assign and an approval in the browser. Or invite them from the dashboard's Roles settings.";
49444
+ /**
49445
+ * The hint for a refused invite-organization-member, or undefined when it is
49446
+ * not that refusal. That one carries no details; a refusal for a grant that
49447
+ * lacks a permission names it in its details, and is left to its own hint.
49448
+ */
49449
+ function inviteRefusalHint(code, body, payload) {
49450
+ if (code !== "grant_forbidden") return void 0;
49451
+ const details = payload && typeof payload === "object" ? payload.details : void 0;
49452
+ if (details && typeof details === "object" && Object.keys(details).length > 0) return void 0;
49453
+ const role = body && typeof body === "object" ? body.role : void 0;
49454
+ if (role === void 0 || role === "member") return MEMBER_INVITE_READS_ALL_MAIL;
49455
+ return role === "none" ? void 0 : ROLE_INVITE_NEEDS_DASHBOARD;
49456
+ }
49457
+ /**
49458
+ * Control characters (C0, DEL and C1), line and paragraph separators and
49459
+ * format characters, which could fake a line of output: a role's name is free
49460
+ * text. Built from code points, as commands/roles.ts builds its pattern, so the
49461
+ * source holds no control character.
49462
+ */
49463
+ const UNPRINTABLE = new RegExp(`[${String.fromCharCode(0)}-${String.fromCharCode(31)}${String.fromCharCode(127)}-${String.fromCharCode(159)}${String.fromCharCode(8232)}${String.fromCharCode(8233)}\\p{Cf}]`, "gu");
49464
+ /**
49465
+ * The warning for giving someone `role`, from GET /roles, or null when it
49466
+ * reads less or cannot be told. A role, or a template the organization has
49467
+ * not copied yet, reads every mailbox when it holds mail:read over the whole
49468
+ * organization and the organization gives such roles all of its mail (whole-
49469
+ * organization access, which the dashboard also assumes when the answer does
49470
+ * not say).
49471
+ */
49472
+ function roleReadsAllMailWarning(role, listed) {
49473
+ if (!listed || typeof listed !== "object") return null;
49474
+ const data = listed;
49475
+ if ((data.invitations && typeof data.invitations === "object" ? data.invitations : null)?.members_read_all_mail === false) return null;
49476
+ const byKey = (value) => value === role;
49477
+ const byId = (value) => typeof value === "string" && value.toLowerCase() === role.toLowerCase();
49478
+ const roles = Array.isArray(data.roles) ? data.roles : [];
49479
+ const templates = Array.isArray(data.templates) ? data.templates : [];
49480
+ const found = roles.find((r) => byKey(r.key) || byId(r.id)) ?? templates.find((t) => byKey(t.key));
49481
+ const readsMail = Array.isArray(found?.permissions) && found.permissions.includes("mail:read");
49482
+ if (!found || (found.mail_scope ?? "org") !== "org" || !readsMail) return null;
49483
+ return `Warning: the role "${Array.from((typeof found.name === "string" ? found.name : "").replace(UNPRINTABLE, " ").replace(/\s+/g, " ").trim()).slice(0, 80).join("").trim() || role}" reads all of this organization's mail while this organization uses whole-organization access, so the person given it reads every mailbox. For only their own mail, use a narrower role: \`primitive roles list --json\` shows each role's mail_scope.`;
49484
+ }
49485
+ /** How long the warning waits for GET /roles before leaving it out. */
49486
+ const ROLES_TIMEOUT_MS = 3e3;
49487
+ /**
49488
+ * GET /roles, or null when it cannot be read in time: the warning is then
49489
+ * left out rather than guessed. The signal stops the request; the race bounds
49490
+ * everything around it too, such as a saved login's token exchange on a 401.
49491
+ */
49492
+ async function readRoles(client, timeoutMs = ROLES_TIMEOUT_MS) {
49493
+ let timer;
49494
+ const late = new Promise((resolve) => {
49495
+ timer = setTimeout(() => resolve(null), timeoutMs);
49496
+ });
49497
+ try {
49498
+ return await Promise.race([fetchRoles(client, timeoutMs), late]);
49499
+ } finally {
49500
+ clearTimeout(timer);
49501
+ }
49502
+ }
49503
+ async function fetchRoles(client, timeoutMs) {
49504
+ try {
49505
+ const result = await client.get({
49506
+ responseStyle: "fields",
49507
+ security: [{
49508
+ scheme: "bearer",
49509
+ type: "http"
49510
+ }],
49511
+ signal: AbortSignal.timeout(timeoutMs),
49512
+ url: "/roles"
49513
+ });
49514
+ if (result.error) return null;
49515
+ return result.data?.data ?? null;
49516
+ } catch {
49517
+ return null;
49518
+ }
49519
+ }
47978
49520
  // src/oclif/runtime-session.ts
47979
49521
  /**
47980
49522
  * The variables each runtime sets to its exact session ID, first match wins.
@@ -48558,7 +50100,6 @@ function serializeErrorPayload(payload) {
48558
50100
  * outcome is unknown.
48559
50101
  */
48560
50102
  function buildThrownSendFailureEnvelope(params) {
48561
- const message = params.error instanceof Error ? params.error.message : String(params.error);
48562
50103
  const outcome = params.requestStarted ? "uncertain" : "not_sent";
48563
50104
  return {
48564
50105
  outcome,
@@ -48570,7 +50111,7 @@ function buildThrownSendFailureEnvelope(params) {
48570
50111
  }),
48571
50112
  sent: null,
48572
50113
  http_status: null,
48573
- error: { message },
50114
+ error: thrownErrorBody(params.error),
48574
50115
  follow_up_commands: params.requestStarted && params.attemptStartedAtIso ? buildSendCommandFollowUps({
48575
50116
  attemptStartedAtIso: params.attemptStartedAtIso,
48576
50117
  idempotencyKey: params.idempotencyKey,
@@ -48595,7 +50136,7 @@ const HINTS = /* @__PURE__ */ new Set([
48595
50136
  /** A printable `<protocol>/<version>`; anything else is treated as unknown. */
48596
50137
  const KIND = /^[a-z][a-z0-9._-]{0,63}\/[1-9][0-9]{0,3}$/;
48597
50138
  const UUID$2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
48598
- function record$1(value) {
50139
+ function record$2(value) {
48599
50140
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
48600
50141
  }
48601
50142
  function categoryOf(hint, kind, fyi, candidate) {
@@ -48628,12 +50169,12 @@ function replyExpectation(category) {
48628
50169
  * treated as `none`, as the API documents.
48629
50170
  */
48630
50171
  function readEmailInteraction(detail) {
48631
- const row = record$1(detail);
50172
+ const row = record$2(detail);
48632
50173
  if (!row || typeof row.interaction_hint !== "string") return null;
48633
50174
  const hint = HINTS.has(row.interaction_hint) ? row.interaction_hint : "none";
48634
50175
  const rawKind = row.interaction_kind;
48635
50176
  const kind = (hint === "card" || hint === "status") && typeof rawKind === "string" && KIND.test(rawKind) ? rawKind : null;
48636
- const fyi = row.fyi === true || record$1(row.collaboration)?.fyi === true;
50177
+ const fyi = row.fyi === true || record$2(row.collaboration)?.fyi === true;
48637
50178
  const category = categoryOf(hint, kind, fyi, row.interaction_candidate === true);
48638
50179
  const expectation = replyExpectation(category);
48639
50180
  return {
@@ -49197,14 +50738,14 @@ function bareAddress$1(value) {
49197
50738
  const address = (/<([^<>\s]+@[^<>\s]+)>\s*$/.exec(value)?.[1] ?? value).trim().toLowerCase();
49198
50739
  return /^[^\s<>@]+@[^\s<>@]+$/.test(address) ? address : null;
49199
50740
  }
49200
- function record(value) {
50741
+ function record$1(value) {
49201
50742
  return value && typeof value === "object" && !Array.isArray(value) ? value : null;
49202
50743
  }
49203
50744
  function newerRows(value) {
49204
50745
  if (!Array.isArray(value)) return void 0;
49205
50746
  const rows = [];
49206
50747
  for (const item of value.slice(-20)) {
49207
- const row = record(item);
50748
+ const row = record$1(item);
49208
50749
  const from = bareAddress$1(row?.from);
49209
50750
  if (!row || typeof row.id !== "string" || !UUID.test(row.id) || !from || typeof row.received_at !== "string" || !Number.isFinite(Date.parse(row.received_at))) return void 0;
49210
50751
  rows.push({
@@ -49240,10 +50781,10 @@ async function readThreadContext(client, threadId, afterEmailId, signal) {
49240
50781
  result = await read();
49241
50782
  }
49242
50783
  if (result.error !== void 0) return null;
49243
- const body = record(result.data);
49244
- const data = record(body?.data);
50784
+ const body = record$1(result.data);
50785
+ const data = record$1(body?.data);
49245
50786
  if (body?.success !== true || !data || !Array.isArray(data.messages)) return null;
49246
- const messages = data.messages.filter((message) => Boolean(record(message)) && typeof message.id === "string" && ["inbound", "outbound"].includes(message.direction));
50787
+ const messages = data.messages.filter((message) => Boolean(record$1(message)) && typeof message.id === "string" && ["inbound", "outbound"].includes(message.direction));
49247
50788
  const count = data.newer_inbound_count;
49248
50789
  const newerInboundCount = typeof count === "number" && Number.isSafeInteger(count) && count >= 0 ? count : void 0;
49249
50790
  const newerInbound = newerInboundCount === void 0 ? void 0 : newerRows(data.newer_inbound);
@@ -49307,7 +50848,7 @@ const SERVER_RELATIONSHIPS = {
49307
50848
  };
49308
50849
  /** The `collaboration` object of an email read, when the server sends one. */
49309
50850
  function collaborationOf(detail) {
49310
- return record(record(detail)?.collaboration);
50851
+ return record$1(record$1(detail)?.collaboration);
49311
50852
  }
49312
50853
  /**
49313
50854
  * The sender relationship the server computed for this email
@@ -49327,7 +50868,7 @@ function serverRelationship(detail) {
49327
50868
  * older server sends neither and nothing is muted by it.
49328
50869
  */
49329
50870
  function serverMuted(detail) {
49330
- return collaborationOf(detail)?.muted === true || record(detail)?.muted === true;
50871
+ return collaborationOf(detail)?.muted === true || record$1(detail)?.muted === true;
49331
50872
  }
49332
50873
  /**
49333
50874
  * An address fit to print as trusted metadata, or `unavailable`. The
@@ -49463,7 +51004,7 @@ function savedConnectedProfiles(configDir, exclude) {
49463
51004
  */
49464
51005
  async function otherProfiles(configDir, session, current) {
49465
51006
  if (session) try {
49466
- const { connectedProfilesForSession } = await import("./machine-session-BIOGBT9g.js").then((n) => n.s);
51007
+ const { connectedProfilesForSession } = await import("./machine-session-YQWxPePp.js").then((n) => n.s);
49467
51008
  return {
49468
51009
  inSession: true,
49469
51010
  profiles: connectedProfilesForSession(configDir, session, current)
@@ -49537,6 +51078,56 @@ async function explainPendingMailNotFound(configDir, emailId, write = (line) =>
49537
51078
  if (outcome.kind === "dropped") write(`Dropped the pending notice for ${id} from profile ${current.profile} after ${outcome.reads} consecutive not_found reads; the email is no longer visible to this profile.\n`);
49538
51079
  } catch {}
49539
51080
  }
51081
+ const RULE_REQUEST_WEBHOOK_CREATE_HINT = "Hint: a webhook for rule request events is made by the person it is for, signed in as themself: after `primitive login`, run this without --api-key, PRIMITIVE_API_KEY or PRIMITIVE_AGENT_PROFILE set, or create it in the dashboard. An API key, another app or an agent cannot make one. If you are already signed in as yourself, try again. It is a route target whatever your access, so it never takes a domain's or the organization's default slot.";
51082
+ const RULE_REQUEST_WEBHOOK_HTTP_HINT = "Hint: rule request events go only to an http webhook (kind http).";
51083
+ const RULE_REQUEST_WEBHOOK_SUBSCRIBE_HINT = "Hint: rule request events go only to a webhook made for them. If this webhook does not list them yet, it cannot be given them: create a new one with `--event-types 'email_rule_request.*'`, signed in as yourself. If it does, only its maker, signed in as themself, can change it.";
51084
+ const RULE_REQUEST_WEBHOOK_KEY_HINT = "Hint: a webhook for rule request events is made by the person it is for, signed in as themself, and stays theirs: it cannot be given to an API key.";
51085
+ const RULE_REQUEST_WEBHOOK_CHANGE_HINT = "Hint: only the person who made a webhook for rule request events, signed in as themself (not with an API key, another app or an agent), can change it. It can be deleted as any other webhook can.";
51086
+ const RULE_REQUEST_WEBHOOK_DEFAULT_HINT = "Hint: a webhook for rule request events is always a route target. Leave out --no-is-route-target, --takeover and --no-takeover (is_route_target: false and takeover in a raw body).";
51087
+ const RULE_REQUEST_EVENT_PREFIX = "email_rule_request.";
51088
+ function record(value) {
51089
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
51090
+ }
51091
+ /**
51092
+ * Whether a create or update body lists any rule request event. Matched by
51093
+ * prefix, as the API matches it, so a raw body's literal
51094
+ * `email_rule_request.*` counts too.
51095
+ */
51096
+ function listsRuleRequestEvents(body) {
51097
+ const types = record(record(body)?.rules)?.event_types;
51098
+ return Array.isArray(types) && types.some((t) => typeof t === "string" && t.startsWith(RULE_REQUEST_EVENT_PREFIX));
51099
+ }
51100
+ function refusalReason(payload) {
51101
+ const top = record(payload);
51102
+ return record(top?.details)?.reason ?? record(record(top?.error)?.details)?.reason;
51103
+ }
51104
+ /**
51105
+ * The hint after a refused createEndpoint, or undefined when none applies.
51106
+ * The one refusal reason covers a webhook given to a key, one that is not an
51107
+ * http webhook, and one made by anyone but a person signed in as themself (or
51108
+ * whose membership could not be confirmed), told apart by what was sent.
51109
+ */
51110
+ function createEndpointRuleRequestHint(code, body, payload) {
51111
+ const asked = record(body);
51112
+ if (refusalReason(payload) === "rule_request_events_need_own_webhook") {
51113
+ if (asked && "owner_key_id" in asked) return RULE_REQUEST_WEBHOOK_KEY_HINT;
51114
+ if (asked?.kind !== void 0 && asked.kind !== "http") return RULE_REQUEST_WEBHOOK_HTTP_HINT;
51115
+ return RULE_REQUEST_WEBHOOK_CREATE_HINT;
51116
+ }
51117
+ if (code === "validation_error" && listsRuleRequestEvents(body) && (asked?.is_route_target === false || asked?.takeover !== void 0)) return RULE_REQUEST_WEBHOOK_DEFAULT_HINT;
51118
+ }
51119
+ /**
51120
+ * The hint after a refused updateEndpoint, or undefined when none applies.
51121
+ * The one refusal reason covers three changes, told apart by what was sent:
51122
+ * the events added to an existing webhook, the webhook given to a key, and
51123
+ * any other change by someone who is not its maker acting as themself.
51124
+ */
51125
+ function updateEndpointRuleRequestHint(_code, body, payload) {
51126
+ if (refusalReason(payload) !== "rule_request_events_need_own_webhook") return void 0;
51127
+ const asked = record(body);
51128
+ if (asked && "owner_key_id" in asked) return RULE_REQUEST_WEBHOOK_KEY_HINT;
51129
+ return listsRuleRequestEvents(body) ? RULE_REQUEST_WEBHOOK_SUBSCRIBE_HINT : RULE_REQUEST_WEBHOOK_CHANGE_HINT;
51130
+ }
49540
51131
  // src/oclif/sent-list-labels.ts
49541
51132
  /**
49542
51133
  * `sent list` under a connected agent profile returns this address's own
@@ -50232,7 +51823,7 @@ function forbiddenHint(payload) {
50232
51823
  if (!permission || record.reason !== "permission_missing") return null;
50233
51824
  return `Hint: This operation needs ${permission}, which this credential does not have. Some operations stay closed to keys and the CLI even with it. Check a credential with \`primitive whoami --check ${permission}\`.`;
50234
51825
  }
50235
- function writeErrorWithHints(payload, response) {
51826
+ function writeErrorWithHints(payload, response, options = {}) {
50236
51827
  const formatted = formatErrorPayload(payload);
50237
51828
  process.stderr.write(`${formatted}\n`);
50238
51829
  for (const line of cliCommandLines(formatted)) process.stderr.write(`${line}\n`);
@@ -50241,7 +51832,7 @@ function writeErrorWithHints(payload, response) {
50241
51832
  process.stderr.write(`${rateLimitHint(response)}\n`);
50242
51833
  return;
50243
51834
  }
50244
- if (!code) return;
51835
+ if (!code || options.codeHint === false) return;
50245
51836
  if (code === "grant_forbidden") {
50246
51837
  process.stderr.write(`${grantForbiddenHint(payload)}\n`);
50247
51838
  return;
@@ -50412,6 +52003,7 @@ function buildFlags(operation) {
50412
52003
  description: ATTACHMENT_WAIT_FLAG_DESCRIPTION
50413
52004
  });
50414
52005
  const extension = OPERATION_EXTENSIONS[operation.sdkName];
52006
+ if (extension?.confirm && !("yes" in extension.flags)) throw new Error(`${operation.sdkName}: a CLI extension that asks for confirmation must add --yes`);
50415
52007
  for (const [name, flag] of Object.entries(extension?.flags ?? {})) {
50416
52008
  if (flags[name] !== void 0) throw new Error(`${operation.sdkName}: --${name} is both a generated flag and a CLI extension flag`);
50417
52009
  flags[name] = flag;
@@ -50485,6 +52077,7 @@ function domainVerificationFailedHint(payload) {
50485
52077
  ].join("\n");
50486
52078
  }
50487
52079
  const OPERATION_HINTS = {
52080
+ deleteEmailRule: "Asks for confirmation first in a terminal. Pass --yes (-y) to delete without a prompt; without a terminal, or with --json, it is required.",
50488
52081
  addDomain: "Tip: after this returns a domain id, run `primitive domains zone-file --id <domain-id> --output <domain>.zone` when the user wants an importable DNS zone file.",
50489
52082
  verifyDomain: "Tip: if DNS is still missing, run `primitive domains zone-file --id <domain-id> --output <domain>.zone` to give the user an importable DNS zone file.",
50490
52083
  downloadDomainZoneFile: "Tip: prefer `primitive domains zone-file --id <domain-id> --output <domain>.zone` for CLI-friendly file output.",
@@ -50500,7 +52093,7 @@ const OPERATION_HINTS = {
50500
52093
  deleteMemory: "Tip: prefer `primitive memories delete <key> [--function <function-id>]` for the common delete path.",
50501
52094
  searchMemories: "Tip: prefer `primitive memories search [prefix] [--metadata-only] [--function <function-id>]` for the common search path.",
50502
52095
  startAgentSignup: "Tip: pass --terms-accepted. Capture the signup_token from the response and feed it to `primitive agent verify-agent-signup --signup-token <token> --verification-code <6-digit-code>` (the verify flag accepts --code as an alias). The high-level `primitive signup <email>` command walks an interactive user through both steps with friendlier prompts.",
50503
- testEmailRule: "Tip: --expect-match and --expect-no-match (or --expect-file) set expect, the emails the rule must match and must not. When no expect is sent, by these flags or in the body, a saved rule's saved expectations are judged instead. The exit code follows whichever expectations the answer judged: 1 when one failed; 1 when one is undecided and testing again will not decide it (its email cannot be found or read, cannot be classified, or cannot be tested: skipped); 3 when the only undecided ones can be decided later (not classified yet, or a question unanswered), so test again later. Exit 1 also means the request failed. With --json, error.code tells these apart: expectations_failed, expectations_unresolvable, expectations_undecided (exit 3), or the API's own code. Without --json a readable report follows the JSON on stderr: warnings, how each condition held, the expectations, and the values each email that did not match was compared on.",
52096
+ testEmailRule: "Tip: --expect-match and --expect-no-match (or --expect-file) set expect, the emails the rule must match and must not. When no expect is sent, by these flags or in the body, a saved rule's saved expectations are judged instead. Either way, the answer reports the expectations it judged in expectations: passed, counts and per-email results. (create-email-rule and update-email-rule instead judge the rule's saved expectations on the version they save, in expectations_check: passed, decided, failed and undecided.) The exit code follows that report: 1 when one failed; 1 when one is undecided and testing again will not decide it (its email cannot be found or read, cannot be classified, or cannot be tested: skipped); 3 when the only undecided ones can be decided later (not classified yet, or a question unanswered), so test again later. Exit 1 also means the request failed. With --json, error.code tells these apart: expectations_failed, expectations_unresolvable, expectations_undecided (exit 3), or the API's own code. Without --json a readable report follows the JSON on stderr: warnings, how each condition held, the expectations, and the values each email that did not match was compared on.",
50504
52097
  verifyAgentSignup: "Tip: pass --verification-code <code> (or --code; both work). The response carries OAuth tokens but not your assigned inbox domain; run `primitive domains list` (or `primitive whoami`) after success to see the managed *.primitive.email address that routes to this account."
50505
52098
  };
50506
52099
  const OPERATION_FLAG_ALIASES = { verifyAgentSignup: { verification_code: ["code"] } };
@@ -50515,7 +52108,10 @@ const OPERATION_EXTENSIONS = {
50515
52108
  const MAIL_RELAY_NOT_ENABLED_MESSAGE = "Mail relay is not enabled for this account, so the domain was not added. Retry without --mail-relay to point the domain's MX at Primitive directly, or ask Primitive to enable mail relay for your account.";
50516
52109
  const MAIL_RELAY_UNAVAILABLE_MESSAGE = "No mail relay is available on this API server right now, so the domain was not added. Retry later, or retry without --mail-relay to point the domain's MX at Primitive directly.";
50517
52110
  const OPERATION_ERROR_HINTS = {
50518
- updateEmailRule: (code, _body, payload, path) => code === "email_rule_conflict" ? versionMovedHint(payload, path?.id) : void 0,
52111
+ inviteOrganizationMember: (code, body, payload) => inviteRefusalHint(code, body, payload),
52112
+ createEndpoint: (code, body, payload) => createEndpointRuleRequestHint(code, body, payload),
52113
+ updateEndpoint: (code, body, payload) => updateEndpointRuleRequestHint(code, body, payload),
52114
+ updateEmailRule: (code, body, payload, path) => updateEmailRuleErrorHint(code, payload, path?.id, body),
50519
52115
  verifyDomain: (code, _body, payload) => code === "dns_verification_failed" ? domainVerificationFailedHint(payload) : void 0,
50520
52116
  addDomain: (code, body) => {
50521
52117
  if (code === "mail_relay_unavailable") return MAIL_RELAY_UNAVAILABLE_MESSAGE;
@@ -50523,6 +52119,7 @@ const OPERATION_ERROR_HINTS = {
50523
52119
  if (code === "feature_disabled" && mailRelay) return MAIL_RELAY_NOT_ENABLED_MESSAGE;
50524
52120
  }
50525
52121
  };
52122
+ const OPERATION_ERROR_HINTS_REPLACING_CODE_HINT = /* @__PURE__ */ new Set(["inviteOrganizationMember"]);
50526
52123
  const OPERATION_SUCCESS_HOOKS = { verifyAgentSignup: ({ envelope, configDir, apiBaseUrl, writeStderr }) => {
50527
52124
  const data = envelope?.data;
50528
52125
  if (!data?.access_token || !data?.refresh_token) return;
@@ -50566,6 +52163,16 @@ function createOperationCommand(operation) {
50566
52163
  if (value === void 0 || value === "") throw new Errors.CLIError(`Missing the ${positionalFlag}: pass it as the first argument or with --${positionalFlag}.`, { exit: 2 });
50567
52164
  parsedFlags[positionalFlag] = value;
50568
52165
  }
52166
+ const confirm = extension?.confirm;
52167
+ if (confirm && parsedFlags.yes !== true) {
52168
+ if (parsedFlags.json === true || !(process.stdin.isTTY && process.stderr.isTTY)) throw new Errors.CLIError(confirm.noTerminal, { exit: 2 });
52169
+ const answer = await promptLine(confirm.question(parsedFlags));
52170
+ if (!/^y(es)?$/i.test(answer.trim())) {
52171
+ process.stderr.write(`${confirm.cancelled}\n`);
52172
+ process.exitCode = 1;
52173
+ return;
52174
+ }
52175
+ }
50569
52176
  const headers = collectHeaderFlags(parsedFlags, headerFlagToName);
50570
52177
  if ([
50571
52178
  "startAgentSignup",
@@ -50693,6 +52300,11 @@ function createOperationCommand(operation) {
50693
52300
  });
50694
52301
  }
50695
52302
  };
52303
+ if (operation.sdkName === "setMemberRole") {
52304
+ const role = body && typeof body === "object" ? body.role : void 0;
52305
+ const warning = typeof role === "string" ? roleReadsAllMailWarning(role, await readRoles(apiClient.client)) : null;
52306
+ if (warning) process.stderr.write(`${warning}\n`);
52307
+ }
50696
52308
  const result = paged ? await callPaged() : takesStepUp(operation) ? await callWithApproval() : ATTACHMENT_WAIT_OPERATIONS.has(operation.sdkName) ? await withAttachmentWait(callOperation, {
50697
52309
  wait: parsedFlags.wait !== false,
50698
52310
  errorCode: (error) => extractErrorCode(extractErrorPayload(error)),
@@ -50702,8 +52314,8 @@ function createOperationCommand(operation) {
50702
52314
  }) : await callOperation();
50703
52315
  if (result.error) {
50704
52316
  const errorPayload = extractErrorPayload(result.error);
50705
- writeErrorWithHints(errorPayload, result.response);
50706
52317
  const operationErrorHint = OPERATION_ERROR_HINTS[operation.sdkName]?.(extractErrorCode(errorPayload), body, errorPayload, collectValues(operation.pathParams, parsedFlags, operation.sdkName));
52318
+ writeErrorWithHints(errorPayload, result.response, { codeHint: !(operationErrorHint && OPERATION_ERROR_HINTS_REPLACING_CODE_HINT.has(operation.sdkName)) });
50707
52319
  if (operationErrorHint) process.stderr.write(`${operationErrorHint}\n`);
50708
52320
  if (stepUpUrl) process.stderr.write(approveThenRerunHint(stepUpUrl));
50709
52321
  if (replyStateSurface && isAwaitingRejectedError(errorPayload)) process.stderr.write(`${awaitingRejectedError(replyStateSurface).message}\n`);
@@ -50729,7 +52341,7 @@ function createOperationCommand(operation) {
50729
52341
  }
50730
52342
  });
50731
52343
  if (operation.sdkName === "getEmail" && extractErrorCode(errorPayload) === "not_found" && typeof parsedFlags.id === "string") await explainPendingMailNotFound(this.config.configDir, parsedFlags.id);
50732
- process.exitCode = 1;
52344
+ process.exitCode = extension?.errorExitCode?.(errorPayload) || 1;
50733
52345
  return;
50734
52346
  }
50735
52347
  if (operation.binaryResponse) {
@@ -50779,11 +52391,17 @@ function createOperationCommand(operation) {
50779
52391
  if (cursor && !jsonOutput) process.stderr.write(`next cursor: ${cursor}\n`);
50780
52392
  if (pagingNote && !jsonOutput) process.stderr.write(`${pagingNote}\n`);
50781
52393
  const summary = [];
50782
- if (Array.isArray(envelope?.data) && envelope.data.length === 0) {
50783
- const hint = EMPTY_RESULT_HINTS[operation.sdkName];
50784
- if (hint && jsonOutput) {
50785
- if (withEnvelope) summary.push(hint);
50786
- } else if (hint) process.stderr.write(`${hint}\n`);
52394
+ if (Array.isArray(envelope?.data) && envelope.data.length === 0 && (!jsonOutput || withEnvelope)) {
52395
+ const tail = query.since !== void 0;
52396
+ const more = envelope?.meta?.cursor ?? null;
52397
+ const hint = operation.sdkName === "listEmails" ? emptyEmailListHint({
52398
+ filterFlags: listFilters(query).map((name) => `--${parameterFlagName(operation.sdkName, name)}`),
52399
+ ...tail ? { pastFlag: "--since" } : query.cursor !== void 0 ? { pastFlag: "--cursor" } : {},
52400
+ nextCursor: tail ? null : more,
52401
+ caller: tail || more ? null : await readCallerMailFor(auth, apiClient.client)
52402
+ }) : EMPTY_RESULT_HINTS[operation.sdkName];
52403
+ if (hint && jsonOutput) summary.push(hint);
52404
+ else if (hint) process.stderr.write(`${hint}\n`);
50787
52405
  }
50788
52406
  if (operation.sdkName === "listSentEmails") {
50789
52407
  const note = sentListOtherSendersNote(envelope?.data, auth.connectedAgent?.agentAddress);
@@ -50854,7 +52472,6 @@ function createOperationCommand(operation) {
50854
52472
  const EMPTY_RESULT_HINTS = {
50855
52473
  listDeliveries: "(no results) No webhook deliveries logged yet. If you have an endpoint configured but expected to see test fires here: test deliveries from `primitive endpoints test` are NOT logged in this list, they're synchronous and visible only in the test-endpoint command's response. Real deliveries are logged when an event fans out to your endpoints: an inbound `email.received`, or a `sent_email.*` event for an endpoint that lists it in rules.event_types (they arrive within a few minutes of the result). If you have no endpoints, run `primitive endpoints list` to check.",
50856
52474
  listEndpoints: "(no results) No webhook endpoints configured. Add one with `primitive endpoints create --url <your-url>`.",
50857
- listEmails: "(no results) No inbound emails received yet on this account. Send one to a verified domain to populate this list. For a compact view, prefer `primitive emails latest`.",
50858
52475
  listDomains: "(no results) No domains on this account. Add one with `primitive domains add --domain <yourdomain.example>`.",
50859
52476
  listFilters: "(no results) No filter rules configured.",
50860
52477
  listFunctions: "(no results) No Functions configured yet. Start with `primitive functions templates`, then `primitive functions init --template <template>` and `primitive functions deploy --name <name> --file <bundle>`."
@@ -50862,4 +52479,4 @@ const EMPTY_RESULT_HINTS = {
50862
52479
  function canonicalizeCliReferences(description) {
50863
52480
  return description.replaceAll("`primitive emails:latest`", "`primitive emails latest`").replaceAll("`primitive describe emails:get-email | jq '.responseSchema.properties'`", "`primitive describe emails:get | jq '.responseSchema.properties'`");
50864
52481
  }
50865
- export { replyExpectation as $, refuseOrgSelectionForProfile as $t, bareAddress$1 as A, connectNextSteps as At, wakeReadCommand as B, withFlagSuggestion as Bt, terminalApprovalIo as C, sendWaitClampNotice as Ct, listenForCopyHotkey as D, thrownErrorExitCode as Dt, formatBrowserLoginInstructions as E, successfulSendOutcome as Et, readThreadContext as F, currentMailSessionKey as Ft, clearReadPendingMail as G, ATTACHMENT_WAIT_FLAG_DESCRIPTION as Gt, wakeRelationship as H, classificationOf as Ht, sentInThread as I, detectAgentRuntime as It, recordPendingMail as J, ConnectedAgentKeyRequiredError as Jt, pendingMailPath as K, ATTACHMENT_WAIT_MAX_MS as Kt, serverMuted as L, detectRuntimeSession as Lt, displayAddress as M, generatedAgentName as Mt, formatMailWakeLine as N, identitySuggestions as Nt, openBrowser as O, DEFAULT_ENROLL_NAME as Ot, latestOwnOutbound as P, loadSkillLine as Pt, readEmailInteraction as Q, refreshStoredCliCredentials as Qt, serverRelationship as R, runtimeSessionVariableList as Rt, promptLine as S, sendOutcomeExitCode as St, canOfferCopyHotkey as T, serializeErrorPayload as Tt, claimPendingMail as U, isEmailId as Ut, wakeRecipientField as V, classificationFactLines as Vt, clearConsumedPendingMail as W, noFactsMessage as Wt, interactionHumanLine as X, createAuthenticatedCliApiClient as Xt, removePendingMail as Y, SAVED_CLI_OAUTH_SESSION_EXPIRED_MESSAGE as Yt, interactionNextActions as Z, createCliApiClient as Zt, writeErrorWithHints as _, formatSendFailureSummary as _t, createOperationCommand as a, revokeUserLogin as an, apiResultHttpStatus as at, STEP_UP_TOKEN_FLAG_DESCRIPTION as b, reportSendCommandResult as bt, formatErrorPayload as c, sanitizeForTerminal as cn, buildThrownSendFailureEnvelope as ct, isGrantRefusal as d, deriveSendIdempotencyKey as dt, resolveCliApiRequestConfig as en, replyInteractionWarning as et, parameterFlagName as f, excludeSignalReplies as ft, takesStepUp as g, formatPriorRepliesWarning as gt, surfaceUnauthorizedHint as h, formatPriorRepliesCheckSkipped as ht, TIME_FLAG_DESCRIPTION as i, fetchCliOrgs as in, SEND_OUTCOME_HELP as it, describeWake as j, defaultConnectionName as jt, explainPendingMailNotFound as k, cliInvocation as kt, hasControlCharacter as l, setKnownCommands as ln, checkPriorReplies as lt, runWithTiming as m, formatDeletedEarlierSendNotice as mt, API_ERROR_CODES as n, CliOrgRefusedError as nn, IDEMPOTENCY_KEY_FLAG_DESCRIPTION as nt, extractErrorCode as o, switchCurrentOrg as on, assertValidIdempotencyKey as ot, readTextFileFlag as p, followUpCommandPrefix as pt, readPendingMail as q, withAttachmentWait as qt, OPERATION_PATH_FLAGS as r, credentialsFromUserLogin as rn, MAX_SEND_WAIT_TIMEOUT_MS as rt, extractErrorPayload as s, routeWarningLines as sn, buildFollowUpCommand as st, API_BASE_URL_FLAG_DESCRIPTION as t, selectedOrgFromEnv as tn, wakeInteractionSentence as tt, headerFlagName as u, operationManifest as un, classifySendError as ut, BROWSER_FLAG_DESCRIPTION as v, formatSendRecordFailureSummary as vt, withApproval as w, sentHistoryWindowStart as wt, approveThenRerunHint as x, sendIdentityFields as xt, STEP_UP_HEADER as y, priorRepliesThatWentOut as yt, skillFirstLine as z, formatAlreadySentNotice as zt };
52482
+ export { readEmailInteraction as $, SAVED_CLI_OAUTH_SESSION_EXPIRED_MESSAGE as $t, explainPendingMailNotFound as A, cliInvocation as At, skillFirstLine as B, formatAlreadySentNotice as Bt, promptLine as C, sendOutcomeExitCode as Ct, formatBrowserLoginInstructions as D, successfulSendOutcome as Dt, canOfferCopyHotkey as E, serializeErrorPayload as Et, latestOwnOutbound as F, loadSkillLine as Ft, clearConsumedPendingMail as G, classificationFactLines as Gt, wakeRecipientField as H, emptyEmailListHint as Ht, readThreadContext as I, currentMailSessionKey as It, readPendingMail as J, noFactsMessage as Jt, clearReadPendingMail as K, classificationOf as Kt, sentInThread as L, detectAgentRuntime as Lt, describeWake as M, defaultConnectionName as Mt, displayAddress as N, generatedAgentName as Nt, listenForCopyHotkey as O, thrownErrorExitCode as Ot, formatMailWakeLine as P, identitySuggestions as Pt, interactionNextActions as Q, ConnectedAgentKeyRequiredError as Qt, serverMuted as R, detectRuntimeSession as Rt, approveThenRerunHint as S, sendIdentityFields as St, withApproval as T, sentHistoryWindowStart as Tt, wakeRelationship as U, mailReach as Ut, wakeReadCommand as V, withFlagSuggestion as Vt, claimPendingMail as W, readCallerMailFor as Wt, removePendingMail as X, ATTACHMENT_WAIT_MAX_MS as Xt, recordPendingMail as Y, ATTACHMENT_WAIT_FLAG_DESCRIPTION as Yt, interactionHumanLine as Z, withAttachmentWait as Zt, takesStepUp as _, setKnownCommands as _n, formatPriorRepliesWarning as _t, createOperationCommand as a, selectedOrgFromEnv as an, SEND_OUTCOME_HELP as at, STEP_UP_HEADER as b, priorRepliesThatWentOut as bt, formatErrorPayload as c, credentialsFromUserLogin as cn, buildFollowUpCommand as ct, isGrantRefusal as d, revokeUserLogin as dn, classifySendError as dt, createAuthenticatedCliApiClient as en, replyExpectation as et, parameterFlagName as f, roleKeyLabel as fn, deriveSendIdempotencyKey as ft, surfaceUnauthorizedHint as g, sanitizeForTerminal as gn, formatPriorRepliesCheckSkipped as gt, runWithTiming as h, routeWarningLines as hn, formatDeletedEarlierSendNotice as ht, TIME_FLAG_DESCRIPTION as i, resolveCliApiRequestConfig as in, MAX_SEND_WAIT_TIMEOUT_MS as it, bareAddress$1 as j, connectNextSteps as jt, openBrowser as k, DEFAULT_ENROLL_NAME as kt, hasControlCharacter as l, fetchCliOrgs as ln, buildThrownSendFailureEnvelope as lt, refusalDetails as m, switchCurrentOrg as mn, followUpCommandPrefix as mt, API_ERROR_CODES as n, refreshStoredCliCredentials as nn, wakeInteractionSentence as nt, extractErrorCode as o, CliOrgRefusedError as on, apiResultHttpStatus as ot, readTextFileFlag as p, roleKeyText as pn, excludeSignalReplies as pt, pendingMailPath as q, isEmailId as qt, OPERATION_PATH_FLAGS as r, refuseOrgSelectionForProfile as rn, IDEMPOTENCY_KEY_FLAG_DESCRIPTION as rt, extractErrorPayload as s, RESTRICTED_ROLE_NOTE as sn, assertValidIdempotencyKey as st, API_BASE_URL_FLAG_DESCRIPTION as t, createCliApiClient as tn, replyInteractionWarning as tt, headerFlagName as u, isBuiltInRoleKey as un, checkPriorReplies as ut, writeErrorWithHints as v, operationManifest as vn, formatSendFailureSummary as vt, terminalApprovalIo as w, sendWaitClampNotice as wt, STEP_UP_TOKEN_FLAG_DESCRIPTION as x, reportSendCommandResult as xt, BROWSER_FLAG_DESCRIPTION as y, formatSendRecordFailureSummary as yt, serverRelationship as z, runtimeSessionVariableList as zt };