spree_api 5.6.0.rc2 → 5.6.0.rc4

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.
Files changed (26) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/concerns/spree/api/v3/admin/auth_cookies.rb +8 -2
  3. data/app/controllers/concerns/spree/api/v3/channel_resolution.rb +54 -8
  4. data/app/controllers/concerns/spree/api/v3/error_handler.rb +4 -0
  5. data/app/controllers/spree/api/v3/admin/api_keys_controller.rb +5 -5
  6. data/app/controllers/spree/api/v3/admin/customers/addresses_controller.rb +29 -23
  7. data/app/controllers/spree/api/v3/store/base_controller.rb +7 -2
  8. data/app/controllers/spree/api/v3/store/carts_controller.rb +6 -1
  9. data/app/controllers/spree/api/v3/store/channel_controller.rb +31 -0
  10. data/app/controllers/spree/api/v3/store/customer/addresses_controller.rb +8 -0
  11. data/app/controllers/spree/api/v3/store/resource_controller.rb +12 -0
  12. data/app/jobs/spree/webhook_delivery_job.rb +6 -1
  13. data/app/serializers/spree/api/v3/admin/api_key_serializer.rb +6 -1
  14. data/app/serializers/spree/api/v3/admin/customer_group_serializer.rb +3 -4
  15. data/app/serializers/spree/api/v3/admin/webhook_delivery_serializer.rb +6 -1
  16. data/app/serializers/spree/api/v3/cart_serializer.rb +8 -1
  17. data/app/serializers/spree/api/v3/channel_serializer.rb +9 -1
  18. data/app/serializers/spree/api/v3/customer_group_serializer.rb +15 -0
  19. data/app/serializers/spree/api/v3/customer_serializer.rb +8 -0
  20. data/app/serializers/spree/api/v3/product_serializer.rb +1 -1
  21. data/app/services/spree/webhooks/deliver_webhook.rb +6 -2
  22. data/app/subscribers/spree/webhook_event_subscriber.rb +5 -2
  23. data/config/locales/en.yml +2 -0
  24. data/config/routes.rb +3 -0
  25. data/lib/spree/api/dependencies.rb +1 -0
  26. metadata +8 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0b165c94c2975472728b228761cc6cb41f1a677a6368dc9a0be992b5840444cd
4
- data.tar.gz: 1ee381dfe91a8ccc58e959cec117f1beb992dde08b6c04488e18ea45baf1ec3c
3
+ metadata.gz: ca50a0fac8c0d346157c3d42a2969414d5f4e8a392aae87176cb02069c828e21
4
+ data.tar.gz: dc56eff71e891ef304710a9a2f2bfc7c72be17a50d0a24ba1335b137ea817057
5
5
  SHA512:
6
- metadata.gz: 71038600173f2b56747b328f32fab9030ade806d07f3823e1e3e261098f85bdf24d54b0e8c7eb131e669a1f856bbb84c0d6cd3bba2974eef2739c87b73e27ddd
7
- data.tar.gz: 8847edd08dbe74a437313d47add4fc557f5930dfdce57cca0b8812249e1f9ea40cc2416aef628e97a13a999777031bd6de381532a4093bceb6311f554854acb8
6
+ metadata.gz: e1097d160a66eac8cee1e1b9169f06781c4105ddb01fce2bf560bcf0c94aeb7e0f947bd36675e17e7b6532af02c47999d242f23fb8359abaa866d3257775afd0
7
+ data.tar.gz: a48b3317819fd03b05d71b016a7856b5c1a8da5d238080964dd9980b0eef328f2a712aed8a903efffde9a86ee1c902903495aee339d01e4a5e216fae0886ffb7
@@ -10,7 +10,7 @@ module Spree
10
10
  # CSRF protection:
11
11
  # We deliberately do NOT use a CSRF token here. The threat model is fully
12
12
  # covered by the combination of:
13
- # - SameSite=Lax (dev) / SameSite=None; Secure (prod) on the refresh cookie
13
+ # - SameSite=Lax (http) / SameSite=None; Secure (https) on the refresh cookie
14
14
  # - Spree::AllowedOrigin allowlist enforced via Rack::Cors with credentials: true
15
15
  # - CORS preflight blocking cross-origin requests from non-allowlisted Origins
16
16
  # A double-submit CSRF token would only add value if the AllowedOrigin allowlist
