millionsend 0.5.0 → 0.6.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d9260a596d597a0aad944da04656fb5c99606374b48b9e235f3e883917ef2bc6
4
- data.tar.gz: 677e347b5604bd68edf198df96802d652470d3e4aef679460fdb24bce3563993
3
+ metadata.gz: 0d6e46f436a0805ef763edac14bbd254e6f17503c598a8374b6abe9f45efcc76
4
+ data.tar.gz: 04c8ab67229c03c8769c7f9ef872b0e11b98d8aa8919a21c0d4c6ce6cc0582f9
5
5
  SHA512:
6
- metadata.gz: fb4c0031b1c2b22911ebba5b0f5d45c05b613196b2e665c11212d5146c5e7102c4cdbba343245acf814bffc6e8394d603c2c124d325a25c4c70fc1a49ac21789
7
- data.tar.gz: bae43393a0387f459f882638a9eb6a7961cdc6d9b94b602d914cd199c071ce7e403706610e89e1426273e347cb1a3d6393518cf7d9f91d567352692c8765f2fe
6
+ metadata.gz: ea4ff1283155e92f65aea2088a6ae4abdfd5032b3c93c243eb838a30ab141070c328bc7d2a278e0399894ec1e18300abef83a727ace9ed48a4b7e3271941e284
7
+ data.tar.gz: e845a9d24731c584b931a0de44fd7f0adee3760bcaa8d45d55412e301c6e9ad545cc1d62e294938e8adbdba07ebc204d156a96584d2d5b16fc721826853137dc
data/README.md CHANGED
@@ -128,7 +128,12 @@ Millionsend::Contacts.list(limit: 50)
128
128
  Millionsend::Contacts::Topics.update(email: "ada@acme.dev", topics: [{ id: topic_id, subscription: "opt_out" }]) # resend-ruby shape
129
129
  Millionsend::Contacts.topics_update("ada@acme.dev", [{ id: topic_id, subscription: "opt_out" }]) # positional
130
130
  Millionsend::Contacts::Topics.list(email: "ada@acme.dev") # GET /contacts/:id/topics — every topic with its effective
131
- # subscription; explicit: false when it is the topic's default
131
+ # subscription (explicit: false when it is the topic's default)
132
+ # and its visibility ("public" | "private")
133
+
134
+ # Preference-center link (MillionSend extension) — POST /contacts/:id/preferences-link
135
+ link = Millionsend::Contacts.preferences_link("ada@acme.dev") # by id or email; also preferences_link(id: ...) / (email: ...)
136
+ link[:url] # the contact's hosted preference page; no expiry, so show it only to that contact
132
137
 
133
138
  # Segment membership — POST / DELETE /contacts/:id/segments/:segment_id
134
139
  Millionsend::Contacts::Segments.add("ada@acme.dev", segment_id)
