primcli 3.0.0-preview.5 → 3.0.0-preview.6

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,4 +1,4 @@
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-CP0h4alF.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
4
  import { d as readMailJson, h as writeMailJson, m as withMailLock, p as syncMailDirectory, u as privateMailPermissions } from "./shared-mail-files-BAQSGxxz.js";
@@ -663,7 +663,7 @@ const operationManifest = [
663
663
  "binaryResponse": false,
664
664
  "bodyRequired": false,
665
665
  "command": "whoami",
666
- "description": "Current authenticated identity. member_address is null for machine credentials or an unavailable member address.",
666
+ "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
667
  "hasJsonBody": false,
668
668
  "method": "GET",
669
669
  "operationId": "whoami",
@@ -721,6 +721,14 @@ const operationManifest = [
721
721
  "request_id": { "type": "string" },
722
722
  "auth_method": { "type": ["string", "null"] },
723
723
  "key_id": { "type": ["string", "null"] },
724
+ "email": {
725
+ "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.",
726
+ "type": ["string", "null"]
727
+ },
728
+ "role_name": {
729
+ "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.",
730
+ "type": ["string", "null"]
731
+ },
724
732
  "grant": {
725
733
  "type": ["object", "null"],
726
734
  "propertyNames": { "type": "string" },
@@ -757,7 +765,9 @@ const operationManifest = [
757
765
  "role",
758
766
  "request_id",
759
767
  "auth_method",
760
- "key_id"
768
+ "key_id",
769
+ "email",
770
+ "role_name"
761
771
  ]
762
772
  },
763
773
  "sdkName": "whoami",
@@ -769,7 +779,7 @@ const operationManifest = [
769
779
  "binaryResponse": false,
770
780
  "bodyRequired": false,
771
781
  "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.",
782
+ "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
783
  "hasJsonBody": false,
774
784
  "method": "GET",
775
785
  "operationId": "listActivityObjects",
@@ -777,7 +787,15 @@ const operationManifest = [
777
787
  "pathParams": [],
778
788
  "queryParams": [
779
789
  {
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).",
790
+ "default": "created",
791
+ "description": "created (the default) or asked.",
792
+ "enum": ["created", "asked"],
793
+ "name": "relation",
794
+ "required": false,
795
+ "type": "string"
796
+ },
797
+ {
798
+ "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
799
  "enum": null,
782
800
  "name": "kind",
783
801
  "required": false,
@@ -822,7 +840,7 @@ const operationManifest = [
822
840
  "properties": {
823
841
  "covers": {
824
842
  "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."
843
+ "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
844
  },
827
845
  "items": {
828
846
  "type": "array",
@@ -854,21 +872,21 @@ const operationManifest = [
854
872
  "owned_now": {
855
873
  "type": "string",
856
874
  "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."
875
+ "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
876
  },
859
877
  "created_through": {
860
878
  "type": "object",
861
879
  "properties": {
862
880
  "kind": {
863
881
  "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)."
882
+ "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
883
  },
866
884
  "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.",
885
+ "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
886
  "type": ["string", "null"]
869
887
  },
870
888
  "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.",
889
+ "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
890
  "type": ["string", "null"]
873
891
  }
874
892
  },
@@ -929,7 +947,7 @@ const operationManifest = [
929
947
  ]
930
948
  },
931
949
  "sdkName": "listActivityObjects",
932
- "summary": "List what you created",
950
+ "summary": "List what you created, or what you were asked",
933
951
  "tag": "Activity",
934
952
  "tagCommand": "activity"
935
953
  },
@@ -9450,7 +9468,7 @@ const operationManifest = [
9450
9468
  "binaryResponse": false,
9451
9469
  "bodyRequired": false,
9452
9470
  "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.",
9471
+ "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
9472
  "hasJsonBody": false,
9455
9473
  "method": "GET",
9456
9474
  "operationId": "getKey",
@@ -9484,12 +9502,25 @@ const operationManifest = [
9484
9502
  "grant": {
9485
9503
  "type": "object",
9486
9504
  "properties": {
9505
+ "ref": {
9506
+ "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.",
9507
+ "type": "string"
9508
+ },
9487
9509
  "template": { "type": ["string", "null"] },
9488
9510
  "permissions": {
9489
9511
  "type": ["array", "null"],
9490
9512
  "items": { "type": "string" }
9491
9513
  },
9492
- "mail_scope": {}
9514
+ "mail_scope": {},
9515
+ "resources": {
9516
+ "type": ["object", "null"],
9517
+ "propertyNames": { "type": "string" },
9518
+ "additionalProperties": {
9519
+ "type": "array",
9520
+ "items": { "type": "string" }
9521
+ },
9522
+ "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)."
9523
+ }
9493
9524
  },
9494
9525
  "required": [
9495
9526
  "template",
@@ -9505,14 +9536,31 @@ const operationManifest = [
9505
9536
  "expires_at": { "type": ["string", "null"] },
9506
9537
  "suspended": { "type": "boolean" },
9507
9538
  "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.",
9539
+ "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
9540
  "type": "string",
9510
9541
  "enum": [
9511
9542
  "active",
9512
9543
  "suspended",
9513
9544
  "expired",
9514
- "inactive"
9545
+ "inactive",
9546
+ "revoked"
9515
9547
  ]
9548
+ },
9549
+ "revoked_at": {
9550
+ "description": "When the key was revoked (ISO 8601); null for a key that is not revoked.",
9551
+ "type": ["string", "null"]
9552
+ },
9553
+ "revoked_by": {
9554
+ "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).",
9555
+ "type": ["string", "null"]
9556
+ },
9557
+ "rotated_from": {
9558
+ "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.",
9559
+ "type": ["string", "null"]
9560
+ },
9561
+ "rotated_to": {
9562
+ "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.",
9563
+ "type": ["string", "null"]
9516
9564
  }
9517
9565
  },
9518
9566
  "required": [
@@ -10049,7 +10097,7 @@ const operationManifest = [
10049
10097
  "binaryResponse": false,
10050
10098
  "bodyRequired": false,
10051
10099
  "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.",
10100
+ "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
10101
  "hasJsonBody": false,
10054
10102
  "method": "GET",
10055
10103
  "operationId": "listCredentials",
@@ -10286,7 +10334,7 @@ const operationManifest = [
10286
10334
  "binaryResponse": false,
10287
10335
  "bodyRequired": false,
10288
10336
  "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.",
10337
+ "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
10338
  "hasJsonBody": false,
10291
10339
  "method": "GET",
10292
10340
  "operationId": "listKeys",
@@ -10298,6 +10346,12 @@ const operationManifest = [
10298
10346
  "name": "cursor",
10299
10347
  "required": false,
10300
10348
  "type": "string"
10349
+ }, {
10350
+ "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.",
10351
+ "enum": ["true", "false"],
10352
+ "name": "include_revoked",
10353
+ "required": false,
10354
+ "type": "string"
10301
10355
  }],
10302
10356
  "headerParams": [],
10303
10357
  "permissions": ["keys:read"],
@@ -10325,12 +10379,25 @@ const operationManifest = [
10325
10379
  "grant": {
10326
10380
  "type": "object",
10327
10381
  "properties": {
10382
+ "ref": {
10383
+ "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.",
10384
+ "type": "string"
10385
+ },
10328
10386
  "template": { "type": ["string", "null"] },
10329
10387
  "permissions": {
10330
10388
  "type": ["array", "null"],
10331
10389
  "items": { "type": "string" }
10332
10390
  },
10333
- "mail_scope": {}
10391
+ "mail_scope": {},
10392
+ "resources": {
10393
+ "type": ["object", "null"],
10394
+ "propertyNames": { "type": "string" },
10395
+ "additionalProperties": {
10396
+ "type": "array",
10397
+ "items": { "type": "string" }
10398
+ },
10399
+ "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)."
10400
+ }
10334
10401
  },
10335
10402
  "required": [
10336
10403
  "template",
@@ -10346,14 +10413,31 @@ const operationManifest = [
10346
10413
  "expires_at": { "type": ["string", "null"] },
10347
10414
  "suspended": { "type": "boolean" },
10348
10415
  "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.",
10416
+ "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
10417
  "type": "string",
10351
10418
  "enum": [
10352
10419
  "active",
10353
10420
  "suspended",
10354
10421
  "expired",
10355
- "inactive"
10422
+ "inactive",
10423
+ "revoked"
10356
10424
  ]
10425
+ },
10426
+ "revoked_at": {
10427
+ "description": "When the key was revoked (ISO 8601); null for a key that is not revoked.",
10428
+ "type": ["string", "null"]
10429
+ },
10430
+ "revoked_by": {
10431
+ "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).",
10432
+ "type": ["string", "null"]
10433
+ },
10434
+ "rotated_from": {
10435
+ "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.",
10436
+ "type": ["string", "null"]
10437
+ },
10438
+ "rotated_to": {
10439
+ "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.",
10440
+ "type": ["string", "null"]
10357
10441
  }
10358
10442
  },
10359
10443
  "required": [
@@ -10472,7 +10556,7 @@ const operationManifest = [
10472
10556
  "binaryResponse": false,
10473
10557
  "bodyRequired": true,
10474
10558
  "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.",
10559
+ "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
10560
  "hasJsonBody": true,
10477
10561
  "method": "POST",
10478
10562
  "operationId": "revokeCredential",
@@ -10583,6 +10667,51 @@ const operationManifest = [
10583
10667
  },
10584
10668
  "required": ["kind", "id"]
10585
10669
  }
10670
+ },
10671
+ "already_revoked": {
10672
+ "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.",
10673
+ "type": "boolean"
10674
+ },
10675
+ "keep_failed": {
10676
+ "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.",
10677
+ "type": "array",
10678
+ "items": {
10679
+ "type": "object",
10680
+ "properties": {
10681
+ "kind": { "type": "string" },
10682
+ "id": { "type": "string" }
10683
+ },
10684
+ "required": ["kind", "id"]
10685
+ }
10686
+ },
10687
+ "standing_objects": {
10688
+ "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.",
10689
+ "type": "array",
10690
+ "items": {
10691
+ "type": "object",
10692
+ "properties": {
10693
+ "kind": {
10694
+ "type": "string",
10695
+ "description": "endpoint, function_trigger, pull_subscription, recipient_route, filter_rule or wake_schedule."
10696
+ },
10697
+ "id": { "type": "string" },
10698
+ "label": {
10699
+ "type": "string",
10700
+ "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."
10701
+ },
10702
+ "org_owned": {
10703
+ "type": "boolean",
10704
+ "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."
10705
+ }
10706
+ },
10707
+ "required": [
10708
+ "kind",
10709
+ "id",
10710
+ "label",
10711
+ "org_owned"
10712
+ ],
10713
+ "additionalProperties": {}
10714
+ }
10586
10715
  }
10587
10716
  },
10588
10717
  "required": [
@@ -10601,7 +10730,7 @@ const operationManifest = [
10601
10730
  "binaryResponse": false,
10602
10731
  "bodyRequired": true,
10603
10732
  "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.",
10733
+ "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
10734
  "hasJsonBody": true,
10606
10735
  "method": "POST",
10607
10736
  "operationId": "rotateKey",
@@ -10631,12 +10760,20 @@ const operationManifest = [
10631
10760
  "access": "step_up",
10632
10761
  "requestSchema": {
10633
10762
  "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
- } },
10763
+ "properties": {
10764
+ "overlap_seconds": {
10765
+ "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.",
10766
+ "type": "integer",
10767
+ "minimum": 0,
10768
+ "maximum": 604800
10769
+ },
10770
+ "expires_at": {
10771
+ "type": ["string", "null"],
10772
+ "format": "date-time",
10773
+ "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)))$",
10774
+ "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."
10775
+ }
10776
+ },
10640
10777
  "additionalProperties": false
10641
10778
  },
10642
10779
  "responseSchema": {
@@ -10655,7 +10792,7 @@ const operationManifest = [
10655
10792
  "type": "string",
10656
10793
  "format": "date-time",
10657
10794
  "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."
10795
+ "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
10796
  }
10660
10797
  },
10661
10798
  "required": [
@@ -10767,7 +10904,7 @@ const operationManifest = [
10767
10904
  "binaryResponse": false,
10768
10905
  "bodyRequired": true,
10769
10906
  "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.",
10907
+ "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
10908
  "hasJsonBody": true,
10772
10909
  "method": "PATCH",
10773
10910
  "operationId": "updateKey",
@@ -10800,7 +10937,8 @@ const operationManifest = [
10800
10937
  "expires_at": {
10801
10938
  "type": ["string", "null"],
10802
10939
  "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)))$"
10940
+ "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
+ "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
10942
  },
10805
10943
  "grant": { "anyOf": [{
10806
10944
  "type": "string",
@@ -10956,6 +11094,11 @@ const operationManifest = [
10956
11094
  ],
10957
11095
  "additionalProperties": {}
10958
11096
  }
11097
+ },
11098
+ "grant_unguarded": {
11099
+ "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).",
11100
+ "type": "boolean",
11101
+ "const": true
10959
11102
  }
10960
11103
  },
10961
11104
  "required": [
@@ -12773,7 +12916,7 @@ const operationManifest = [
12773
12916
  "binaryResponse": false,
12774
12917
  "bodyRequired": false,
12775
12918
  "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.",
12919
+ "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
12920
  "hasJsonBody": true,
12778
12921
  "method": "POST",
12779
12922
  "operationId": "acceptEmailRuleRequest",
@@ -12933,7 +13076,8 @@ const operationManifest = [
12933
13076
  },
12934
13077
  "warnings": {
12935
13078
  "type": "array",
12936
- "items": { "type": "string" }
13079
+ "items": { "type": "string" },
13080
+ "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
13081
  },
12938
13082
  "compiled_by": { "type": "string" },
12939
13083
  "vocabulary": { "type": "string" },
@@ -13016,6 +13160,14 @@ const operationManifest = [
13016
13160
  "type": ["string", "null"],
13017
13161
  "format": "uuid",
13018
13162
  "description": "The share accepting created, while it exists."
13163
+ },
13164
+ "message": {
13165
+ "description": "What the member asking wrote about why; null when they wrote nothing.",
13166
+ "type": ["string", "null"]
13167
+ },
13168
+ "decline_reason": {
13169
+ "description": "Why the member asked declined it; null unless they gave a reason when declining.",
13170
+ "type": ["string", "null"]
13019
13171
  }
13020
13172
  },
13021
13173
  "required": [
@@ -13232,8 +13384,10 @@ const operationManifest = [
13232
13384
  "removed_by_beneficiary",
13233
13385
  "stopped_by_admin",
13234
13386
  "rule_changed",
13387
+ "rule_deleted",
13235
13388
  null
13236
- ]
13389
+ ],
13390
+ "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
13391
  },
13238
13392
  "accepted_rule_version": {
13239
13393
  "type": "integer",
@@ -13994,7 +14148,7 @@ const operationManifest = [
13994
14148
  "binaryResponse": false,
13995
14149
  "bodyRequired": true,
13996
14150
  "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.",
14151
+ "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
14152
  "hasJsonBody": true,
13999
14153
  "method": "POST",
14000
14154
  "operationId": "createEmailRuleRequest",
@@ -14031,6 +14185,12 @@ const operationManifest = [
14031
14185
  "maxLength": 200,
14032
14186
  "description": "A short name for the rule."
14033
14187
  },
14188
+ "message": {
14189
+ "type": ["string", "null"],
14190
+ "minLength": 1,
14191
+ "maxLength": 500,
14192
+ "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."
14193
+ },
14034
14194
  "english": {
14035
14195
  "type": "string",
14036
14196
  "minLength": 1,
@@ -14162,7 +14322,8 @@ const operationManifest = [
14162
14322
  },
14163
14323
  "warnings": {
14164
14324
  "type": "array",
14165
- "items": { "type": "string" }
14325
+ "items": { "type": "string" },
14326
+ "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
14327
  },
14167
14328
  "compiled_by": { "type": "string" },
14168
14329
  "vocabulary": { "type": "string" },
@@ -14246,6 +14407,14 @@ const operationManifest = [
14246
14407
  "format": "uuid",
14247
14408
  "description": "The share accepting created, while it exists."
14248
14409
  },
14410
+ "message": {
14411
+ "description": "What the member asking wrote about why; null when they wrote nothing.",
14412
+ "type": ["string", "null"]
14413
+ },
14414
+ "decline_reason": {
14415
+ "description": "Why the member asked declined it; null unless they gave a reason when declining.",
14416
+ "type": ["string", "null"]
14417
+ },
14249
14418
  "target_can_accept": {
14250
14419
  "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
14420
  "type": ["boolean", "null"]
@@ -14287,8 +14456,8 @@ const operationManifest = [
14287
14456
  "binaryResponse": false,
14288
14457
  "bodyRequired": false,
14289
14458
  "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,
14459
+ "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.",
14460
+ "hasJsonBody": true,
14292
14461
  "method": "POST",
14293
14462
  "operationId": "declineEmailRuleRequest",
14294
14463
  "path": "/email-rule-requests/{id}/decline",
@@ -14309,7 +14478,16 @@ const operationManifest = [
14309
14478
  }],
14310
14479
  "permissions": ["mail:read"],
14311
14480
  "access": null,
14312
- "requestSchema": null,
14481
+ "requestSchema": {
14482
+ "type": "object",
14483
+ "properties": { "reason": {
14484
+ "type": ["string", "null"],
14485
+ "minLength": 1,
14486
+ "maxLength": 500,
14487
+ "description": "Why you are declining, up to 500 characters; null or absent for none. The member who asked sees it with the request."
14488
+ } },
14489
+ "additionalProperties": false
14490
+ },
14313
14491
  "responseSchema": {
14314
14492
  "type": "object",
14315
14493
  "properties": {
@@ -14365,7 +14543,8 @@ const operationManifest = [
14365
14543
  },
14366
14544
  "warnings": {
14367
14545
  "type": "array",
14368
- "items": { "type": "string" }
14546
+ "items": { "type": "string" },
14547
+ "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
14548
  },
14370
14549
  "compiled_by": { "type": "string" },
14371
14550
  "vocabulary": { "type": "string" },
@@ -14448,6 +14627,14 @@ const operationManifest = [
14448
14627
  "type": ["string", "null"],
14449
14628
  "format": "uuid",
14450
14629
  "description": "The share accepting created, while it exists."
14630
+ },
14631
+ "message": {
14632
+ "description": "What the member asking wrote about why; null when they wrote nothing.",
14633
+ "type": ["string", "null"]
14634
+ },
14635
+ "decline_reason": {
14636
+ "description": "Why the member asked declined it; null unless they gave a reason when declining.",
14637
+ "type": ["string", "null"]
14451
14638
  }
14452
14639
  },
14453
14640
  "required": [
@@ -14482,7 +14669,7 @@ const operationManifest = [
14482
14669
  "binaryResponse": false,
14483
14670
  "bodyRequired": false,
14484
14671
  "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.",
14672
+ "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
14673
  "hasJsonBody": false,
14487
14674
  "method": "DELETE",
14488
14675
  "operationId": "deleteEmailRule",
@@ -14501,7 +14688,41 @@ const operationManifest = [
14501
14688
  "requestSchema": null,
14502
14689
  "responseSchema": {
14503
14690
  "type": "object",
14504
- "properties": { "deleted": { "type": "boolean" } },
14691
+ "properties": {
14692
+ "deleted": { "type": "boolean" },
14693
+ "ended_shares": {
14694
+ "description": "The shares of this rule that the delete ended (end_reason rule_deleted). Each keeps its record.",
14695
+ "type": "array",
14696
+ "items": {
14697
+ "type": "string",
14698
+ "format": "uuid"
14699
+ }
14700
+ },
14701
+ "keys": {
14702
+ "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.",
14703
+ "type": "array",
14704
+ "items": {
14705
+ "type": "object",
14706
+ "properties": {
14707
+ "id": {
14708
+ "type": "string",
14709
+ "format": "uuid"
14710
+ },
14711
+ "name": { "type": ["string", "null"] },
14712
+ "key_prefix": { "type": "string" }
14713
+ },
14714
+ "required": [
14715
+ "id",
14716
+ "name",
14717
+ "key_prefix"
14718
+ ]
14719
+ }
14720
+ },
14721
+ "other_keys": {
14722
+ "description": "How many more keys, not revoked, list the rule that you may not read. They are not named.",
14723
+ "type": "integer"
14724
+ }
14725
+ },
14505
14726
  "required": ["deleted"]
14506
14727
  },
14507
14728
  "sdkName": "deleteEmailRule",
@@ -14793,7 +15014,7 @@ const operationManifest = [
14793
15014
  "binaryResponse": false,
14794
15015
  "bodyRequired": false,
14795
15016
  "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.",
15017
+ "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
15018
  "hasJsonBody": false,
14798
15019
  "method": "GET",
14799
15020
  "operationId": "getEmailRuleRequest",
@@ -14865,7 +15086,8 @@ const operationManifest = [
14865
15086
  },
14866
15087
  "warnings": {
14867
15088
  "type": "array",
14868
- "items": { "type": "string" }
15089
+ "items": { "type": "string" },
15090
+ "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
15091
  },
14870
15092
  "compiled_by": { "type": "string" },
14871
15093
  "vocabulary": { "type": "string" },
@@ -14948,6 +15170,14 @@ const operationManifest = [
14948
15170
  "type": ["string", "null"],
14949
15171
  "format": "uuid",
14950
15172
  "description": "The share accepting created, while it exists."
15173
+ },
15174
+ "message": {
15175
+ "description": "What the member asking wrote about why; null when they wrote nothing.",
15176
+ "type": ["string", "null"]
15177
+ },
15178
+ "decline_reason": {
15179
+ "description": "Why the member asked declined it; null unless they gave a reason when declining.",
15180
+ "type": ["string", "null"]
14951
15181
  }
14952
15182
  },
14953
15183
  "required": [
@@ -15158,8 +15388,10 @@ const operationManifest = [
15158
15388
  "removed_by_beneficiary",
15159
15389
  "stopped_by_admin",
15160
15390
  "rule_changed",
15391
+ "rule_deleted",
15161
15392
  null
15162
- ]
15393
+ ],
15394
+ "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
15395
  },
15164
15396
  "accepted_rule_version": {
15165
15397
  "type": "integer",
@@ -15553,6 +15785,39 @@ const operationManifest = [
15553
15785
  "type": "boolean",
15554
15786
  "const": true
15555
15787
  },
15788
+ "withheld_by": {
15789
+ "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.",
15790
+ "type": "array",
15791
+ "items": {
15792
+ "type": "object",
15793
+ "properties": {
15794
+ "id": {
15795
+ "type": "string",
15796
+ "format": "uuid"
15797
+ },
15798
+ "name": { "type": "string" },
15799
+ "reason": {
15800
+ "type": "string",
15801
+ "enum": [
15802
+ "matched",
15803
+ "undecided",
15804
+ "outside_scope_stale"
15805
+ ],
15806
+ "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."
15807
+ }
15808
+ },
15809
+ "required": [
15810
+ "id",
15811
+ "name",
15812
+ "reason"
15813
+ ]
15814
+ }
15815
+ },
15816
+ "withheld_by_hidden": {
15817
+ "type": "integer",
15818
+ "minimum": 0,
15819
+ "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."
15820
+ },
15556
15821
  "created_at": {
15557
15822
  "type": "string",
15558
15823
  "format": "date-time"
@@ -15703,7 +15968,8 @@ const operationManifest = [
15703
15968
  },
15704
15969
  "warnings": {
15705
15970
  "type": "array",
15706
- "items": { "type": "string" }
15971
+ "items": { "type": "string" },
15972
+ "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
15973
  },
15708
15974
  "compiled_by": { "type": "string" },
15709
15975
  "vocabulary": { "type": "string" },
@@ -15786,6 +16052,14 @@ const operationManifest = [
15786
16052
  "type": ["string", "null"],
15787
16053
  "format": "uuid",
15788
16054
  "description": "The share accepting created, while it exists."
16055
+ },
16056
+ "message": {
16057
+ "description": "What the member asking wrote about why; null when they wrote nothing.",
16058
+ "type": ["string", "null"]
16059
+ },
16060
+ "decline_reason": {
16061
+ "description": "Why the member asked declined it; null unless they gave a reason when declining.",
16062
+ "type": ["string", "null"]
15789
16063
  }
15790
16064
  },
15791
16065
  "required": [
@@ -15841,6 +16115,36 @@ const operationManifest = [
15841
16115
  "responseSchema": {
15842
16116
  "type": "object",
15843
16117
  "properties": {
16118
+ "email": {
16119
+ "description": "The email these results are about, so a saved answer says which one it was.",
16120
+ "type": "object",
16121
+ "properties": {
16122
+ "id": {
16123
+ "type": "string",
16124
+ "format": "uuid"
16125
+ },
16126
+ "from_address": {
16127
+ "description": "The sender address, as GET /v1/emails/{id} shows it.",
16128
+ "type": ["string", "null"]
16129
+ },
16130
+ "recipient": {
16131
+ "type": "string",
16132
+ "description": "The mailbox this copy of the email was received in, as GET /v1/emails/{id} shows it."
16133
+ },
16134
+ "subject": { "type": ["string", "null"] },
16135
+ "received_at": {
16136
+ "type": "string",
16137
+ "format": "date-time"
16138
+ }
16139
+ },
16140
+ "required": [
16141
+ "id",
16142
+ "from_address",
16143
+ "recipient",
16144
+ "subject",
16145
+ "received_at"
16146
+ ]
16147
+ },
15844
16148
  "rules": {
15845
16149
  "type": "array",
15846
16150
  "items": {
@@ -15873,6 +16177,10 @@ const operationManifest = [
15873
16177
  "description": "The version of the rule that decided."
15874
16178
  },
15875
16179
  "matched": { "type": "boolean" },
16180
+ "near_miss": {
16181
+ "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.",
16182
+ "type": "boolean"
16183
+ },
15876
16184
  "parts": {
15877
16185
  "type": "array",
15878
16186
  "items": {
@@ -15948,10 +16256,43 @@ const operationManifest = [
15948
16256
  }, { "type": "null" }]
15949
16257
  },
15950
16258
  "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.",
16259
+ "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
16260
  "type": "boolean",
15953
16261
  "const": true
15954
16262
  },
16263
+ "withheld_by": {
16264
+ "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.",
16265
+ "type": "array",
16266
+ "items": {
16267
+ "type": "object",
16268
+ "properties": {
16269
+ "id": {
16270
+ "type": "string",
16271
+ "format": "uuid"
16272
+ },
16273
+ "name": { "type": "string" },
16274
+ "reason": {
16275
+ "type": "string",
16276
+ "enum": [
16277
+ "matched",
16278
+ "undecided",
16279
+ "outside_scope_stale"
16280
+ ],
16281
+ "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."
16282
+ }
16283
+ },
16284
+ "required": [
16285
+ "id",
16286
+ "name",
16287
+ "reason"
16288
+ ]
16289
+ }
16290
+ },
16291
+ "withheld_by_hidden": {
16292
+ "type": "integer",
16293
+ "minimum": 0,
16294
+ "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."
16295
+ },
15955
16296
  "created_at": {
15956
16297
  "type": "string",
15957
16298
  "format": "date-time"
@@ -16524,8 +16865,10 @@ const operationManifest = [
16524
16865
  "removed_by_beneficiary",
16525
16866
  "stopped_by_admin",
16526
16867
  "rule_changed",
16868
+ "rule_deleted",
16527
16869
  null
16528
- ]
16870
+ ],
16871
+ "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
16872
  },
16530
16873
  "accepted_rule_version": {
16531
16874
  "type": "integer",
@@ -16997,8 +17340,10 @@ const operationManifest = [
16997
17340
  "removed_by_beneficiary",
16998
17341
  "stopped_by_admin",
16999
17342
  "rule_changed",
17343
+ "rule_deleted",
17000
17344
  null
17001
- ]
17345
+ ],
17346
+ "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
17347
  },
17003
17348
  "accepted_rule_version": {
17004
17349
  "type": "integer",
@@ -17420,6 +17765,10 @@ const operationManifest = [
17420
17765
  "format": "date-time"
17421
17766
  },
17422
17767
  "from_address": { "type": ["string", "null"] },
17768
+ "recipient": {
17769
+ "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.",
17770
+ "type": "string"
17771
+ },
17423
17772
  "subject": { "type": ["string", "null"] },
17424
17773
  "parts": {
17425
17774
  "description": "How each top-level part of the rule decided. Present for matched, near_miss and no_match.",
@@ -18577,7 +18926,8 @@ const operationManifest = [
18577
18926
  },
18578
18927
  "warnings": {
18579
18928
  "type": "array",
18580
- "items": { "type": "string" }
18929
+ "items": { "type": "string" },
18930
+ "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
18931
  },
18582
18932
  "compiled_by": { "type": "string" },
18583
18933
  "vocabulary": { "type": "string" },
@@ -18660,6 +19010,14 @@ const operationManifest = [
18660
19010
  "type": ["string", "null"],
18661
19011
  "format": "uuid",
18662
19012
  "description": "The share accepting created, while it exists."
19013
+ },
19014
+ "message": {
19015
+ "description": "What the member asking wrote about why; null when they wrote nothing.",
19016
+ "type": ["string", "null"]
19017
+ },
19018
+ "decline_reason": {
19019
+ "description": "Why the member asked declined it; null unless they gave a reason when declining.",
19020
+ "type": ["string", "null"]
18663
19021
  }
18664
19022
  },
18665
19023
  "required": [
@@ -20473,7 +20831,7 @@ const operationManifest = [
20473
20831
  "type": "string"
20474
20832
  },
20475
20833
  {
20476
- "description": "Exact case-insensitive delivered recipient mailbox, applied before pagination. Combines with existing filters and cursors; does not search message text.",
20834
+ "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
20835
  "enum": null,
20478
20836
  "name": "recipient",
20479
20837
  "required": false,
@@ -21955,7 +22313,7 @@ const operationManifest = [
21955
22313
  "binaryResponse": false,
21956
22314
  "bodyRequired": true,
21957
22315
  "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.",
22316
+ "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
22317
  "hasJsonBody": true,
21960
22318
  "method": "POST",
21961
22319
  "operationId": "createEndpoint",
@@ -22257,13 +22615,13 @@ const operationManifest = [
22257
22615
  "type": "boolean"
22258
22616
  },
22259
22617
  "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.",
22618
+ "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
22619
  "type": ["string", "null"],
22262
22620
  "format": "uuid",
22263
22621
  "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
22622
  },
22265
22623
  "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.",
22624
+ "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
22625
  "type": ["string", "null"],
22268
22626
  "enum": [
22269
22627
  "active",
@@ -22272,6 +22630,65 @@ const operationManifest = [
22272
22630
  null
22273
22631
  ]
22274
22632
  },
22633
+ "disabled_reason": {
22634
+ "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.",
22635
+ "type": ["string", "null"]
22636
+ },
22637
+ "delivery": {
22638
+ "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.",
22639
+ "type": ["object", "null"],
22640
+ "properties": {
22641
+ "delivering": {
22642
+ "type": "boolean",
22643
+ "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."
22644
+ },
22645
+ "reasons": {
22646
+ "type": "array",
22647
+ "items": {
22648
+ "type": "object",
22649
+ "properties": {
22650
+ "code": { "type": "string" },
22651
+ "message": { "type": "string" }
22652
+ },
22653
+ "required": ["code", "message"],
22654
+ "additionalProperties": {}
22655
+ },
22656
+ "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."
22657
+ },
22658
+ "held_for_owner": {
22659
+ "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.",
22660
+ "type": ["object", "null"],
22661
+ "properties": {
22662
+ "count": {
22663
+ "type": "integer",
22664
+ "minimum": -9007199254740991,
22665
+ "maximum": 9007199254740991,
22666
+ "description": "Rule-granted emails waiting for this webhook because its owner could not act at their last check."
22667
+ },
22668
+ "oldest_queued_at": {
22669
+ "type": "string",
22670
+ "format": "date-time",
22671
+ "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))$",
22672
+ "description": "When the oldest of them was queued."
22673
+ },
22674
+ "expires_at": {
22675
+ "type": "string",
22676
+ "format": "date-time",
22677
+ "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))$",
22678
+ "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."
22679
+ }
22680
+ },
22681
+ "required": [
22682
+ "count",
22683
+ "oldest_queued_at",
22684
+ "expires_at"
22685
+ ],
22686
+ "additionalProperties": {}
22687
+ }
22688
+ },
22689
+ "required": ["delivering", "reasons"],
22690
+ "additionalProperties": {}
22691
+ },
22275
22692
  "signed_with": {
22276
22693
  "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
22694
  "type": "string",
@@ -22338,7 +22755,7 @@ const operationManifest = [
22338
22755
  "binaryResponse": false,
22339
22756
  "bodyRequired": false,
22340
22757
  "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.",
22758
+ "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
22759
  "hasJsonBody": false,
22343
22760
  "method": "GET",
22344
22761
  "operationId": "getEndpoint",
@@ -22525,13 +22942,13 @@ const operationManifest = [
22525
22942
  "type": "boolean"
22526
22943
  },
22527
22944
  "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.",
22945
+ "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
22946
  "type": ["string", "null"],
22530
22947
  "format": "uuid",
22531
22948
  "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
22949
  },
22533
22950
  "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.",
22951
+ "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
22952
  "type": ["string", "null"],
22536
22953
  "enum": [
22537
22954
  "active",
@@ -22540,6 +22957,65 @@ const operationManifest = [
22540
22957
  null
22541
22958
  ]
22542
22959
  },
22960
+ "disabled_reason": {
22961
+ "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.",
22962
+ "type": ["string", "null"]
22963
+ },
22964
+ "delivery": {
22965
+ "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.",
22966
+ "type": ["object", "null"],
22967
+ "properties": {
22968
+ "delivering": {
22969
+ "type": "boolean",
22970
+ "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."
22971
+ },
22972
+ "reasons": {
22973
+ "type": "array",
22974
+ "items": {
22975
+ "type": "object",
22976
+ "properties": {
22977
+ "code": { "type": "string" },
22978
+ "message": { "type": "string" }
22979
+ },
22980
+ "required": ["code", "message"],
22981
+ "additionalProperties": {}
22982
+ },
22983
+ "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."
22984
+ },
22985
+ "held_for_owner": {
22986
+ "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.",
22987
+ "type": ["object", "null"],
22988
+ "properties": {
22989
+ "count": {
22990
+ "type": "integer",
22991
+ "minimum": -9007199254740991,
22992
+ "maximum": 9007199254740991,
22993
+ "description": "Rule-granted emails waiting for this webhook because its owner could not act at their last check."
22994
+ },
22995
+ "oldest_queued_at": {
22996
+ "type": "string",
22997
+ "format": "date-time",
22998
+ "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))$",
22999
+ "description": "When the oldest of them was queued."
23000
+ },
23001
+ "expires_at": {
23002
+ "type": "string",
23003
+ "format": "date-time",
23004
+ "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))$",
23005
+ "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."
23006
+ }
23007
+ },
23008
+ "required": [
23009
+ "count",
23010
+ "oldest_queued_at",
23011
+ "expires_at"
23012
+ ],
23013
+ "additionalProperties": {}
23014
+ }
23015
+ },
23016
+ "required": ["delivering", "reasons"],
23017
+ "additionalProperties": {}
23018
+ },
22543
23019
  "signed_with": {
22544
23020
  "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
23021
  "type": "string",
@@ -22786,13 +23262,13 @@ const operationManifest = [
22786
23262
  "type": "boolean"
22787
23263
  },
22788
23264
  "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.",
23265
+ "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
23266
  "type": ["string", "null"],
22791
23267
  "format": "uuid",
22792
23268
  "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
23269
  },
22794
23270
  "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.",
23271
+ "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
23272
  "type": ["string", "null"],
22797
23273
  "enum": [
22798
23274
  "active",
@@ -22801,6 +23277,65 @@ const operationManifest = [
22801
23277
  null
22802
23278
  ]
22803
23279
  },
23280
+ "disabled_reason": {
23281
+ "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.",
23282
+ "type": ["string", "null"]
23283
+ },
23284
+ "delivery": {
23285
+ "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.",
23286
+ "type": ["object", "null"],
23287
+ "properties": {
23288
+ "delivering": {
23289
+ "type": "boolean",
23290
+ "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."
23291
+ },
23292
+ "reasons": {
23293
+ "type": "array",
23294
+ "items": {
23295
+ "type": "object",
23296
+ "properties": {
23297
+ "code": { "type": "string" },
23298
+ "message": { "type": "string" }
23299
+ },
23300
+ "required": ["code", "message"],
23301
+ "additionalProperties": {}
23302
+ },
23303
+ "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."
23304
+ },
23305
+ "held_for_owner": {
23306
+ "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.",
23307
+ "type": ["object", "null"],
23308
+ "properties": {
23309
+ "count": {
23310
+ "type": "integer",
23311
+ "minimum": -9007199254740991,
23312
+ "maximum": 9007199254740991,
23313
+ "description": "Rule-granted emails waiting for this webhook because its owner could not act at their last check."
23314
+ },
23315
+ "oldest_queued_at": {
23316
+ "type": "string",
23317
+ "format": "date-time",
23318
+ "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))$",
23319
+ "description": "When the oldest of them was queued."
23320
+ },
23321
+ "expires_at": {
23322
+ "type": "string",
23323
+ "format": "date-time",
23324
+ "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))$",
23325
+ "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."
23326
+ }
23327
+ },
23328
+ "required": [
23329
+ "count",
23330
+ "oldest_queued_at",
23331
+ "expires_at"
23332
+ ],
23333
+ "additionalProperties": {}
23334
+ }
23335
+ },
23336
+ "required": ["delivering", "reasons"],
23337
+ "additionalProperties": {}
23338
+ },
22804
23339
  "signed_with": {
22805
23340
  "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
23341
  "type": "string",
@@ -22994,7 +23529,7 @@ const operationManifest = [
22994
23529
  "binaryResponse": false,
22995
23530
  "bodyRequired": false,
22996
23531
  "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",
23532
+ "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
23533
  "hasJsonBody": false,
22999
23534
  "method": "POST",
23000
23535
  "operationId": "testEndpoint",
@@ -23031,9 +23566,58 @@ const operationManifest = [
23031
23566
  "signature": {
23032
23567
  "type": "string",
23033
23568
  "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."
23569
+ },
23570
+ "reachable": {
23571
+ "type": "boolean",
23572
+ "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."
23573
+ },
23574
+ "delivery": {
23575
+ "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.",
23576
+ "type": ["object", "null"],
23577
+ "properties": {
23578
+ "event_type": {
23579
+ "type": "string",
23580
+ "description": "The event type checked."
23581
+ },
23582
+ "would_deliver": {
23583
+ "type": "boolean",
23584
+ "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."
23585
+ },
23586
+ "reasons": {
23587
+ "type": "array",
23588
+ "items": {
23589
+ "type": "object",
23590
+ "properties": {
23591
+ "code": { "type": "string" },
23592
+ "message": { "type": "string" }
23593
+ },
23594
+ "required": ["code", "message"],
23595
+ "additionalProperties": {}
23596
+ },
23597
+ "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."
23598
+ },
23599
+ "events_paused_until": {
23600
+ "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.",
23601
+ "type": ["string", "null"],
23602
+ "format": "date-time",
23603
+ "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))$"
23604
+ }
23605
+ },
23606
+ "required": [
23607
+ "event_type",
23608
+ "would_deliver",
23609
+ "reasons",
23610
+ "events_paused_until"
23611
+ ],
23612
+ "additionalProperties": {}
23034
23613
  }
23035
23614
  },
23036
- "required": ["status", "body"]
23615
+ "required": [
23616
+ "status",
23617
+ "body",
23618
+ "reachable",
23619
+ "delivery"
23620
+ ]
23037
23621
  },
23038
23622
  "sdkName": "testEndpoint",
23039
23623
  "summary": "Send a test webhook",
@@ -23044,7 +23628,7 @@ const operationManifest = [
23044
23628
  "binaryResponse": false,
23045
23629
  "bodyRequired": true,
23046
23630
  "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",
23631
+ "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
23632
  "hasJsonBody": true,
23049
23633
  "method": "POST",
23050
23634
  "operationId": "testEndpointRules",
@@ -23082,15 +23666,15 @@ const operationManifest = [
23082
23666
  "properties": {
23083
23667
  "would_deliver": {
23084
23668
  "type": "boolean",
23085
- "description": "Whether the delivery path would deliver this email to the endpoint."
23669
+ "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
23670
  },
23087
23671
  "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"
23672
+ "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.",
23673
+ "type": ["string", "null"]
23090
23674
  },
23091
23675
  "reason": {
23092
- "type": ["string", "null"],
23093
- "description": "Human-readable explanation of the failing rule; null when the message matches."
23676
+ "description": "Why, in words. Null when it would deliver.",
23677
+ "type": ["string", "null"]
23094
23678
  },
23095
23679
  "rules_valid": {
23096
23680
  "type": "boolean",
@@ -23137,6 +23721,46 @@ const operationManifest = [
23137
23721
  "sender_authenticated",
23138
23722
  "sender_trust_basis"
23139
23723
  ]
23724
+ },
23725
+ "delivery": {
23726
+ "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.",
23727
+ "type": ["object", "null"],
23728
+ "properties": {
23729
+ "event_type": {
23730
+ "type": "string",
23731
+ "description": "The event type checked."
23732
+ },
23733
+ "would_deliver": {
23734
+ "type": "boolean",
23735
+ "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."
23736
+ },
23737
+ "reasons": {
23738
+ "type": "array",
23739
+ "items": {
23740
+ "type": "object",
23741
+ "properties": {
23742
+ "code": { "type": "string" },
23743
+ "message": { "type": "string" }
23744
+ },
23745
+ "required": ["code", "message"],
23746
+ "additionalProperties": {}
23747
+ },
23748
+ "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."
23749
+ },
23750
+ "events_paused_until": {
23751
+ "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.",
23752
+ "type": ["string", "null"],
23753
+ "format": "date-time",
23754
+ "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))$"
23755
+ }
23756
+ },
23757
+ "required": [
23758
+ "event_type",
23759
+ "would_deliver",
23760
+ "reasons",
23761
+ "events_paused_until"
23762
+ ],
23763
+ "additionalProperties": {}
23140
23764
  }
23141
23765
  },
23142
23766
  "required": [
@@ -23146,7 +23770,8 @@ const operationManifest = [
23146
23770
  "rules_valid",
23147
23771
  "subscribed_to_event",
23148
23772
  "event_type",
23149
- "evaluated"
23773
+ "evaluated",
23774
+ "delivery"
23150
23775
  ]
23151
23776
  },
23152
23777
  "sdkName": "testEndpointRules",
@@ -23158,7 +23783,7 @@ const operationManifest = [
23158
23783
  "binaryResponse": false,
23159
23784
  "bodyRequired": true,
23160
23785
  "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.",
23786
+ "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
23787
  "hasJsonBody": true,
23163
23788
  "method": "PATCH",
23164
23789
  "operationId": "updateEndpoint",
@@ -23254,7 +23879,7 @@ const operationManifest = [
23254
23879
  "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
23880
  },
23256
23881
  "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.",
23882
+ "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
23883
  "type": ["string", "null"],
23259
23884
  "format": "uuid",
23260
23885
  "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 +23892,7 @@ const operationManifest = [
23267
23892
  },
23268
23893
  "minProperties": 1
23269
23894
  },
23270
- "responseSchema": {
23895
+ "responseSchema": { "allOf": [{
23271
23896
  "type": "object",
23272
23897
  "properties": {
23273
23898
  "recipient": {
@@ -23437,13 +24062,13 @@ const operationManifest = [
23437
24062
  "type": "boolean"
23438
24063
  },
23439
24064
  "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.",
24065
+ "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
24066
  "type": ["string", "null"],
23442
24067
  "format": "uuid",
23443
24068
  "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
24069
  },
23445
24070
  "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.",
24071
+ "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
24072
  "type": ["string", "null"],
23448
24073
  "enum": [
23449
24074
  "active",
@@ -23452,6 +24077,65 @@ const operationManifest = [
23452
24077
  null
23453
24078
  ]
23454
24079
  },
24080
+ "disabled_reason": {
24081
+ "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.",
24082
+ "type": ["string", "null"]
24083
+ },
24084
+ "delivery": {
24085
+ "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.",
24086
+ "type": ["object", "null"],
24087
+ "properties": {
24088
+ "delivering": {
24089
+ "type": "boolean",
24090
+ "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."
24091
+ },
24092
+ "reasons": {
24093
+ "type": "array",
24094
+ "items": {
24095
+ "type": "object",
24096
+ "properties": {
24097
+ "code": { "type": "string" },
24098
+ "message": { "type": "string" }
24099
+ },
24100
+ "required": ["code", "message"],
24101
+ "additionalProperties": {}
24102
+ },
24103
+ "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."
24104
+ },
24105
+ "held_for_owner": {
24106
+ "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.",
24107
+ "type": ["object", "null"],
24108
+ "properties": {
24109
+ "count": {
24110
+ "type": "integer",
24111
+ "minimum": -9007199254740991,
24112
+ "maximum": 9007199254740991,
24113
+ "description": "Rule-granted emails waiting for this webhook because its owner could not act at their last check."
24114
+ },
24115
+ "oldest_queued_at": {
24116
+ "type": "string",
24117
+ "format": "date-time",
24118
+ "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))$",
24119
+ "description": "When the oldest of them was queued."
24120
+ },
24121
+ "expires_at": {
24122
+ "type": "string",
24123
+ "format": "date-time",
24124
+ "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))$",
24125
+ "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."
24126
+ }
24127
+ },
24128
+ "required": [
24129
+ "count",
24130
+ "oldest_queued_at",
24131
+ "expires_at"
24132
+ ],
24133
+ "additionalProperties": {}
24134
+ }
24135
+ },
24136
+ "required": ["delivering", "reasons"],
24137
+ "additionalProperties": {}
24138
+ },
23455
24139
  "signed_with": {
23456
24140
  "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
24141
  "type": "string",
@@ -23474,7 +24158,24 @@ const operationManifest = [
23474
24158
  "failure_count",
23475
24159
  "consecutive_fails"
23476
24160
  ]
23477
- },
24161
+ }, {
24162
+ "type": "object",
24163
+ "required": ["warnings"],
24164
+ "properties": { "warnings": {
24165
+ "default": [],
24166
+ "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.",
24167
+ "type": "array",
24168
+ "items": {
24169
+ "type": "object",
24170
+ "properties": {
24171
+ "code": { "type": "string" },
24172
+ "message": { "type": "string" }
24173
+ },
24174
+ "required": ["code", "message"],
24175
+ "additionalProperties": {}
24176
+ }
24177
+ } }
24178
+ }] },
23478
24179
  "sdkName": "updateEndpoint",
23479
24180
  "summary": "Update a webhook endpoint",
23480
24181
  "tag": "Endpoints",
@@ -45670,8 +46371,12 @@ const operationManifest = [
45670
46371
  "type": "boolean",
45671
46372
  "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
46373
  },
46374
+ "retry_scheduled": {
46375
+ "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. Null on every attempt of a delivery retried or settled on a schedule of its own, which this does not report: a held one (its release settles it, including the attempts it makes itself), a pull attempt, a rule-granted one, 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.",
46376
+ "type": ["boolean", "null"]
46377
+ },
45673
46378
  "next_attempt_at": {
45674
- "description": "When the next automatic retry of this delivery is due. Not reported yet: always null.",
46379
+ "description": "When the next automatic retry of this delivery is due. Not reported: always null. Whether one is coming is retry_scheduled.",
45675
46380
  "type": ["string", "null"]
45676
46381
  },
45677
46382
  "attempts": {
@@ -45699,6 +46404,10 @@ const operationManifest = [
45699
46404
  "response_body": {
45700
46405
  "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
46406
  "type": ["string", "null"]
46407
+ },
46408
+ "retry_scheduled": {
46409
+ "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. Null on every attempt of a delivery retried or settled on a schedule of its own, which this does not report: a held one (its release settles it, including the attempts it makes itself), a pull attempt, a rule-granted one, 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.",
46410
+ "type": ["boolean", "null"]
45702
46411
  }
45703
46412
  },
45704
46413
  "required": [
@@ -45709,7 +46418,8 @@ const operationManifest = [
45709
46418
  "duration_ms",
45710
46419
  "created_at",
45711
46420
  "response_status",
45712
- "response_body"
46421
+ "response_body",
46422
+ "retry_scheduled"
45713
46423
  ]
45714
46424
  },
45715
46425
  "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 +46444,7 @@ const operationManifest = [
45734
46444
  "updated_at",
45735
46445
  "email",
45736
46446
  "latest_attempt",
46447
+ "retry_scheduled",
45737
46448
  "next_attempt_at",
45738
46449
  "attempts"
45739
46450
  ]
@@ -45907,10 +46618,14 @@ const operationManifest = [
45907
46618
  "type": "boolean",
45908
46619
  "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
46620
  },
46621
+ "retry_scheduled": {
46622
+ "type": ["boolean", "null"],
46623
+ "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. Null on every attempt of a delivery retried or settled on a schedule of its own, which this does not report: a held one (its release settles it, including the attempts it makes itself), a pull attempt, a rule-granted one, 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."
46624
+ },
45910
46625
  "next_attempt_at": {
45911
46626
  "type": ["string", "null"],
45912
46627
  "format": "date-time",
45913
- "description": "When the next automatic retry of this delivery is due. Not reported yet: always null."
46628
+ "description": "When the next automatic retry of this delivery is due. Not reported: always null. Whether one is coming is retry_scheduled."
45914
46629
  }
45915
46630
  },
45916
46631
  "required": [
@@ -45929,6 +46644,7 @@ const operationManifest = [
45929
46644
  "response_status",
45930
46645
  "response_body",
45931
46646
  "latest_attempt",
46647
+ "retry_scheduled",
45932
46648
  "next_attempt_at"
45933
46649
  ]
45934
46650
  }
@@ -45985,6 +46701,16 @@ const operationManifest = [
45985
46701
  "tagCommand": "webhook-deliveries"
45986
46702
  }
45987
46703
  ];
46704
+ // ../packages/api-core/src/openapi/permissions.generated.ts
46705
+ /**
46706
+ * The keys of the built-in roles, from the spec's `BuiltinRoleKey`.
46707
+ * Template and custom roles use other keys.
46708
+ */
46709
+ const BUILTIN_ROLE_KEYS = [
46710
+ "owner",
46711
+ "admin",
46712
+ "member"
46713
+ ];
45988
46714
  // src/oclif/api-path-commands.ts
45989
46715
  /**
45990
46716
  * The hand-rolled command that covers an operation, where the CLI offers one
@@ -46205,6 +46931,27 @@ const TOKEN_SKEW_MS = 6e4;
46205
46931
  const MAX_CACHED_ORG_TOKENS = 20;
46206
46932
  const USER_LOGIN_EXPIRED_MESSAGE = "Saved Primitive CLI login expired or was revoked. Run `primitive login` to sign in again.";
46207
46933
  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>`.";
46934
+ /**
46935
+ * The CLI login and the organization list report a person's stored role:
46936
+ * the largest built-in role (owner, admin or member) their access fully
46937
+ * includes, or `restricted` when it includes none or they hold no role at
46938
+ * all. The role's own name is not in these answers.
46939
+ */
46940
+ const BUILT_IN_ROLE_KEYS = new Set(BUILTIN_ROLE_KEYS);
46941
+ /** Whether a role key names one of the built-in roles. */
46942
+ function isBuiltInRoleKey(role) {
46943
+ return BUILT_IN_ROLE_KEYS.has(role);
46944
+ }
46945
+ /** What the role key `restricted` stands for. */
46946
+ 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";
46947
+ /** A stored role key, labelled for what it is: "built-in role admin", "role restricted". */
46948
+ function roleKeyLabel(role) {
46949
+ return isBuiltInRoleKey(role) ? `built-in role ${role}` : `role ${role}`;
46950
+ }
46951
+ /** A stored role key for a sentence, saying what `restricted` stands for. */
46952
+ function roleKeyText(role) {
46953
+ return role === "restricted" ? `role restricted (${RESTRICTED_ROLE_NOTE})` : roleKeyLabel(role);
46954
+ }
46208
46955
  const REFUSAL_FALLBACK_MESSAGES = {
46209
46956
  not_a_member: "You are not a member of this organization.",
46210
46957
  cli_access_disabled_by_org: "This organization does not allow CLI access.",
@@ -47104,7 +47851,7 @@ function errorDetail(error) {
47104
47851
  return error instanceof Error ? error.message : String(error);
47105
47852
  }
47106
47853
  /** `count` and the word for it: "1 expectation", "2 expectations". */
47107
- function plural(count, one, many) {
47854
+ function plural$1(count, one, many) {
47108
47855
  return `${count} ${count === 1 ? one : many}`;
47109
47856
  }
47110
47857
  function checkEmailId(id, label) {
@@ -47423,21 +48170,21 @@ function expectationsOutcome(expectations, testResults = []) {
47423
48170
  exitCode: 1,
47424
48171
  error: {
47425
48172
  code: "expectations_failed",
47426
- message: `${plural(failedCount, "expectation", "expectations")} failed${failed.length > 0 ? `: ${failed.join(", ")}` : ""}.`
48173
+ message: `${plural$1(failedCount, "expectation", "expectations")} failed${failed.length > 0 ? `: ${failed.join(", ")}` : ""}.`
47427
48174
  }
47428
48175
  };
47429
48176
  if (unresolvable.length > 0) return {
47430
48177
  exitCode: 1,
47431
48178
  error: {
47432
48179
  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)}.`
48180
+ message: `${plural$1(unresolvable.length, "expectation is", "expectations are")} undecided, and testing again will not decide ${unresolvable.length === 1 ? "it" : "them"}: ${why(unresolvable)}.`
47434
48181
  }
47435
48182
  };
47436
48183
  if (undecidedCount > 0) return {
47437
48184
  exitCode: 3,
47438
48185
  error: {
47439
48186
  code: "expectations_undecided",
47440
- message: `${plural(undecidedCount, "expectation is", "expectations are")} undecided${undecided.length > 0 ? `: ${why(undecided)}` : ""}. Test again later.`
48187
+ message: `${plural$1(undecidedCount, "expectation is", "expectations are")} undecided${undecided.length > 0 ? `: ${why(undecided)}` : ""}. Test again later.`
47441
48188
  }
47442
48189
  };
47443
48190
  return {};
@@ -47501,14 +48248,14 @@ function writtenExpectationsOutcome(check, sentExpectations, ruleId) {
47501
48248
  exitCode: 1,
47502
48249
  error: {
47503
48250
  code: "expectations_failed",
47504
- message: `${plural(failed.length, "expectation", "expectations")} failed on this write: ${failed.join(", ")}.`
48251
+ message: `${plural$1(failed.length, "expectation", "expectations")} failed on this write: ${failed.join(", ")}.`
47505
48252
  }
47506
48253
  };
47507
48254
  if (undecided.length > 0) return {
47508
48255
  exitCode: 3,
47509
48256
  error: {
47510
48257
  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}.`
48258
+ message: `${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
48259
  }
47513
48260
  };
47514
48261
  if (check.passed !== true) return {
@@ -47557,6 +48304,33 @@ function emailRuleScopeReport(data) {
47557
48304
  }
47558
48305
  return { lines };
47559
48306
  }
48307
+ /** The delete-email-rule question, naming the rule. */
48308
+ function deleteEmailRuleQuestion(flags) {
48309
+ return `Delete email rule ${typeof flags.id === "string" ? flags.id : "this rule"}? Its versions and decisions are deleted with it. [y/N] `;
48310
+ }
48311
+ /**
48312
+ * What a rule's delete left behind, read from the answer: the shares it
48313
+ * ended, and the keys that still list the rule and now read nothing through
48314
+ * it. An answer without these fields (an older server) prints nothing.
48315
+ */
48316
+ function deleteEmailRuleReport(data, request) {
48317
+ if (!isRecord(data)) return { lines: [] };
48318
+ const answer = data;
48319
+ const lines = [];
48320
+ const ended = Array.isArray(answer.ended_shares) ? answer.ended_shares.length : 0;
48321
+ 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"}.`);
48322
+ const keys = Array.isArray(answer.keys) ? answer.keys : [];
48323
+ const others = typeof answer.other_keys === "number" && answer.other_keys > 0 ? answer.other_keys : 0;
48324
+ const ruleId = typeof request.path.id === "string" ? request.path.id : "<rule-id>";
48325
+ if (keys.length > 0) {
48326
+ 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:`);
48327
+ for (const key of keys) lines.push(` ${key.id} ${key.name ?? "(no name)"} ${key.key_prefix}`);
48328
+ 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.`);
48329
+ }
48330
+ 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.`);
48331
+ 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.");
48332
+ return { lines };
48333
+ }
47560
48334
  function errorDetails(payload) {
47561
48335
  if (!isRecord(payload)) return {};
47562
48336
  if (isRecord(payload.details)) return payload.details;
@@ -47576,7 +48350,9 @@ function versionMovedHint(payload, ruleId) {
47576
48350
  const read = `Read it again with \`primitive email-rules get-email-rule ${id}\` and reapply your change to what it shows`;
47577
48351
  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
48352
  }
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.";
48353
+ /** A write's --fail-on-expectations, the same on create and update. */
48354
+ 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.";
48355
+ 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.";
47580
48356
  const EMAIL_RULE_OPERATION_EXTENSIONS = {
47581
48357
  testEmailRule: {
47582
48358
  flags: {
@@ -47605,8 +48381,8 @@ const EMAIL_RULE_OPERATION_EXTENSIONS = {
47605
48381
  },
47606
48382
  createEmailRule: {
47607
48383
  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." })
48384
+ "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." }),
48385
+ [FAIL_ON_EXPECTATIONS_FLAG]: Flags.boolean({ description: FAIL_ON_EXPECTATIONS_HELP })
47610
48386
  },
47611
48387
  bodyFields: expectationsBodyFields(false),
47612
48388
  bodyProperties: ["expectations"],
@@ -47615,8 +48391,8 @@ const EMAIL_RULE_OPERATION_EXTENSIONS = {
47615
48391
  },
47616
48392
  updateEmailRule: {
47617
48393
  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." })
48394
+ "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." }),
48395
+ [FAIL_ON_EXPECTATIONS_FLAG]: Flags.boolean({ description: FAIL_ON_EXPECTATIONS_HELP })
47620
48396
  },
47621
48397
  bodyFields: expectationsBodyFields(true),
47622
48398
  bodyProperties: ["expectations"],
@@ -47626,6 +48402,18 @@ const EMAIL_RULE_OPERATION_EXTENSIONS = {
47626
48402
  getEmailRuleScope: {
47627
48403
  flags: {},
47628
48404
  report: emailRuleScopeReport
48405
+ },
48406
+ deleteEmailRule: {
48407
+ flags: { yes: Flags.boolean({
48408
+ char: "y",
48409
+ description: "Delete without asking for confirmation. Needed without a terminal, and with --json."
48410
+ }) },
48411
+ confirm: {
48412
+ question: deleteEmailRuleQuestion,
48413
+ noTerminal: "Not deleting: no terminal to confirm in. Pass --yes to delete the rule without a prompt.",
48414
+ cancelled: "Cancelled. The rule was not deleted."
48415
+ },
48416
+ report: deleteEmailRuleReport
47629
48417
  }
47630
48418
  };
47631
48419
  /** The classification in a listEmailRuleResults answer, or null. */
@@ -47642,15 +48430,208 @@ function noFactsMessage(data) {
47642
48430
  if (classificationOf(data)) return "This email's classification has no facts.";
47643
48431
  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
48432
  }
48433
+ // src/oclif/empty-mail-hint.ts
48434
+ /**
48435
+ * Query parameters that page, shape or modify a list without narrowing which
48436
+ * emails it holds on their own. Any other one the caller set is a filter.
48437
+ */
48438
+ const NOT_FILTERS = /* @__PURE__ */ new Set([
48439
+ "cursor",
48440
+ "since",
48441
+ "limit",
48442
+ "include",
48443
+ "wait",
48444
+ "group",
48445
+ "search_regex",
48446
+ "search_case_sensitive",
48447
+ "search_negate"
48448
+ ]);
48449
+ /** How many addresses, domains or rules the hint names before "and N more". */
48450
+ const NAMED_MAX = 3;
48451
+ /** How long the hint waits for the grant before leaving it out. */
48452
+ const GRANT_TIMEOUT_MS = 2e3;
48453
+ /**
48454
+ * The caller's grant from GET /whoami/grant, or undefined when it cannot be
48455
+ * read in time: the hint then says only that the credential may be narrowed.
48456
+ * Reading mail follows `mail:read`'s own scope where it reaches fewer
48457
+ * mailboxes than the grant's.
48458
+ */
48459
+ async function readCallerMail(client, timeoutMs = GRANT_TIMEOUT_MS) {
48460
+ let timer;
48461
+ const late = new Promise((resolve) => {
48462
+ timer = setTimeout(() => resolve(void 0), timeoutMs);
48463
+ });
48464
+ try {
48465
+ return await Promise.race([readGrant(client, timeoutMs), late]);
48466
+ } finally {
48467
+ clearTimeout(timer);
48468
+ }
48469
+ }
48470
+ async function readGrant(client, timeoutMs) {
48471
+ try {
48472
+ const result = await getOwnGrant({
48473
+ client,
48474
+ fetch: (input, init) => globalThis.fetch(input, init),
48475
+ responseStyle: "fields",
48476
+ signal: AbortSignal.timeout(timeoutMs)
48477
+ });
48478
+ if (result.error) return void 0;
48479
+ const grant = result.data?.data?.grant;
48480
+ if (!grant || typeof grant !== "object") return void 0;
48481
+ const g = grant;
48482
+ const scoped = g.scoped_permissions && typeof g.scoped_permissions === "object" ? g.scoped_permissions["mail:read"] : void 0;
48483
+ return {
48484
+ credentialKind: g.credential_kind,
48485
+ mailScope: scoped ?? g.mail_scope
48486
+ };
48487
+ } catch {
48488
+ return;
48489
+ }
48490
+ }
48491
+ /**
48492
+ * What the caller reads, for the hint: a connected agent's key from the
48493
+ * address it was connected for (it may not ask GET /whoami/grant), any other
48494
+ * credential from GET /whoami/grant.
48495
+ */
48496
+ async function readCallerMailFor(auth, client) {
48497
+ const address = auth.connectedAgent?.agentAddress;
48498
+ if (address) return {
48499
+ credentialKind: "connected_key",
48500
+ mailScope: {
48501
+ addresses: [address],
48502
+ domains: []
48503
+ }
48504
+ };
48505
+ if (auth.apiKey?.startsWith("pconn_")) return {
48506
+ credentialKind: "connected_key",
48507
+ mailScope: void 0
48508
+ };
48509
+ return readCallerMail(client);
48510
+ }
48511
+ /** The query parameters the caller set that narrow which emails are listed. */
48512
+ function listFilters(query) {
48513
+ return Object.keys(query).filter((name) => {
48514
+ const value = query[name];
48515
+ if (NOT_FILTERS.has(name) || value === void 0) return false;
48516
+ const off = value === false || value === "false";
48517
+ if (name.startsWith("exclude_")) return !off;
48518
+ if (name === "include_presence") return off;
48519
+ return true;
48520
+ });
48521
+ }
48522
+ function strings(value) {
48523
+ return Array.isArray(value) && value.every((v) => typeof v === "string") ? value : null;
48524
+ }
48525
+ /** What a mail scope from GET /whoami/grant reaches, as far as the hint needs. */
48526
+ function mailReach(scope) {
48527
+ if (scope === "org") return { kind: "org" };
48528
+ if (scope === "unreadable") return { kind: "unreadable" };
48529
+ if (!scope || typeof scope !== "object" || Array.isArray(scope)) return { kind: "unknown" };
48530
+ const record = scope;
48531
+ const addresses = strings(record.addresses ?? []);
48532
+ const domains = strings(record.domains ?? []);
48533
+ const rules = strings(record.rules ?? []);
48534
+ if (!addresses || !domains || !rules) return { kind: "unknown" };
48535
+ if (record.direction === "outbound" && rules.length === 0) return { kind: "outbound_only" };
48536
+ if (addresses.length === 0 && domains.length === 0) return rules.length > 0 ? {
48537
+ kind: "rules_only",
48538
+ rules
48539
+ } : { kind: "none" };
48540
+ const exceptPersonal = typeof record.excluded_personal_mailboxes === "number" && record.excluded_personal_mailboxes > 0;
48541
+ return {
48542
+ kind: "mailboxes",
48543
+ names: [...addresses, ...domains.map((d) => exceptPersonal ? `addresses at ${d} other than other members' personal mailboxes` : `any address at ${d}`)],
48544
+ rules
48545
+ };
48546
+ }
48547
+ function named(names) {
48548
+ const shown = names.slice(0, NAMED_MAX);
48549
+ const rest = names.length - shown.length;
48550
+ const all = rest > 0 ? [...shown, `${rest} more`] : shown;
48551
+ return all.length <= 1 ? all[0] ?? "" : `${all.slice(0, -1).join(", ")} and ${all[all.length - 1]}`;
48552
+ }
48553
+ const plural = (n, one) => `${n} ${one}${n === 1 ? "" : "s"}`;
48554
+ const GRANT_POINTER = "`primitive whoami --json --grant` shows which mail it reaches.";
48555
+ /**
48556
+ * How a rule grants mail, and who can look at it: a key cannot read rules
48557
+ * itself. An id a key lists is an email rule, or a rule shared with the key's
48558
+ * holder, which its holder checks as a share.
48559
+ */
48560
+ function rulesSentence(rules) {
48561
+ 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>\`.`;
48562
+ }
48563
+ function holderOf(kind) {
48564
+ if (kind === "org_key" || kind === "function_key") return "key";
48565
+ if (kind === "connected_key") return "connection";
48566
+ return typeof kind === "string" ? "person" : "unknown";
48567
+ }
48568
+ const NOUN = {
48569
+ key: "This key",
48570
+ connection: "This connection key",
48571
+ person: "This login",
48572
+ unknown: "This credential"
48573
+ };
48574
+ /** A person also reads mail through rules colleagues shared with them, which sit beside the grant, not in it. */
48575
+ const SHARED = "mail from email rules colleagues shared with you (`primitive email-rules list-email-rule-shares` lists them)";
48576
+ /** A connection key also reads member mailboxes its owner gave it, asked for by name. */
48577
+ const GIVEN = "any member mailbox its owner gave it (pass that mailbox with --recipient)";
48578
+ /** What the credential reads, as sentences for the hint, or null when it reads every mailbox. */
48579
+ function reachSentence(reach, holder) {
48580
+ const noun = NOUN[holder];
48581
+ switch (reach.kind) {
48582
+ case "org": return null;
48583
+ case "unknown": return holder === "connection" ? `${noun} reads the address it was connected for, and ${GIVEN}.` : `${noun} may not read every mailbox; ${GRANT_POINTER}`;
48584
+ 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}`;
48585
+ case "unreadable": return `The server could not read this credential's stored mail scope, so it reaches no mail through it. ${GRANT_POINTER}`;
48586
+ 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}`;
48587
+ case "rules_only": return `${noun} reads only mail its ${plural(reach.rules.length, "email rule")} granted. ${rulesSentence(reach.rules)}`;
48588
+ case "mailboxes": {
48589
+ const parts = [
48590
+ named(reach.names),
48591
+ ...reach.rules.length > 0 ? [`mail its ${plural(reach.rules.length, "email rule")} granted`] : [],
48592
+ ...holder === "connection" ? [GIVEN] : [],
48593
+ ...holder === "person" ? [SHARED] : []
48594
+ ];
48595
+ const all = parts.length === 1 ? parts[0] : `${parts.slice(0, -1).join(", ")}, and ${parts[parts.length - 1]}`;
48596
+ return holder === "connection" ? `${noun} reads only ${all}.` : `${noun} reads ${holder === "person" ? "" : "only "}${all}; other mail is not listed.`;
48597
+ }
48598
+ }
48599
+ }
48600
+ /**
48601
+ * The hint for an empty inbound email list: what was asked (the filter flags
48602
+ * given, or a page or tail past the last email), and what this credential
48603
+ * reads. `caller` is undefined when the grant could not be read, and null
48604
+ * when it was not asked for (a tail poll, which should not double its
48605
+ * requests, or a page with a next cursor, whose hint does not need it).
48606
+ */
48607
+ function emptyEmailListHint(input) {
48608
+ if (input.nextCursor) return `(no results) This page came back empty, but the list goes on: continue with --cursor ${input.nextCursor}.`;
48609
+ const reach = input.caller === null ? { kind: "org" } : mailReach(input.caller?.mailScope);
48610
+ const holder = holderOf(input.caller?.credentialKind);
48611
+ const scope = reachSentence(reach, holder);
48612
+ const withScope = (lead) => `${lead}${scope ? ` ${scope}` : ""}`;
48613
+ const filters = holder === "connection" ? input.filterFlags.filter((f) => !f.startsWith("--include-presence")) : input.filterFlags;
48614
+ if (filters.length > 0) return withScope(`(no results) No inbound emails match the filters given (${filters.join(", ")}).`);
48615
+ 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.");
48616
+ 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`.";
48617
+ const noun = NOUN[holder].replace(/^This/, "this");
48618
+ if (reach.kind === "rules_only") return `(no results) No mail ${noun}'s rules granted is readable now. ${scope}`;
48619
+ if (reach.kind === "mailboxes") return `(no results) No inbound emails yet in the mail ${noun} reads. ${scope}`;
48620
+ return `(no results) No inbound emails to show. ${scope}`;
48621
+ }
47645
48622
  // src/oclif/endpoint-event-types.ts
47646
48623
  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.*";
48624
+ 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.";
48625
+ /** Shorthands the CLI expands, since the server would store them as literals that match nothing. */
48626
+ const WILDCARDS = {
48627
+ "sent_email.*": SENT_EMAIL_EVENT_TYPES,
48628
+ "email_rule_request.*": EMAIL_RULE_REQUEST_EVENT_TYPES
48629
+ };
47649
48630
  const KNOWN_EVENT_TYPES = new Set(WEBHOOK_EVENT_TYPES);
47650
48631
  var EventTypesFlagError = class extends Error {};
47651
48632
  /**
47652
48633
  * 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
48634
+ * commas, trim, expand `sent_email.*` and `email_rule_request.*`, and drop duplicates while keeping the
47654
48635
  * order the caller gave.
47655
48636
  */
47656
48637
  function parseEventTypesFlag(values) {
@@ -47659,14 +48640,14 @@ function parseEventTypesFlag(values) {
47659
48640
  for (const value of values) for (const raw of value.split(",")) {
47660
48641
  const name = raw.trim();
47661
48642
  if (name === "") continue;
47662
- const names = name === SENT_EMAIL_WILDCARD ? [...SENT_EMAIL_EVENT_TYPES] : [name];
48643
+ const names = Object.hasOwn(WILDCARDS, name) ? [...WILDCARDS[name] ?? []] : [name];
47663
48644
  for (const entry of names) {
47664
48645
  if (seen.has(entry)) continue;
47665
48646
  seen.add(entry);
47666
48647
  out.push(entry);
47667
48648
  }
47668
48649
  }
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.");
48650
+ 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
48651
  if (out.length > 50) throw new EventTypesFlagError(`--event-types accepts at most 50 event types (got ${out.length}).`);
47671
48652
  return out;
47672
48653
  }
@@ -47975,6 +48956,92 @@ function formatAlreadySentNotice(sent, options = {}) {
47975
48956
  if (deliveryStatus && deliveryStatus !== status) details.push(`delivery status ${deliveryStatus}`);
47976
48957
  return `Already sent: this exact message went out earlier${details.length > 0 ? ` (${details.join(", ")})` : ""}. Nothing new was sent.`;
47977
48958
  }
48959
+ // src/oclif/member-role-mail.ts
48960
+ /**
48961
+ * Why an invitation for Member was refused, for the refusal the server gives
48962
+ * when every Member reads all of the organization's mail: offering Member is
48963
+ * then giving that, which only an invitation sent from the dashboard by
48964
+ * someone who can assign roles may do.
48965
+ */
48966
+ 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.";
48967
+ /** The same refusal for any role above Member, which is assigning a role: the same advice, without the mail clause. */
48968
+ 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.";
48969
+ /**
48970
+ * The hint for a refused invite-organization-member, or undefined when it is
48971
+ * not that refusal. That one carries no details; a refusal for a grant that
48972
+ * lacks a permission names it in its details, and is left to its own hint.
48973
+ */
48974
+ function inviteRefusalHint(code, body, payload) {
48975
+ if (code !== "grant_forbidden") return void 0;
48976
+ const details = payload && typeof payload === "object" ? payload.details : void 0;
48977
+ if (details && typeof details === "object" && Object.keys(details).length > 0) return void 0;
48978
+ const role = body && typeof body === "object" ? body.role : void 0;
48979
+ if (role === void 0 || role === "member") return MEMBER_INVITE_READS_ALL_MAIL;
48980
+ return role === "none" ? void 0 : ROLE_INVITE_NEEDS_DASHBOARD;
48981
+ }
48982
+ /**
48983
+ * Control characters (C0, DEL and C1), line and paragraph separators and
48984
+ * format characters, which could fake a line of output: a role's name is free
48985
+ * text. Built from code points, as commands/roles.ts builds its pattern, so the
48986
+ * source holds no control character.
48987
+ */
48988
+ 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");
48989
+ /**
48990
+ * The warning for giving someone `role`, from GET /roles, or null when it
48991
+ * reads less or cannot be told. A role, or a template the organization has
48992
+ * not copied yet, reads every mailbox when it holds mail:read over the whole
48993
+ * organization and the organization gives such roles all of its mail (whole-
48994
+ * organization access, which the dashboard also assumes when the answer does
48995
+ * not say).
48996
+ */
48997
+ function roleReadsAllMailWarning(role, listed) {
48998
+ if (!listed || typeof listed !== "object") return null;
48999
+ const data = listed;
49000
+ if ((data.invitations && typeof data.invitations === "object" ? data.invitations : null)?.members_read_all_mail === false) return null;
49001
+ const byKey = (value) => value === role;
49002
+ const byId = (value) => typeof value === "string" && value.toLowerCase() === role.toLowerCase();
49003
+ const roles = Array.isArray(data.roles) ? data.roles : [];
49004
+ const templates = Array.isArray(data.templates) ? data.templates : [];
49005
+ const found = roles.find((r) => byKey(r.key) || byId(r.id)) ?? templates.find((t) => byKey(t.key));
49006
+ const readsMail = Array.isArray(found?.permissions) && found.permissions.includes("mail:read");
49007
+ if (!found || (found.mail_scope ?? "org") !== "org" || !readsMail) return null;
49008
+ 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.`;
49009
+ }
49010
+ /** How long the warning waits for GET /roles before leaving it out. */
49011
+ const ROLES_TIMEOUT_MS = 3e3;
49012
+ /**
49013
+ * GET /roles, or null when it cannot be read in time: the warning is then
49014
+ * left out rather than guessed. The signal stops the request; the race bounds
49015
+ * everything around it too, such as a saved login's token exchange on a 401.
49016
+ */
49017
+ async function readRoles(client, timeoutMs = ROLES_TIMEOUT_MS) {
49018
+ let timer;
49019
+ const late = new Promise((resolve) => {
49020
+ timer = setTimeout(() => resolve(null), timeoutMs);
49021
+ });
49022
+ try {
49023
+ return await Promise.race([fetchRoles(client, timeoutMs), late]);
49024
+ } finally {
49025
+ clearTimeout(timer);
49026
+ }
49027
+ }
49028
+ async function fetchRoles(client, timeoutMs) {
49029
+ try {
49030
+ const result = await client.get({
49031
+ responseStyle: "fields",
49032
+ security: [{
49033
+ scheme: "bearer",
49034
+ type: "http"
49035
+ }],
49036
+ signal: AbortSignal.timeout(timeoutMs),
49037
+ url: "/roles"
49038
+ });
49039
+ if (result.error) return null;
49040
+ return result.data?.data ?? null;
49041
+ } catch {
49042
+ return null;
49043
+ }
49044
+ }
47978
49045
  // src/oclif/runtime-session.ts
47979
49046
  /**
47980
49047
  * The variables each runtime sets to its exact session ID, first match wins.
@@ -48595,7 +49662,7 @@ const HINTS = /* @__PURE__ */ new Set([
48595
49662
  /** A printable `<protocol>/<version>`; anything else is treated as unknown. */
48596
49663
  const KIND = /^[a-z][a-z0-9._-]{0,63}\/[1-9][0-9]{0,3}$/;
48597
49664
  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) {
49665
+ function record$2(value) {
48599
49666
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
48600
49667
  }
48601
49668
  function categoryOf(hint, kind, fyi, candidate) {
@@ -48628,12 +49695,12 @@ function replyExpectation(category) {
48628
49695
  * treated as `none`, as the API documents.
48629
49696
  */
48630
49697
  function readEmailInteraction(detail) {
48631
- const row = record$1(detail);
49698
+ const row = record$2(detail);
48632
49699
  if (!row || typeof row.interaction_hint !== "string") return null;
48633
49700
  const hint = HINTS.has(row.interaction_hint) ? row.interaction_hint : "none";
48634
49701
  const rawKind = row.interaction_kind;
48635
49702
  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;
49703
+ const fyi = row.fyi === true || record$2(row.collaboration)?.fyi === true;
48637
49704
  const category = categoryOf(hint, kind, fyi, row.interaction_candidate === true);
48638
49705
  const expectation = replyExpectation(category);
48639
49706
  return {
@@ -49197,14 +50264,14 @@ function bareAddress$1(value) {
49197
50264
  const address = (/<([^<>\s]+@[^<>\s]+)>\s*$/.exec(value)?.[1] ?? value).trim().toLowerCase();
49198
50265
  return /^[^\s<>@]+@[^\s<>@]+$/.test(address) ? address : null;
49199
50266
  }
49200
- function record(value) {
50267
+ function record$1(value) {
49201
50268
  return value && typeof value === "object" && !Array.isArray(value) ? value : null;
49202
50269
  }
49203
50270
  function newerRows(value) {
49204
50271
  if (!Array.isArray(value)) return void 0;
49205
50272
  const rows = [];
49206
50273
  for (const item of value.slice(-20)) {
49207
- const row = record(item);
50274
+ const row = record$1(item);
49208
50275
  const from = bareAddress$1(row?.from);
49209
50276
  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
50277
  rows.push({
@@ -49240,10 +50307,10 @@ async function readThreadContext(client, threadId, afterEmailId, signal) {
49240
50307
  result = await read();
49241
50308
  }
49242
50309
  if (result.error !== void 0) return null;
49243
- const body = record(result.data);
49244
- const data = record(body?.data);
50310
+ const body = record$1(result.data);
50311
+ const data = record$1(body?.data);
49245
50312
  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));
50313
+ const messages = data.messages.filter((message) => Boolean(record$1(message)) && typeof message.id === "string" && ["inbound", "outbound"].includes(message.direction));
49247
50314
  const count = data.newer_inbound_count;
49248
50315
  const newerInboundCount = typeof count === "number" && Number.isSafeInteger(count) && count >= 0 ? count : void 0;
49249
50316
  const newerInbound = newerInboundCount === void 0 ? void 0 : newerRows(data.newer_inbound);
@@ -49307,7 +50374,7 @@ const SERVER_RELATIONSHIPS = {
49307
50374
  };
49308
50375
  /** The `collaboration` object of an email read, when the server sends one. */
49309
50376
  function collaborationOf(detail) {
49310
- return record(record(detail)?.collaboration);
50377
+ return record$1(record$1(detail)?.collaboration);
49311
50378
  }
49312
50379
  /**
49313
50380
  * The sender relationship the server computed for this email
@@ -49327,7 +50394,7 @@ function serverRelationship(detail) {
49327
50394
  * older server sends neither and nothing is muted by it.
49328
50395
  */
49329
50396
  function serverMuted(detail) {
49330
- return collaborationOf(detail)?.muted === true || record(detail)?.muted === true;
50397
+ return collaborationOf(detail)?.muted === true || record$1(detail)?.muted === true;
49331
50398
  }
49332
50399
  /**
49333
50400
  * An address fit to print as trusted metadata, or `unavailable`. The
@@ -49463,7 +50530,7 @@ function savedConnectedProfiles(configDir, exclude) {
49463
50530
  */
49464
50531
  async function otherProfiles(configDir, session, current) {
49465
50532
  if (session) try {
49466
- const { connectedProfilesForSession } = await import("./machine-session-BIOGBT9g.js").then((n) => n.s);
50533
+ const { connectedProfilesForSession } = await import("./machine-session-3H-rNH8v.js").then((n) => n.s);
49467
50534
  return {
49468
50535
  inSession: true,
49469
50536
  profiles: connectedProfilesForSession(configDir, session, current)
@@ -49537,6 +50604,56 @@ async function explainPendingMailNotFound(configDir, emailId, write = (line) =>
49537
50604
  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
50605
  } catch {}
49539
50606
  }
50607
+ 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.";
50608
+ const RULE_REQUEST_WEBHOOK_HTTP_HINT = "Hint: rule request events go only to an http webhook (kind http).";
50609
+ 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.";
50610
+ 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.";
50611
+ 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.";
50612
+ 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).";
50613
+ const RULE_REQUEST_EVENT_PREFIX = "email_rule_request.";
50614
+ function record(value) {
50615
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
50616
+ }
50617
+ /**
50618
+ * Whether a create or update body lists any rule request event. Matched by
50619
+ * prefix, as the API matches it, so a raw body's literal
50620
+ * `email_rule_request.*` counts too.
50621
+ */
50622
+ function listsRuleRequestEvents(body) {
50623
+ const types = record(record(body)?.rules)?.event_types;
50624
+ return Array.isArray(types) && types.some((t) => typeof t === "string" && t.startsWith(RULE_REQUEST_EVENT_PREFIX));
50625
+ }
50626
+ function refusalReason(payload) {
50627
+ const top = record(payload);
50628
+ return record(top?.details)?.reason ?? record(record(top?.error)?.details)?.reason;
50629
+ }
50630
+ /**
50631
+ * The hint after a refused createEndpoint, or undefined when none applies.
50632
+ * The one refusal reason covers a webhook given to a key, one that is not an
50633
+ * http webhook, and one made by anyone but a person signed in as themself (or
50634
+ * whose membership could not be confirmed), told apart by what was sent.
50635
+ */
50636
+ function createEndpointRuleRequestHint(code, body, payload) {
50637
+ const asked = record(body);
50638
+ if (refusalReason(payload) === "rule_request_events_need_own_webhook") {
50639
+ if (asked && "owner_key_id" in asked) return RULE_REQUEST_WEBHOOK_KEY_HINT;
50640
+ if (asked?.kind !== void 0 && asked.kind !== "http") return RULE_REQUEST_WEBHOOK_HTTP_HINT;
50641
+ return RULE_REQUEST_WEBHOOK_CREATE_HINT;
50642
+ }
50643
+ if (code === "validation_error" && listsRuleRequestEvents(body) && (asked?.is_route_target === false || asked?.takeover !== void 0)) return RULE_REQUEST_WEBHOOK_DEFAULT_HINT;
50644
+ }
50645
+ /**
50646
+ * The hint after a refused updateEndpoint, or undefined when none applies.
50647
+ * The one refusal reason covers three changes, told apart by what was sent:
50648
+ * the events added to an existing webhook, the webhook given to a key, and
50649
+ * any other change by someone who is not its maker acting as themself.
50650
+ */
50651
+ function updateEndpointRuleRequestHint(_code, body, payload) {
50652
+ if (refusalReason(payload) !== "rule_request_events_need_own_webhook") return void 0;
50653
+ const asked = record(body);
50654
+ if (asked && "owner_key_id" in asked) return RULE_REQUEST_WEBHOOK_KEY_HINT;
50655
+ return listsRuleRequestEvents(body) ? RULE_REQUEST_WEBHOOK_SUBSCRIBE_HINT : RULE_REQUEST_WEBHOOK_CHANGE_HINT;
50656
+ }
49540
50657
  // src/oclif/sent-list-labels.ts
49541
50658
  /**
49542
50659
  * `sent list` under a connected agent profile returns this address's own
@@ -50232,7 +51349,7 @@ function forbiddenHint(payload) {
50232
51349
  if (!permission || record.reason !== "permission_missing") return null;
50233
51350
  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
51351
  }
50235
- function writeErrorWithHints(payload, response) {
51352
+ function writeErrorWithHints(payload, response, options = {}) {
50236
51353
  const formatted = formatErrorPayload(payload);
50237
51354
  process.stderr.write(`${formatted}\n`);
50238
51355
  for (const line of cliCommandLines(formatted)) process.stderr.write(`${line}\n`);
@@ -50241,7 +51358,7 @@ function writeErrorWithHints(payload, response) {
50241
51358
  process.stderr.write(`${rateLimitHint(response)}\n`);
50242
51359
  return;
50243
51360
  }
50244
- if (!code) return;
51361
+ if (!code || options.codeHint === false) return;
50245
51362
  if (code === "grant_forbidden") {
50246
51363
  process.stderr.write(`${grantForbiddenHint(payload)}\n`);
50247
51364
  return;
@@ -50412,6 +51529,7 @@ function buildFlags(operation) {
50412
51529
  description: ATTACHMENT_WAIT_FLAG_DESCRIPTION
50413
51530
  });
50414
51531
  const extension = OPERATION_EXTENSIONS[operation.sdkName];
51532
+ if (extension?.confirm && !("yes" in extension.flags)) throw new Error(`${operation.sdkName}: a CLI extension that asks for confirmation must add --yes`);
50415
51533
  for (const [name, flag] of Object.entries(extension?.flags ?? {})) {
50416
51534
  if (flags[name] !== void 0) throw new Error(`${operation.sdkName}: --${name} is both a generated flag and a CLI extension flag`);
50417
51535
  flags[name] = flag;
@@ -50485,6 +51603,7 @@ function domainVerificationFailedHint(payload) {
50485
51603
  ].join("\n");
50486
51604
  }
50487
51605
  const OPERATION_HINTS = {
51606
+ 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
51607
  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
51608
  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
51609
  downloadDomainZoneFile: "Tip: prefer `primitive domains zone-file --id <domain-id> --output <domain>.zone` for CLI-friendly file output.",
@@ -50500,7 +51619,7 @@ const OPERATION_HINTS = {
50500
51619
  deleteMemory: "Tip: prefer `primitive memories delete <key> [--function <function-id>]` for the common delete path.",
50501
51620
  searchMemories: "Tip: prefer `primitive memories search [prefix] [--metadata-only] [--function <function-id>]` for the common search path.",
50502
51621
  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.",
51622
+ 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
51623
  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
51624
  };
50506
51625
  const OPERATION_FLAG_ALIASES = { verifyAgentSignup: { verification_code: ["code"] } };
@@ -50515,6 +51634,9 @@ const OPERATION_EXTENSIONS = {
50515
51634
  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
51635
  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
51636
  const OPERATION_ERROR_HINTS = {
51637
+ inviteOrganizationMember: (code, body, payload) => inviteRefusalHint(code, body, payload),
51638
+ createEndpoint: (code, body, payload) => createEndpointRuleRequestHint(code, body, payload),
51639
+ updateEndpoint: (code, body, payload) => updateEndpointRuleRequestHint(code, body, payload),
50518
51640
  updateEmailRule: (code, _body, payload, path) => code === "email_rule_conflict" ? versionMovedHint(payload, path?.id) : void 0,
50519
51641
  verifyDomain: (code, _body, payload) => code === "dns_verification_failed" ? domainVerificationFailedHint(payload) : void 0,
50520
51642
  addDomain: (code, body) => {
@@ -50523,6 +51645,7 @@ const OPERATION_ERROR_HINTS = {
50523
51645
  if (code === "feature_disabled" && mailRelay) return MAIL_RELAY_NOT_ENABLED_MESSAGE;
50524
51646
  }
50525
51647
  };
51648
+ const OPERATION_ERROR_HINTS_REPLACING_CODE_HINT = /* @__PURE__ */ new Set(["inviteOrganizationMember"]);
50526
51649
  const OPERATION_SUCCESS_HOOKS = { verifyAgentSignup: ({ envelope, configDir, apiBaseUrl, writeStderr }) => {
50527
51650
  const data = envelope?.data;
50528
51651
  if (!data?.access_token || !data?.refresh_token) return;
@@ -50566,6 +51689,16 @@ function createOperationCommand(operation) {
50566
51689
  if (value === void 0 || value === "") throw new Errors.CLIError(`Missing the ${positionalFlag}: pass it as the first argument or with --${positionalFlag}.`, { exit: 2 });
50567
51690
  parsedFlags[positionalFlag] = value;
50568
51691
  }
51692
+ const confirm = extension?.confirm;
51693
+ if (confirm && parsedFlags.yes !== true) {
51694
+ if (parsedFlags.json === true || !(process.stdin.isTTY && process.stderr.isTTY)) throw new Errors.CLIError(confirm.noTerminal, { exit: 2 });
51695
+ const answer = await promptLine(confirm.question(parsedFlags));
51696
+ if (!/^y(es)?$/i.test(answer.trim())) {
51697
+ process.stderr.write(`${confirm.cancelled}\n`);
51698
+ process.exitCode = 1;
51699
+ return;
51700
+ }
51701
+ }
50569
51702
  const headers = collectHeaderFlags(parsedFlags, headerFlagToName);
50570
51703
  if ([
50571
51704
  "startAgentSignup",
@@ -50693,6 +51826,11 @@ function createOperationCommand(operation) {
50693
51826
  });
50694
51827
  }
50695
51828
  };
51829
+ if (operation.sdkName === "setMemberRole") {
51830
+ const role = body && typeof body === "object" ? body.role : void 0;
51831
+ const warning = typeof role === "string" ? roleReadsAllMailWarning(role, await readRoles(apiClient.client)) : null;
51832
+ if (warning) process.stderr.write(`${warning}\n`);
51833
+ }
50696
51834
  const result = paged ? await callPaged() : takesStepUp(operation) ? await callWithApproval() : ATTACHMENT_WAIT_OPERATIONS.has(operation.sdkName) ? await withAttachmentWait(callOperation, {
50697
51835
  wait: parsedFlags.wait !== false,
50698
51836
  errorCode: (error) => extractErrorCode(extractErrorPayload(error)),
@@ -50702,8 +51840,8 @@ function createOperationCommand(operation) {
50702
51840
  }) : await callOperation();
50703
51841
  if (result.error) {
50704
51842
  const errorPayload = extractErrorPayload(result.error);
50705
- writeErrorWithHints(errorPayload, result.response);
50706
51843
  const operationErrorHint = OPERATION_ERROR_HINTS[operation.sdkName]?.(extractErrorCode(errorPayload), body, errorPayload, collectValues(operation.pathParams, parsedFlags, operation.sdkName));
51844
+ writeErrorWithHints(errorPayload, result.response, { codeHint: !(operationErrorHint && OPERATION_ERROR_HINTS_REPLACING_CODE_HINT.has(operation.sdkName)) });
50707
51845
  if (operationErrorHint) process.stderr.write(`${operationErrorHint}\n`);
50708
51846
  if (stepUpUrl) process.stderr.write(approveThenRerunHint(stepUpUrl));
50709
51847
  if (replyStateSurface && isAwaitingRejectedError(errorPayload)) process.stderr.write(`${awaitingRejectedError(replyStateSurface).message}\n`);
@@ -50779,11 +51917,17 @@ function createOperationCommand(operation) {
50779
51917
  if (cursor && !jsonOutput) process.stderr.write(`next cursor: ${cursor}\n`);
50780
51918
  if (pagingNote && !jsonOutput) process.stderr.write(`${pagingNote}\n`);
50781
51919
  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`);
51920
+ if (Array.isArray(envelope?.data) && envelope.data.length === 0 && (!jsonOutput || withEnvelope)) {
51921
+ const tail = query.since !== void 0;
51922
+ const more = envelope?.meta?.cursor ?? null;
51923
+ const hint = operation.sdkName === "listEmails" ? emptyEmailListHint({
51924
+ filterFlags: listFilters(query).map((name) => `--${parameterFlagName(operation.sdkName, name)}`),
51925
+ ...tail ? { pastFlag: "--since" } : query.cursor !== void 0 ? { pastFlag: "--cursor" } : {},
51926
+ nextCursor: tail ? null : more,
51927
+ caller: tail || more ? null : await readCallerMailFor(auth, apiClient.client)
51928
+ }) : EMPTY_RESULT_HINTS[operation.sdkName];
51929
+ if (hint && jsonOutput) summary.push(hint);
51930
+ else if (hint) process.stderr.write(`${hint}\n`);
50787
51931
  }
50788
51932
  if (operation.sdkName === "listSentEmails") {
50789
51933
  const note = sentListOtherSendersNote(envelope?.data, auth.connectedAgent?.agentAddress);
@@ -50854,7 +51998,6 @@ function createOperationCommand(operation) {
50854
51998
  const EMPTY_RESULT_HINTS = {
50855
51999
  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
52000
  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
52001
  listDomains: "(no results) No domains on this account. Add one with `primitive domains add --domain <yourdomain.example>`.",
50859
52002
  listFilters: "(no results) No filter rules configured.",
50860
52003
  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 +52005,4 @@ const EMPTY_RESULT_HINTS = {
50862
52005
  function canonicalizeCliReferences(description) {
50863
52006
  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
52007
  }
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 };
52008
+ export { replyExpectation as $, createCliApiClient 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, isEmailId as Gt, wakeRelationship as H, readCallerMailFor as Ht, sentInThread as I, detectAgentRuntime as It, recordPendingMail as J, ATTACHMENT_WAIT_MAX_MS as Jt, pendingMailPath as K, noFactsMessage 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, createAuthenticatedCliApiClient as Qt, serverRelationship as R, runtimeSessionVariableList as Rt, promptLine as S, sendOutcomeExitCode as St, canOfferCopyHotkey as T, serializeErrorPayload as Tt, claimPendingMail as U, classificationFactLines as Ut, wakeRecipientField as V, emptyEmailListHint as Vt, clearConsumedPendingMail as W, classificationOf as Wt, interactionHumanLine as X, ConnectedAgentKeyRequiredError as Xt, removePendingMail as Y, withAttachmentWait as Yt, interactionNextActions as Z, SAVED_CLI_OAUTH_SESSION_EXPIRED_MESSAGE as Zt, writeErrorWithHints as _, formatSendFailureSummary as _t, createOperationCommand as a, RESTRICTED_ROLE_NOTE as an, apiResultHttpStatus as at, STEP_UP_TOKEN_FLAG_DESCRIPTION as b, reportSendCommandResult as bt, formatErrorPayload as c, isBuiltInRoleKey as cn, buildThrownSendFailureEnvelope as ct, isGrantRefusal as d, roleKeyText as dn, deriveSendIdempotencyKey as dt, refreshStoredCliCredentials as en, replyInteractionWarning as et, parameterFlagName as f, switchCurrentOrg as fn, excludeSignalReplies as ft, takesStepUp as g, operationManifest as gn, formatPriorRepliesWarning as gt, surfaceUnauthorizedHint as h, setKnownCommands as hn, formatPriorRepliesCheckSkipped as ht, TIME_FLAG_DESCRIPTION as i, CliOrgRefusedError as in, SEND_OUTCOME_HELP as it, describeWake as j, defaultConnectionName as jt, explainPendingMailNotFound as k, cliInvocation as kt, hasControlCharacter as l, revokeUserLogin as ln, checkPriorReplies as lt, runWithTiming as m, sanitizeForTerminal as mn, formatDeletedEarlierSendNotice as mt, API_ERROR_CODES as n, resolveCliApiRequestConfig as nn, IDEMPOTENCY_KEY_FLAG_DESCRIPTION as nt, extractErrorCode as o, credentialsFromUserLogin as on, assertValidIdempotencyKey as ot, readTextFileFlag as p, routeWarningLines as pn, followUpCommandPrefix as pt, readPendingMail as q, ATTACHMENT_WAIT_FLAG_DESCRIPTION as qt, OPERATION_PATH_FLAGS as r, selectedOrgFromEnv as rn, MAX_SEND_WAIT_TIMEOUT_MS as rt, extractErrorPayload as s, fetchCliOrgs as sn, buildFollowUpCommand as st, API_BASE_URL_FLAG_DESCRIPTION as t, refuseOrgSelectionForProfile as tn, wakeInteractionSentence as tt, headerFlagName as u, roleKeyLabel 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 };