@@ -48,8 +48,14 @@ module Spree
48
48
  cookies.signed[REFRESH_COOKIE_NAME].presence
49
49
  end
50
50
 
51
+ # Keyed off the request scheme, not the Rails env: Rails silently refuses to
52
+ # write a Secure cookie on a non-SSL request, so a production app served over
53
+ # plain http (local Docker image, http-only intranet) would never get the
54
+ # refresh cookie at all. SameSite=None requires Secure, so http falls back to
55
+ # Lax — fine for same-site setups (ports don't factor into site identity);
56
+ # cross-site dashboards need https anyway.
51
57
  def base_cookie_attributes
52
- if Rails.env.production?
58
+ if request.ssl?
53
59
  { secure: true, same_site: :none }
54
60
  else
55
61
  { secure: false, same_site: :lax }
@@ -6,41 +6,87 @@ module Spree
6
6
  # read channel context without threading it through method args.
7
7
  #
8
8
  # Resolution order:
9
- # 1. +X-Spree-Channel+ header value matched against +channels.code+ —
9
+ # 1. The authenticated publishable key's channel binding
10
+ # (+Spree::ApiKey#channel+) — server-assigned, cannot be overridden;
11
+ # a +X-Spree-Channel+ header naming a different channel is a 422
12
+ # 2. +X-Spree-Channel+ header value matched against +channels.code+ —
10
13
  # or, if it looks like a prefixed ID (+ch_…+), against +channels.id+
11
14
  # — scoped to the current store
12
- # 2. +current_store.default_channel+
15
+ # 3. +current_store.default_channel+
13
16
  #
14
17
  # The concern is a no-op if no channel matches — callers fall back to
15
18
  # +Spree::Current.channel+'s store-default behavior.
19
+ #
20
+ # IMPORTANT: key-bound resolution requires +authenticate_api_key!+ to run
21
+ # BEFORE +set_current_channel+ — both Store API branches order their
22
+ # callbacks accordingly (prepended in Store::BaseController, inherited
23
+ # ahead of the include in Store::ResourceController).
16
24
  module ChannelResolution
17
25
  extend ActiveSupport::Concern
18
26
 
19
27
  CHANNEL_HEADER = 'X-Spree-Channel'.freeze
20
28
 
21
29
  included do
30
+ # Prepended: LocaleAndCurrency's set_locale/set_currency callbacks
31
+ # (registered up in V3::BaseController) consult Spree::Current's
32
+ # fallback chain, which must derive from the REQUEST's store — not
33
+ # the Store.default fallback — before they run.
34
+ prepend_before_action :set_current_store_context
22
35
  before_action :set_current_channel
23
36
  end
24
37
 
25
38
  protected
26
39
 
27
40
  def current_channel
28
- @current_channel ||= channel_from_header || Spree::Current.channel
41
+ @current_channel ||= channel_from_api_key || channel_from_header || Spree::Current.channel
29
42
  end
30
43
 
31
44
  private
32
45
 
33
- # Only write to Spree::Current when the header resolves a specific
34
- # channel. The store-default fallback is handled lazily by
46
+ # Only write to Spree::Current when the key or header resolves a
47
+ # specific channel. The store-default fallback is handled lazily by
35
48
  # +Spree::Current.channel+ itself, which avoids one query per
36
- # header-less API request.
49
+ # unbound, header-less API request.
50
+ def set_current_store_context
51
+ Spree::Current.store = current_store
52
+ end
53
+
37
54
  def set_current_channel
55
+ bound = channel_from_api_key
56
+
57
+ if bound
58
+ header = channel_from_header
59
+ if header && header.id != bound.id
60
+ render_error(
61
+ code: ErrorHandler::ERROR_CODES[:channel_mismatch],
62
+ message: Spree.t('api.errors.channel_mismatch', default: 'The requested channel does not match the channel this API key is bound to'),
63
+ status: :unprocessable_entity
64
+ )
65
+ return
66
+ end
67
+
68
+ unless bound.active?
69
+ render_error(
70
+ code: ErrorHandler::ERROR_CODES[:channel_inactive],
71
+ message: Spree.t('api.errors.channel_inactive', default: 'The channel this API key is bound to is not active'),
72
+ status: :forbidden
73
+ )
74
+ return
75
+ end
76
+ end
77
+
38
78
  # Memoize so a later +current_channel+ call (e.g. the storefront gate,