@@ -143,6 +148,10 @@ result = Millionsend::Contacts::Batch.create(
143
148
  result[:data] # [{ index:, id:, status: "created" | "updated" | "skipped" }]
144
149
  result[:counts] # { created:, updated:, skipped:, failed: }
145
150
  result[:errors] # permissive mode only: [{ index:, message: }]
151
+
152
+ # Bulk delete (MillionSend extension) — up to 1000 per call, exactly one of ids: / emails:
153
+ Millionsend::Contacts::Batch.remove(emails: ["a@acme.dev", "b@acme.dev"]) # or ids: [...]
154
+ # => { data: [{ object: "contact", contact: "<uuid>", deleted: true }, ...] } — only the rows actually deleted
146
155
  ```
147
156
 
148
157
  Contacts are addressable by id or email; when an `update` hash carries both, the email wins.
@@ -237,14 +246,26 @@ hook = Millionsend::Webhooks.create(
237
246
  signing_secret: "whsec_..." # optional: reuse an existing secret so the receiver keeps verifying
238
247
  )
239
248
  hook[:signing_secret]
240
- Millionsend::Webhooks.get(hook[:id]) # also returns signing_secret
249
+ Millionsend::Webhooks.get(hook[:id]) # also returns signing_secret and previous_secret_expires_at
241
250
  Millionsend::Webhooks.list
242
251
  Millionsend::Webhooks.update(hook[:id], events: ["email.opened"], status: "disabled")
243
252
  Millionsend::Webhooks.remove(hook[:id])
253
+
254
+ # Rotate the signing secret (MillionSend extension) — POST /webhooks/:id/rotate
255
+ rotated = Millionsend::Webhooks.rotate(hook[:id]) # mints a new secret, 24h overlap
256
+ rotated = Millionsend::Webhooks.rotate(hook[:id], signing_secret: "whsec_...", overlap_hours: 0) # bring your own, no overlap
257
+ rotated[:signing_secret] # the secret now signing deliveries
258
+ rotated[:previous_secret_expires_at] # ISO time until which the old secret also signs, or nil
244
259
  ```
245
260
 
261
+ During the overlap window (`overlap_hours`, 0–72, default 24) every delivery carries both
262
+ signatures, so a receiver holding either verifies; `Webhooks.get` reports the window's end
263
+ as `previous_secret_expires_at` (`nil` when none is open).
264
+
246
265
  Events: `email.sent`, `email.delivered`, `email.delivery_delayed`, `email.bounced`,
247
- `email.complained`, `email.opened`, `email.clicked`, `deliverability.warning`,
266
+ `email.complained`, `email.opened`, `email.clicked`, `contact.created`, `contact.updated`,
267
+ `contact.deleted`, `contact.unsubscribed`, `contact.resubscribed`, `contact.topic_opt_in`,
268
+ `contact.topic_opt_out`, `suppression.added`, `suppression.removed`, `deliverability.warning`,
248
269
  `deliverability.paused`, `quota.warning`, `quota.reached`, `quota.paused`.
249
270
 
250
271
  ### API keys
@@ -368,8 +389,9 @@ works as in resend-ruby. Notes:
368
389
  Resend's audiences alias.
369
390
  - Not in the API (yet), so not here: broadcast recipients/clicked links, email sharing and
370
391
  metrics, contact imports, receiving, automations, logs, OAuth grants, webhook event replay.
371
- - MillionSend extensions with no Resend counterpart: `Segments`, `Contacts::Batch`, `Usage`,
372
- `Deliverability`, `Emails.get_insights`, `Suppressions` `origin: "unsubscribe"`.
392
+ - MillionSend extensions with no Resend counterpart: `Segments`, `Contacts::Batch`,
393
+ `Contacts.preferences_link`, `Webhooks.rotate`, `Usage`, `Deliverability`,
394
+ `Emails.get_insights`, `Suppressions` `origin: "unsubscribe"`.
373
395
 
374
396
  ## License
375
397
 
@@ -42,6 +42,14 @@ module Millionsend
42
42
  Millionsend::Request.new(method: :patch, path: "#{member_path(id_or_email)}/topics", body: topics).perform
43
43
  end
44
44
 
45
+ # POST /contacts/:id_or_email/preferences-link — the contact's hosted
46
+ # preference page, { object: "preferences_link", contact:, url: }. The
47
+ # url is a contact-scoped capability with no expiry: hand it only to that
48
+ # contact. 422 when the instance cannot build hosted links.
49
+ def preferences_link(id_or_email)
50
+ Millionsend::Request.new(method: :post, path: "#{member_path(id_or_email)}/preferences-link").perform
51
+ end
52
+
45
53
  # Every member method also accepts resend-ruby's addressing hash
46
54
  # ({ id: } / { email: } / { contact_id: }) in place of the bare value.
47
55
  def member_path(id_or_email)
@@ -54,9 +62,10 @@ module Millionsend
54
62
  module Topics
55
63
  class << self
56
64
  # GET /contacts/:id_or_email/topics — { id: | email: } or a bare id/email.
57
- # Every topic comes back with the contact's effective subscription;
58
- # explicit: false means it is the topic's default, not a stored choice.
59
- # Unpaginated, like Topics.list.
65
+ # Every topic comes back with the contact's effective subscription
66
+ # (explicit: false means it is the topic's default, not a stored
67
+ # choice) and its visibility ("public" | "private"). Unpaginated, like
68
+ # Topics.list.
60
69
  def list(params)
61
70
  Millionsend::Request.new(method: :get, path: "#{Millionsend::Contacts.member_path(params)}/topics").perform
62
71
  end
@@ -68,7 +77,8 @@ module Millionsend
68
77
  end
69
78
  end
70
79
 
71
- # Bulk contact creation — a MillionSend extension (Resend imports via CSV).
80
+ # Bulk contact creation and deletion — MillionSend extensions (Resend
81
+ # imports via CSV and deletes one at a time).
72
82
  module Batch
73
83
  class << self
74
84
  # POST /contacts/batch with a bare array of up to 1000 create payloads.
@@ -85,6 +95,14 @@ module Millionsend
85
95
  **Millionsend::Util.request_options(options)
86
96
  ).perform
87
97
  end
98
+
99
+ # POST /contacts/batch/remove — { ids: [...] } or { emails: [...] }
100
+ # (exactly one, up to 1000). Returns { data: [{ object:, contact:,
101
+ # deleted: true }] } listing only the rows actually deleted; unknown
102
+ # ids or addresses are skipped.
103
+ def remove(params)
104
+ Millionsend::Request.new(method: :post, path: "/contacts/batch/remove", body: params).perform
105
+ end
88
106
  end
89
107
  end
90
108
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Millionsend
4
- VERSION = "0.5.0"
4
+ VERSION = "0.6.0"
5
5
  end
@@ -10,7 +10,8 @@ module Millionsend
10
10
  Millionsend::Request.new(method: :post, path: "/webhooks", body: params).perform
11
11
  end
12
12
 
13
- # GET /webhooks/:id — includes signing_secret.
13
+ # GET /webhooks/:id — includes signing_secret and
14
+ # previous_secret_expires_at (nil unless a rotation's overlap window is open).
14
15
  def get(id)
15
16
  Millionsend::Request.new(method: :get, path: member_path(id)).perform
16
17
  end
@@ -30,6 +31,14 @@ module Millionsend
30
31
  Millionsend::Request.new(method: :delete, path: member_path(id)).perform
31
32
  end
32
33
 
34
+ # POST /webhooks/:id/rotate — signing_secret (optional whsec_ value to
35
+ # bring your own; omitted mints one) and overlap_hours (0..72, default
36
+ # 24) during which deliveries carry both signatures. Returns { object:,
37
+ # id:, signing_secret:, previous_secret_expires_at: }.
38
+ def rotate(id, params = {})
39
+ Millionsend::Request.new(method: :post, path: "#{member_path(id)}/rotate", body: params).perform
40
+ end
41
+
33
42
  private
34
43
 
35
44
  def member_path(id)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: millionsend
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - MillionSend