39
- # serializer params) reuses this row instead of re-querying the header.
40
- @current_channel = channel_from_header
79
+ # serializer params) reuses this row instead of re-querying.
80
+ @current_channel = bound || channel_from_header
41
81
  Spree::Current.channel = @current_channel if @current_channel
42
82
  end
43
83
 
84
+ def channel_from_api_key
85
+ return nil unless respond_to?(:current_api_key, true)
86
+
87
+ current_api_key&.channel
88
+ end
89
+
44
90
  def channel_from_header
45
91
  value = request.headers[CHANNEL_HEADER].presence
46
92
  return nil if value.blank?
@@ -21,6 +21,10 @@ module Spree
21
21
  record_not_found: 'record_not_found',
22
22
  resource_invalid: 'resource_invalid',
23
23
 
24
+ # Channel errors
25
+ channel_mismatch: 'channel_mismatch',
26
+ channel_inactive: 'channel_inactive',
27
+
24
28
  # Cart errors
25
29
  cart_not_found: 'cart_not_found',
26
30
  cart_cannot_transition: 'cart_cannot_transition',
@@ -87,14 +87,14 @@ module Spree
87
87
  end
88
88
  end
89
89
 
90
- # `key_type` and `scopes` are create-only (scope immutability lives on
91
- # Spree::ApiKey); update is limited to the human-facing `name`. Stripping
92
- # them here keeps them out of mass assignment so a rename returns 200
93
- # rather than 422.
90
+ # `key_type`, `scopes`, and `channel_id` are create-only (immutability
91
+ # lives on Spree::ApiKey); update is limited to the human-facing `name`.
92
+ # Stripping them here keeps them out of mass assignment so a rename
93
+ # returns 200 rather than 422.
94
94
  def permitted_params
95
95
  return params.permit(:name) if action_name == 'update'
96
96
 
97
- params.permit(:name, :key_type, scopes: [])
97
+ params.permit(:name, :key_type, :channel_id, scopes: [])
98
98
  end
99
99
 
100
100
  private
@@ -7,17 +7,19 @@ module Spree
7
7
 
8
8
  # POST /api/v3/admin/customers/:customer_id/addresses
9
9
  def create
10
- @resource = @parent.addresses.new(address_attrs)
11
- authorize_resource!(@resource, :create)
10
+ authorize_resource!(Spree::Address.new(user_id: @parent.id), :create)
12
11
 
13
- ApplicationRecord.transaction do
14
- if @resource.save
15
- apply_default_flags(@resource)
16
- render json: serialize_resource(@resource.reload), status: :created
17
- else
18
- render_validation_error(@resource.errors)
19
- raise ActiveRecord::Rollback
20
- end
12
+ result = Spree.address_create_service.call(
13
+ address_params: address_attrs,
14
+ user: @parent,
15
+ default_billing: default_billing_flag,
16
+ default_shipping: default_shipping_flag
17
+ )
18
+
19
+ if result.success?
20
+ render json: serialize_resource(result.value), status: :created
21
+ else
22
+ render_validation_error(result.value.errors)
21
23
  end
22
24
  end
23
25
 
@@ -25,14 +27,17 @@ module Spree
25
27
  def update
26
28
  authorize_resource!(@resource)
27
29
 
28
- ApplicationRecord.transaction do
29
- if @resource.update(address_attrs)
30
- apply_default_flags(@resource)
31
- render json: serialize_resource(@resource.reload)
32
- else
33
- render_validation_error(@resource.errors)
34
- raise ActiveRecord::Rollback
35
- end
30
+ result = Spree.address_update_service.call(
31
+ address: @resource,
32
+ address_params: address_attrs,
33
+ default_billing: default_billing_flag,
34
+ default_shipping: default_shipping_flag
35
+ )
36
+
37
+ if result.success?
38
+ render json: serialize_resource(result.value)
39
+ else
40
+ render_validation_error(result.value.errors)
36
41
  end
37
42
  end
38
43
 
@@ -69,11 +74,12 @@ module Spree
69
74
  )
70
75
  end
71
76
 
72
- def apply_default_flags(address)
73
- updates = {}
74
- updates[:bill_address_id] = address.id if ActiveModel::Type::Boolean.new.cast(params[:is_default_billing])
75
- updates[:ship_address_id] = address.id if ActiveModel::Type::Boolean.new.cast(params[:is_default_shipping])
76
- @parent.update!(updates) if updates.any?
77
+ def default_billing_flag
78
+ ActiveModel::Type::Boolean.new.cast(params[:is_default_billing])
79
+ end
80
+
81
+ def default_shipping_flag
82
+ ActiveModel::Type::Boolean.new.cast(params[:is_default_shipping])
77
83
  end
78
84
  end
79
85
  end
@@ -14,8 +14,13 @@ module Spree
14
14
  # +allow_guest_storefront_access!+.
15
15
  include Spree::Api::V3::StorefrontGating
16
16
 
17
- # Require publishable API key for all Store API requests
18
- before_action :authenticate_api_key!
17
+ # Require publishable API key for all Store API requests. Prepended so
18
+ # the key is authenticated BEFORE ChannelResolution's
19
+ # +set_current_channel+ and the StorefrontGating 401 guard run —
20
+ # a channel-bound key must resolve its channel ahead of the gate, and
21
+ # this matches Store::ResourceController, where +authenticate_request!+
22
+ # is inherited ahead of both concerns.
23
+ prepend_before_action :authenticate_api_key!
19
24
  end
20
25
  end
21
26
  end
@@ -141,8 +141,13 @@ module Spree
141
141
  Spree.api.cart_serializer
142
142
  end
143
143
 
144
+ # Channel-scoped: a storefront surface lists only its own channel's
145
+ # carts — without this, a blended DTC + wholesale storefront leaks
146
+ # the user's wholesale cart into the DTC surface (and vice versa).
147
+ # Explicit by-ID lookups (show/update) stay cross-channel so a shared
148
+ # checkout can load any cart the caller is authorized for.
144
149
  def scope
145
- current_store.carts.where(user: current_user).order(updated_at: :desc)
150
+ current_store.carts.where(user: current_user, channel: current_channel).order(updated_at: :desc)
146
151
  end
147
152
 
148
153
  private
@@ -0,0 +1,31 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Store
5
+ # Exposes the channel context the request resolved to (bound key →
6
+ # X-Spree-Channel header → store default) so a storefront can read its
7
+ # own identity and access posture. Deliberately a singular resource:
8
+ # the Store API never enumerates a store's channels — that would leak
9
+ # the existence of gated surfaces (wholesale, POS) to anyone holding
10
+ # the publishable key.
11
+ class ChannelController < Store::BaseController
12
+ # A gated storefront must be able to read "this channel requires
13
+ # login" before authentication to render a sign-in wall instead of
14
+ # an error page.
15
+ allow_guest_storefront_access!
16
+
17
+ # GET /api/v3/store/channel
18
+ def show
19
+ render json: serialize_resource(current_channel)
20
+ end
21
+
22
+ protected
23
+
24
+ def serializer_class
25
+ Spree.api.channel_serializer
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -35,6 +35,14 @@ module Spree
35
35
  end
36
36
  end
37
37
 
38
+ # DELETE /api/v3/store/customers/me/addresses/:id
39
+ def destroy
40
+ @resource.destroy!
41
+ head :no_content
42
+ rescue ActiveRecord::RecordNotDestroyed => e
43
+ render_validation_error(e.record.errors.presence || e.message)
44
+ end
45
+
38
46
  protected
39
47
 
40
48
  def set_parent
@@ -9,6 +9,18 @@ module Spree
9
9
  include Spree::Api::V3::ChannelResolution
10
10
  include Spree::Api::V3::StorefrontGating
11
11
 
12
+ # The inherited +set_parent+/+set_resource+ callbacks were registered
13
+ # before the two concerns above, so they'd run resource lookups ahead
14
+ # of channel resolution (wrong channel scoping for X-Spree-Channel /
15
+ # key-bound requests) and ahead of the login gate (guests could probe
16
+ # resource existence on a gated channel via 404s). Re-register them so
17
+ # lookups always run inside the resolved channel context, behind the
18
+ # gate.
19
+ skip_before_action :set_parent
20
+ skip_before_action :set_resource
21
+ before_action :set_parent
22
+ before_action :set_resource, only: [:show, :update, :destroy]
23
+
12
24
  protected
13
25
 
14
26
  def authenticate_request!
@@ -13,10 +13,15 @@ module Spree
13
13
 
14
14
  # Accept optional second argument for backward compatibility with jobs
15
15
  # enqueued before this change was deployed.
16
- def perform(delivery_id, _deprecated_secret_key = nil)
16
+ #
17
+ # `payload_secrets` carries credentials withheld from the persisted payload
18
+ # so the outgoing request body stays complete. See
19
+ # {Spree::WebhookPayloadRedaction}.
20
+ def perform(delivery_id, _deprecated_secret_key = nil, payload_secrets: nil)
17
21
  delivery = Spree::WebhookDelivery.find_by(id: delivery_id)
18
22
  return if delivery.nil?
19
23
 
24
+ delivery.payload_secrets = payload_secrets
20
25
  secret_key = delivery.webhook_endpoint.secret_key
21
26
  Spree::Webhooks::DeliverWebhook.call(delivery: delivery, secret_key: secret_key)
22
27
  end
@@ -18,12 +18,17 @@ module Spree
18
18
  scopes: [:string, multi: true],
19
19
  revoked_at: [:string, nullable: true],
20
20
  last_used_at: [:string, nullable: true],
21
- created_by_email: [:string, nullable: true]
21
+ created_by_email: [:string, nullable: true],
22
+ channel_id: [:string, nullable: true]
22
23
 
23
24
  attributes :name, :key_type, :token_prefix, :scopes,
24
25
  created_at: :iso8601, updated_at: :iso8601,
25
26
  revoked_at: :iso8601, last_used_at: :iso8601
26
27
 
28
+ attribute :channel_id do |key|
29
+ key.channel&.prefixed_id
30
+ end
31
+
27
32
  # Returned only on the create response — `plaintext_token` is held in
28
33
  # memory on the model after `generate_token` and is never persisted
29
34
  # for secret keys, so we serialize it whenever it's available rather
@@ -5,12 +5,11 @@ module Spree
5
5
  # Serializes Spree::CustomerGroup for the admin pickers
6
6
  # (e.g. promotion rule, customer-group filters). Surfaces only
7
7
  # what the admin UI needs to display + select rows.
8
- class CustomerGroupSerializer < V3::BaseSerializer
9
- typelize name: :string,
10
- description: 'string | null',
8
+ class CustomerGroupSerializer < V3::CustomerGroupSerializer
9
+ typelize description: 'string | null',
11
10
  customers_count: :number
12
11
 
13
- attributes :name, :description, :customers_count,
12
+ attributes :description, :customers_count,
14
13
  created_at: :iso8601, updated_at: :iso8601
15
14
 
16
15
  # Members are paginated separately via `/customers?customer_group_id_in=…`
@@ -25,10 +25,15 @@ module Spree
25
25
 
26
26
  attributes :event_name, :event_id, :response_code, :execution_time,
27
27
  :error_type, :request_errors, :response_body, :success,
28
- :payload,
29
28
  created_at: :iso8601, updated_at: :iso8601,
30
29
  delivered_at: :iso8601
31
30
 
31
+ # Redacted again on read: deliveries written before payload redaction
32
+ # shipped still hold live credentials in the column.
33
+ attribute :payload do |delivery|
34
+ Spree::WebhookPayloadRedaction.split(delivery.payload).first
35
+ end
36
+
32
37
  attribute :webhook_endpoint_id do |delivery|
33
38
  delivery.webhook_endpoint&.prefixed_id
34
39
  end
@@ -5,7 +5,7 @@ module Spree
5
5
  # Pre-purchase cart data with checkout progression info
6
6
  class CartSerializer < BaseSerializer
7
7
  typelize number: :string, current_step: :string, completed_steps: 'string[]', token: :string, email: [:string, nullable: true],
8
- customer_note: [:string, nullable: true], market_id: [:string, nullable: true],
8
+ customer_note: [:string, nullable: true], market_id: [:string, nullable: true], channel_id: [:string, nullable: true],
9
9
  currency: :string, locale: [:string, nullable: true], total_quantity: :number,
10
10
  requirements: 'Array<{step: string, field: string, message: string}>',
11
11
  item_total: [:string, nullable: true], display_item_total: [:string, nullable: true],
@@ -34,6 +34,13 @@ module Spree
34
34
  order.market&.prefixed_id
35
35
  end
36
36
 
37
+ # Which surface this cart belongs to — lets a multi-channel storefront
38
+ # verify a cookie-persisted cart against its own channel before
39
+ # adopting it (see the wholesale reference storefront's cart guard).
40
+ attribute :channel_id do |order|
41
+ order.channel&.prefixed_id
42
+ end
43
+
37
44
  attributes :number, :token, :email, :customer_note,
38
45
  :currency, :locale, :total_quantity, :warnings
39
46
 
@@ -5,9 +5,17 @@ module Spree
5
5
  typelize name: :string,
6
6
  code: :string,
7
7
  active: :boolean,
8
- default: :boolean
8
+ default: :boolean,
9
+ storefront_access: :string,
10
+ guest_checkout: :boolean
9
11
 
10
12
  attributes :name, :code, :active, :default
13
+
14
+ # Resolved (channel → store fallback) values — what a client should act
15
+ # on, as opposed to the raw nullable preferences on the admin serializer.
16
+ attribute :storefront_access, &:resolved_storefront_access
17
+
18
+ attribute :guest_checkout, &:resolved_guest_checkout
11
19
  end
12
20
  end
13
21
  end
@@ -0,0 +1,15 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ # Minimal customer-group shape for the Store API. Exposed on
5
+ # `customers/me` so a storefront can branch on membership (e.g. an
6
+ # approved wholesale buyer) — pricing itself always resolves
7
+ # server-side.
8
+ class CustomerGroupSerializer < BaseSerializer
9
+ typelize name: :string
10
+
11
+ attributes :name
12
+ end
13
+ end
14
+ end
15
+ end
@@ -37,6 +37,14 @@ module Spree
37
37
  store = params&.dig(:store) || Spree::Current.store
38
38
  user.newsletter_subscriber(store)
39
39
  end
40
+
41
+ # Membership signal for storefront branching (e.g. wholesale approval);
42
+ # scoped to the request store (params first — Spree::Current.store falls
43
+ # back to the DEFAULT store, wrong on sibling stores' domains) so other
44
+ # stores' memberships never leak.
45
+ many :customer_groups,
46
+ proc { |groups, params| groups.for_store(params&.dig(:store) || Spree::Current.store) },
47
+ resource: proc { Spree.api.customer_group_serializer }
40
48
  end
41
49
  end
42
50
  end
@@ -48,7 +48,7 @@ module Spree
48
48
  attribute :description do |product|
49
49
  next if product.description.blank?
50
50
 
51
- Nokogiri::HTML.fragment(product.description).text.squish
51
+ Spree::RichTextHelper.to_plain_text(product.description)
52
52
  end
53
53
 
54
54
  attribute :description_html do |product|
@@ -56,7 +56,7 @@ module Spree
56
56
  'X-Spree-Webhook-Timestamp' => webhook_timestamp.to_s,
57
57
  'X-Spree-Webhook-Event' => @delivery.event_name
58
58
  }
59
- body = @delivery.payload.to_json
59
+ body = payload_json
60
60
  http_options = { open_timeout: TIMEOUT, read_timeout: TIMEOUT, verify_mode: ssl_verify_mode }
61
61
 
62
62
  # SSRF protection is disabled in development so webhooks can reach
@@ -77,10 +77,14 @@ module Spree
77
77
  end
78
78
 
79
79
  def generate_signature
80
- payload_json = @delivery.payload.to_json
81
80
  OpenSSL::HMAC.hexdigest('SHA256', @secret_key, "#{webhook_timestamp}.#{payload_json}")
82
81
  end
83
82
 
83
+ # Memoized so the signature is computed over exactly the bytes sent.
84
+ def payload_json
85
+ @payload_json ||= @delivery.deliverable_payload.to_json
86
+ end
87
+
84
88
  def webhook_timestamp
85
89
  @webhook_timestamp ||= Time.current.to_i
86
90
  end
@@ -56,13 +56,16 @@ module Spree
56
56
  )
57
57
  end
58
58
 
59
+ # Live credentials go over the wire but never into the delivery log.
60
+ persisted_payload, secrets = Spree::WebhookPayloadRedaction.split(payload)
61
+
59
62
  delivery = endpoint.webhook_deliveries.create!(
60
63
  event_name: event.name,
61
64
  event_id: event.id,
62
- payload: payload
65
+ payload: persisted_payload
63
66
  )
64
67
 
65
- Spree::WebhookDeliveryJob.perform_later(delivery.id)
68
+ Spree::WebhookDeliveryJob.perform_later(delivery.id, payload_secrets: secrets)
66
69
  rescue ActiveRecord::RecordNotUnique
67
70
  # Race condition: another thread already created this delivery — safe to ignore
68
71
  rescue StandardError => e
@@ -10,6 +10,8 @@ en:
10
10
  invalid_taxonomy_id: Invalid taxonomy id.
11
11
  must_specify_api_key: You must specify an API key.
12
12
  errors:
13
+ channel_inactive: The channel this API key is bound to is not active.
14
+ channel_mismatch: The requested channel does not match the channel this API key is bound to.
13
15
  guest_checkout_not_allowed: You must be signed in to complete checkout.
14
16
  storefront_login_required: Authentication required to access this store.
15
17
  negative_quantity: quantity is negative
data/config/routes.rb CHANGED
@@ -20,6 +20,9 @@ Spree::Core::Engine.add_routes do
20
20
  resources :currencies, only: [:index]
21
21
  resources :locales, only: [:index]
22
22
 
23
+ # Current channel context (never a list — see Store::ChannelController)
24
+ resource :channel, only: [:show], controller: 'channel'
25
+
23
26
  # Catalog
24
27
  resources :products, only: [:index, :show] do
25
28
  collection do
@@ -112,6 +112,7 @@ module Spree
112
112
  fulfillment_serializer: 'Spree::Api::V3::FulfillmentSerializer',
113
113
  address_serializer: 'Spree::Api::V3::AddressSerializer',
114
114
  customer_serializer: 'Spree::Api::V3::CustomerSerializer',
115
+ customer_group_serializer: 'Spree::Api::V3::CustomerGroupSerializer',
115
116
  country_serializer: 'Spree::Api::V3::CountrySerializer',
116
117
  channel_serializer: 'Spree::Api::V3::ChannelSerializer',
117
118
  product_publication_serializer: 'Spree::Api::V3::ProductPublicationSerializer',
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spree_api
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.6.0.rc2
4
+ version: 5.6.0.rc4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vendo Connect Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-07-17 00:00:00.000000000 Z
11
+ date: 2026-07-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rswag-specs
@@ -72,14 +72,14 @@ dependencies:
72
72
  requirements:
73
73
  - - '='
74
74
  - !ruby/object:Gem::Version
75
- version: 5.6.0.rc2
75
+ version: 5.6.0.rc4
76
76
  type: :runtime
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - '='
81
81
  - !ruby/object:Gem::Version
82
- version: 5.6.0.rc2
82
+ version: 5.6.0.rc4
83
83
  description: Spree's API
84
84
  email:
85
85
  - hello@spreecommerce.org
@@ -189,6 +189,7 @@ files:
189
189
  - app/controllers/spree/api/v3/store/carts/store_credits_controller.rb
190
190
  - app/controllers/spree/api/v3/store/carts_controller.rb
191
191
  - app/controllers/spree/api/v3/store/categories_controller.rb
192
+ - app/controllers/spree/api/v3/store/channel_controller.rb
192
193
  - app/controllers/spree/api/v3/store/countries_controller.rb
193
194
  - app/controllers/spree/api/v3/store/currencies_controller.rb
194
195
  - app/controllers/spree/api/v3/store/customer/addresses_controller.rb
@@ -291,6 +292,7 @@ files:
291
292
  - app/serializers/spree/api/v3/credit_card_serializer.rb
292
293
  - app/serializers/spree/api/v3/currency_serializer.rb
293
294
  - app/serializers/spree/api/v3/custom_field_serializer.rb
295
+ - app/serializers/spree/api/v3/customer_group_serializer.rb
294
296
  - app/serializers/spree/api/v3/customer_return_serializer.rb
295
297
  - app/serializers/spree/api/v3/customer_serializer.rb
296
298
  - app/serializers/spree/api/v3/delivery_method_serializer.rb
@@ -380,9 +382,9 @@ licenses:
380
382
  - BSD-3-Clause
381
383
  metadata:
382
384
  bug_tracker_uri: https://github.com/spree/spree/issues
383
- changelog_uri: https://github.com/spree/spree/releases/tag/v5.6.0.rc2
385
+ changelog_uri: https://github.com/spree/spree/releases/tag/v5.6.0.rc4
384
386
  documentation_uri: https://docs.spreecommerce.org/
385
- source_code_uri: https://github.com/spree/spree/tree/v5.6.0.rc2
387
+ source_code_uri: https://github.com/spree/spree/tree/v5.6.0.rc4
386
388
  post_install_message:
387
389
  rdoc_options: []
388
390
  require_paths: