spree_api 6.0.0 → 6.0.2

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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -2
  3. data/app/controllers/concerns/spree/api/v3/admin/embed_permissions.rb +6 -1
  4. data/app/controllers/spree/api/v3/admin/admin_users_controller.rb +10 -0
  5. data/app/controllers/spree/api/v3/admin/invitation_acceptances_controller.rb +8 -4
  6. data/app/controllers/spree/api/v3/admin/sellers_controller.rb +1 -1
  7. data/app/controllers/spree/api/v3/admin/store_controller.rb +23 -7
  8. data/app/controllers/spree/api/v3/seller/invitation_acceptances_controller.rb +15 -5
  9. data/app/controllers/spree/api/v3/store/customers_controller.rb +1 -0
  10. data/app/controllers/spree/api/v3/store/store_controller.rb +25 -0
  11. data/app/serializers/spree/api/v3/address_serializer.rb +2 -0
  12. data/app/serializers/spree/api/v3/admin/address_serializer.rb +2 -0
  13. data/app/serializers/spree/api/v3/admin/admin_user_serializer.rb +20 -2
  14. data/app/serializers/spree/api/v3/admin/category_serializer.rb +1 -3
  15. data/app/serializers/spree/api/v3/admin/claim_serializer.rb +2 -0
  16. data/app/serializers/spree/api/v3/admin/collection_serializer.rb +1 -4
  17. data/app/serializers/spree/api/v3/admin/company_serializer.rb +2 -0
  18. data/app/serializers/spree/api/v3/admin/credit_card_serializer.rb +2 -0
  19. data/app/serializers/spree/api/v3/admin/customer_serializer.rb +2 -0
  20. data/app/serializers/spree/api/v3/admin/delivery_method_serializer.rb +2 -0
  21. data/app/serializers/spree/api/v3/admin/fulfillment_serializer.rb +1 -0
  22. data/app/serializers/spree/api/v3/admin/gift_card_serializer.rb +2 -0
  23. data/app/serializers/spree/api/v3/admin/invitation_serializer.rb +1 -1
  24. data/app/serializers/spree/api/v3/admin/line_item_serializer.rb +1 -0
  25. data/app/serializers/spree/api/v3/admin/media_serializer.rb +2 -0
  26. data/app/serializers/spree/api/v3/admin/newsletter_subscriber_serializer.rb +2 -0
  27. data/app/serializers/spree/api/v3/admin/option_type_serializer.rb +2 -0
  28. data/app/serializers/spree/api/v3/admin/option_value_serializer.rb +2 -0
  29. data/app/serializers/spree/api/v3/admin/order_serializer.rb +2 -0
  30. data/app/serializers/spree/api/v3/admin/payment_method_serializer.rb +2 -0
  31. data/app/serializers/spree/api/v3/admin/payment_serializer.rb +8 -2
  32. data/app/serializers/spree/api/v3/admin/payment_source_serializer.rb +2 -0
  33. data/app/serializers/spree/api/v3/admin/product_serializer.rb +1 -3
  34. data/app/serializers/spree/api/v3/admin/return_serializer.rb +2 -0
  35. data/app/serializers/spree/api/v3/admin/seller_payout_serializer.rb +2 -1
  36. data/app/serializers/spree/api/v3/admin/seller_serializer.rb +4 -0
  37. data/app/serializers/spree/api/v3/admin/session_serializer.rb +17 -0
  38. data/app/serializers/spree/api/v3/admin/store_credit_serializer.rb +2 -0
  39. data/app/serializers/spree/api/v3/admin/store_serializer.rb +3 -4
  40. data/app/serializers/spree/api/v3/admin/variant_serializer.rb +1 -3
  41. data/app/serializers/spree/api/v3/base_serializer.rb +28 -0
  42. data/app/serializers/spree/api/v3/category_serializer.rb +1 -4
  43. data/app/serializers/spree/api/v3/claim_serializer.rb +2 -0
  44. data/app/serializers/spree/api/v3/collection_serializer.rb +1 -4
  45. data/app/serializers/spree/api/v3/company_serializer.rb +2 -0
  46. data/app/serializers/spree/api/v3/credit_card_serializer.rb +2 -0
  47. data/app/serializers/spree/api/v3/customer_serializer.rb +2 -0
  48. data/app/serializers/spree/api/v3/delivery_method_serializer.rb +2 -0
  49. data/app/serializers/spree/api/v3/fulfillment_serializer.rb +2 -0
  50. data/app/serializers/spree/api/v3/gift_card_serializer.rb +1 -0
  51. data/app/serializers/spree/api/v3/line_item_serializer.rb +2 -0
  52. data/app/serializers/spree/api/v3/media_serializer.rb +2 -0
  53. data/app/serializers/spree/api/v3/newsletter_subscriber_serializer.rb +2 -0
  54. data/app/serializers/spree/api/v3/option_type_serializer.rb +2 -0
  55. data/app/serializers/spree/api/v3/option_value_serializer.rb +2 -0
  56. data/app/serializers/spree/api/v3/order_serializer.rb +2 -0
  57. data/app/serializers/spree/api/v3/payment_method_serializer.rb +2 -0
  58. data/app/serializers/spree/api/v3/payment_serializer.rb +3 -1
  59. data/app/serializers/spree/api/v3/payment_source_serializer.rb +2 -0
  60. data/app/serializers/spree/api/v3/product_serializer.rb +1 -4
  61. data/app/serializers/spree/api/v3/return_serializer.rb +2 -0
  62. data/app/serializers/spree/api/v3/seller/invitation_serializer.rb +22 -1
  63. data/app/serializers/spree/api/v3/seller/profile_serializer.rb +1 -1
  64. data/app/serializers/spree/api/v3/seller_serializer.rb +2 -0
  65. data/app/serializers/spree/api/v3/store_credit_serializer.rb +2 -0
  66. data/app/serializers/spree/api/v3/store_serializer.rb +16 -0
  67. data/app/serializers/spree/api/v3/variant_serializer.rb +1 -4
  68. data/app/services/spree/api/v3/request_schema.rb +15 -7
  69. data/config/routes.rb +5 -2
  70. data/lib/spree/api/agent_workflow_endpoints.rb +2 -2
  71. data/lib/spree/api/agent_write_schemas.rb +6 -6
  72. data/lib/spree/api/dependencies.rb +2 -2
  73. data/lib/spree/api/enum_types.rb +2 -1
  74. data/lib/spree/api/oauth.rb +40 -20
  75. data/lib/typelizer/open_enums.rb +22 -3
  76. data/openapi/admin.yaml +87631 -0
  77. data/openapi/store.yaml +25412 -0
  78. metadata +12 -7
@@ -152,6 +152,12 @@ module Spree
152
152
  }.freeze
153
153
  private_constant :PREFERENCE_LIST_NAMES
154
154
 
155
+ # Settings declared with a code format share the code list request
156
+ # bodies take, so a value read back is the type a write takes.
157
+ FORMAT_CODE_LISTS = { 'iso-country' => :country, 'currency' => :currency, 'locale' => :locale,
158
+ 'timezone' => :time_zone }.freeze
159
+ private_constant :FORMAT_CODE_LISTS
160
+
155
161
  def self.preference_type_hint(schema, name)
156
162
  types = Array(schema['type'])
157
163
  options = {}
@@ -165,6 +171,9 @@ module Spree
165
171
  if schema['enum']
166
172
  options[:enum] = schema['enum'].compact
167
173
  options[:enum_type_name] = PREFERENCE_LIST_NAMES[name.to_sym] if PREFERENCE_LIST_NAMES.key?(name.to_sym)
174
+ elsif (code_list = FORMAT_CODE_LISTS[(schema['items'] || schema)['format']])
175
+ options[:enum_type_name], values = Spree::Api::V3::RequestSchema::CODE_LISTS.fetch(code_list)
176
+ options[:enum] = values.call
168
177
  end
169
178
  options.empty? ? hint : [hint, options]
170
179
  end
@@ -192,6 +201,25 @@ module Spree
192
201
  end
193
202
  end
194
203
 
204
+ # Declares the opt-in `custom_fields` expand. The Store API sends only
205
+ # values whose definition is storefront visible; Admin API serializers
206
+ # pass `admin: true` to send every value.
207
+ #
208
+ # @param admin [Boolean] whether to send every value rather than only storefront visible ones
209
+ # @return [void]
210
+ def self.expandable_custom_fields(admin: false)
211
+ if admin
212
+ many :custom_fields,
213
+ resource: proc { Spree.api.admin_custom_field_serializer },
214
+ if: proc { expand?('custom_fields') }
215
+ else
216
+ many :storefront_custom_fields,
217
+ key: :custom_fields,
218
+ resource: proc { Spree.api.custom_field_serializer },
219
+ if: proc { expand?('custom_fields') }
220
+ end
221
+ end
222
+
195
223
  # Plain decimal notation. BigDecimal renders 0.06 as "0.6e-1", which
196
224
  # is what a client would otherwise print beside a unit.
197
225
  #
@@ -50,10 +50,7 @@ module Spree
50
50
  resource: proc { Spree.api.category_serializer },
51
51
  if: proc { expand?('ancestors') }
52
52
 
53
- many :storefront_custom_fields,
54
- key: :custom_fields,
55
- resource: proc { Spree.api.custom_field_serializer },
56
- if: proc { expand?('custom_fields') }
53
+ expandable_custom_fields
57
54
  end
58
55
  end
59
56
  end
@@ -42,6 +42,8 @@ module Spree
42
42
  many :claim_line_items,
43
43
  resource: proc { Spree.api.claim_line_item_serializer },
44
44
  if: proc { expand?('claim_line_items') }
45
+
46
+ expandable_custom_fields
45
47
  end
46
48
  end
47
49
  end
@@ -29,10 +29,7 @@ module Spree
29
29
  image_url_for(collection.square_image)
30
30
  end
31
31
 
32
- many :storefront_custom_fields,
33
- key: :custom_fields,
34
- resource: proc { Spree.api.custom_field_serializer },
35
- if: proc { expand?('custom_fields') }
32
+ expandable_custom_fields
36
33
  end
37
34
  end
38
35
  end
@@ -25,6 +25,8 @@ module Spree
25
25
  { 'id' => node.prefixed_id, 'name' => node.name, 'kind' => node.kind }
26
26
  end
27
27
  end
28
+
29
+ expandable_custom_fields
28
30
  end
29
31
  end
30
32
  end
@@ -7,6 +7,8 @@ module Spree
7
7
  gateway_payment_profile_id: [:string, nullable: true]
8
8
 
9
9
  attributes :brand, :last4, :month, :year, :name, :default, :gateway_payment_profile_id
10
+
11
+ expandable_custom_fields
10
12
  end
11
13
  end
12
14
  end
@@ -54,6 +54,8 @@ module Spree
54
54
  many :customer_groups,
55
55
  proc { |groups, params| groups.for_store(params&.dig(:store) || Spree::Current.store) },
56
56
  resource: proc { Spree.api.customer_group_serializer }
57
+
58
+ expandable_custom_fields
57
59
  end
58
60
  end
59
61
  end
@@ -15,6 +15,8 @@ module Spree
15
15
  attribute :digital, &:digital?
16
16
  attribute :pickup, &:pickup?
17
17
  attribute :pickup_point, &:pickup_point?
18
+
19
+ expandable_custom_fields
18
20
  end
19
21
  end
20
22
  end
@@ -59,6 +59,8 @@ module Spree
59
59
  one :stock_location, resource: proc { Spree.api.stock_location_serializer }
60
60
  many :delivery_rates, resource: proc { Spree.api.delivery_rate_serializer }
61
61
  many :tax_lines, resource: proc { Spree.api.tax_line_serializer }, if: proc { expand?('tax_lines') }
62
+
63
+ expandable_custom_fields
62
64
  end
63
65
  end
64
66
  end
@@ -51,6 +51,7 @@ module Spree
51
51
 
52
52
  attribute :active, &:active?
53
53
 
54
+ expandable_custom_fields
54
55
  end
55
56
  end
56
57
  end
@@ -73,6 +73,8 @@ module Spree
73
73
  # exactly-one-adjustable rule makes the nested view complete, and the
74
74
  # owner already carries the tax totals.
75
75
  many :tax_lines, resource: proc { Spree.api.tax_line_serializer }, if: proc { expand?('tax_lines') }
76
+
77
+ expandable_custom_fields
76
78
  end
77
79
  end
78
80
  end
@@ -99,6 +99,8 @@ module Spree
99
99
  def url_helpers
100
100
  Rails.application.routes.url_helpers
101
101
  end
102
+
103
+ expandable_custom_fields
102
104
  end
103
105
  end
104
106
  end
@@ -21,6 +21,8 @@ module Spree
21
21
  attribute :customer_id do |subscriber|
22
22
  subscriber.customer&.prefixed_id if current_user && subscriber.customer_id == current_user.id
23
23
  end
24
+
25
+ expandable_custom_fields
24
26
  end
25
27
  end
26
28
  end
@@ -5,6 +5,8 @@ module Spree
5
5
  typelize name: :string, label: :string, position: :number, kind: [:string, enum: Spree::OptionType::KINDS, enum_type_name: 'OptionTypeKind']
6
6
 
7
7
  attributes :name, :label, :position, :kind
8
+
9
+ expandable_custom_fields
8
10
  end
9
11
  end
10
12
  end
@@ -21,6 +21,8 @@ module Spree
21
21
  attribute :image_url do |option_value|
22
22
  image_url_for(option_value.image)
23
23
  end
24
+
25
+ expandable_custom_fields
24
26
  end
25
27
  end
26
28
  end
@@ -79,6 +79,8 @@ module Spree
79
79
  one :shipping_address, resource: proc { Spree.api.address_serializer }
80
80
  one :gift_card, resource: proc { Spree.api.gift_card_serializer }
81
81
  one :market, resource: proc { Spree.api.market_serializer }
82
+
83
+ expandable_custom_fields
82
84
  end
83
85
  end
84
86
  end
@@ -14,6 +14,8 @@ module Spree
14
14
  attribute :session_required, &:session_required?
15
15
 
16
16
  attribute :source_required, &:source_required?
17
+
18
+ expandable_custom_fields
17
19
  end
18
20
  end
19
21
  end
@@ -42,10 +42,12 @@ module Spree
42
42
  Spree.api.payment_source_serializer
43
43
  end
44
44
 
45
- serializer.new(payment.source).to_h
45
+ serializer.new(payment.source, params: nested_params('source')).to_h
46
46
  end
47
47
 
48
48
  one :payment_method, resource: proc { Spree.api.payment_method_serializer }
49
+
50
+ expandable_custom_fields
49
51
  end
50
52
  end
51
53
  end
@@ -5,6 +5,8 @@ module Spree
5
5
  typelize gateway_payment_profile_id: [:string, nullable: true]
6
6
 
7
7
  attributes :gateway_payment_profile_id
8
+
9
+ expandable_custom_fields
8
10
  end
9
11
  end
10
12
  end
@@ -137,10 +137,7 @@ module Spree
137
137
  resource: proc { Spree.api.category_serializer },
138
138
  if: proc { expand?('categories') }
139
139
 
140
- many :storefront_custom_fields,
141
- key: :custom_fields,
142
- resource: proc { Spree.api.custom_field_serializer },
143
- if: proc { expand?('custom_fields') }
140
+ expandable_custom_fields
144
141
 
145
142
  typelize prior_price: ['PriceHistory', nullable: true],
146
143
  seller_id: [:string, nullable: true]
@@ -44,6 +44,8 @@ module Spree
44
44
  many :return_line_items,
45
45
  resource: proc { Spree.api.return_line_item_serializer },
46
46
  if: proc { expand?('return_line_items') }
47
+
48
+ expandable_custom_fields
47
49
  end
48
50
  end
49
51
  end
@@ -5,12 +5,33 @@ module Spree
5
5
  class InvitationSerializer < V3::BaseSerializer
6
6
  typelize email: :string, status: [:string, enum: Spree::Invitation.statuses, enum_type_name: 'InvitationStatus'],
7
7
  expires_at: [:string, nullable: true],
8
- accepted_at: [:string, nullable: true]
8
+ accepted_at: [:string, nullable: true],
9
+ inviter_email: :string,
10
+ invitee_exists: [:boolean, optional: true],
11
+ seller: '{ id: string; name: string }'
9
12
 
10
13
  attributes :email,
11
14
  created_at: :iso8601, expires_at: :iso8601, accepted_at: :iso8601
12
15
 
13
16
  attributes status: :string
17
+
18
+ attribute :inviter_email do |invitation|
19
+ invitation.inviter.email
20
+ end
21
+
22
+ # Drives the acceptance page's sign-in vs sign-up branch, so only the
23
+ # public lookup carries it: whoever holds the emailed link already
24
+ # knows the address. A seller's team screen does not get to learn
25
+ # which addresses hold accounts elsewhere on the marketplace.
26
+ attribute :invitee_exists, if: proc { params[:acceptance] } do |invitation|
27
+ invitation.invitee_account.present?
28
+ end
29
+
30
+ # The smallest shape the acceptance page's title ("Join <seller>")
31
+ # needs; the page is public, so nothing more of the seller goes out.
32
+ attribute :seller do |invitation|
33
+ { id: invitation.resource.prefixed_id, name: invitation.resource.name }
34
+ end
14
35
  end
15
36
  end
16
37
  end
@@ -17,7 +17,7 @@ module Spree
17
17
  products_count: :number,
18
18
  default_currency: [:string, enum: Spree::Money::Rounding::CURRENCY_CODES, enum_type_name: 'CurrencyCode'],
19
19
  supported_currencies: [:string, multi: true, enum: Spree::Money::Rounding::CURRENCY_CODES, enum_type_name: 'CurrencyCode'],
20
- timezone: [:string, nullable: true]
20
+ timezone: [:string, nullable: true, enum: Spree::TimeZones::SUGGESTED, enum_type_name: 'TimeZone']
21
21
 
22
22
  attributes :timezone,
23
23
  :status, :legal_name, :registration_number,
@@ -42,6 +42,8 @@ module Spree
42
42
  many :policies,
43
43
  resource: proc { Spree.api.policy_serializer },
44
44
  if: proc { expand?('policies') }
45
+
46
+ expandable_custom_fields
45
47
  end
46
48
  end
47
49
  end
@@ -7,6 +7,8 @@ module Spree
7
7
  money_attributes :amount, :amount_used, :amount_remaining, :display_amount,
8
8
  :display_amount_used, :display_amount_remaining
9
9
  attributes :currency
10
+
11
+ expandable_custom_fields
10
12
  end
11
13
  end
12
14
  end
@@ -0,0 +1,16 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ # The current store's public identity and default SEO metadata, in the
5
+ # request's locale. Missing translations fall back to the store's default
6
+ # locale.
7
+ class StoreSerializer < BaseSerializer
8
+ typelize name: :string,
9
+ seo_title: [:string, nullable: true],
10
+ meta_description: [:string, nullable: true]
11
+
12
+ attributes :name, :seo_title, :meta_description
13
+ end
14
+ end
15
+ end
16
+ end
@@ -127,10 +127,7 @@ module Spree
127
127
 
128
128
  many :option_values, resource: proc { Spree.api.option_value_serializer }
129
129
 
130
- many :storefront_custom_fields,
131
- key: :custom_fields,
132
- resource: proc { Spree.api.custom_field_serializer },
133
- if: proc { expand?('custom_fields') }
130
+ expandable_custom_fields
134
131
 
135
132
  typelize prior_price: ['PriceHistory', nullable: true]
136
133
 
@@ -40,22 +40,25 @@ module Spree
40
40
  datetime: { type: :string, format: :'date-time' },
41
41
  id: { type: :string },
42
42
  any: {},
43
- # Codes from the closed lists below.
43
+ # Codes from the lists below.
44
44
  country: { type: :string },
45
45
  currency: { type: :string },
46
46
  locale: { type: :string },
47
+ time_zone: { type: :string },
47
48
  weight_unit: { type: :string },
48
49
  dimension_unit: { type: :string }
49
50
  }.freeze
50
51
 
51
- # The closed lists a code field takes, published once per API as a
52
- # named component — the same `CountryCode`, `CurrencyCode` and
53
- # `WeightUnit` types the serializers declare, so a value read from a
54
- # response is the type a request takes.
52
+ # The lists a code field takes, published once per API as a named
53
+ # component — the same `CountryCode`, `CurrencyCode` and `WeightUnit`
54
+ # types the serializers declare, so a value read from a response is the
55
+ # type a request takes. A list `Typelizer::OpenEnums` does not close
56
+ # only suggests its values.
55
57
  CODE_LISTS = {
56
58
  country: ['CountryCode', -> { Spree::IsoData.country_codes }],
57
59
  currency: ['CurrencyCode', -> { Spree::Money::Rounding::CURRENCY_CODES }],
58
60
  locale: ['LocaleCode', -> { Spree::Locales::ALL }],
61
+ time_zone: ['TimeZone', -> { Spree::TimeZones::SUGGESTED }],
59
62
  weight_unit: ['WeightUnit', -> { Spree::Variant::WEIGHT_UNITS }],
60
63
  dimension_unit: ['DimensionUnit', -> { Spree::Variant::DIMENSION_UNITS }]
61
64
  }.freeze
@@ -348,7 +351,12 @@ module Spree
348
351
  next unless CODE_LISTS.key?(field.kind)
349
352
 
350
353
  name, values = CODE_LISTS[field.kind]
351
- lists[name] = { type: :string, enum: values.call }
354
+ lists[name] = if Typelizer::OpenEnums.closed?(name)
355
+ { type: :string, enum: values.call }
356
+ else
357
+ Typelizer::OpenEnums.open_schema({ type: :string }, values.call)
358
+ .merge(description: Typelizer::OpenEnums.description(name)).compact
359
+ end
352
360
  end
353
361
  end
354
362
 
@@ -469,7 +477,7 @@ module Spree
469
477
 
470
478
  # The same open list the serializers publish (`enum_type_name`):
471
479
  # clients autocomplete the built-in values and still accept any other.
472
- { anyOf: [schema.merge(enum: field.enum), schema] }
480
+ Typelizer::OpenEnums.open_schema(schema, field.enum)
473
481
  end
474
482
  end
475
483
  end
data/config/routes.rb CHANGED
@@ -22,6 +22,9 @@ Spree::Core::Engine.add_routes do
22
22
  # Current channel context (never a list — see Store::ChannelController)
23
23
  resource :channel, only: [:show], controller: 'channel'
24
24
 
25
+ # Current store context (name and default SEO metadata, localized)
26
+ resource :store, only: [:show], controller: 'store'
27
+
25
28
  # Catalog
26
29
  resources :products, only: [:index, :show] do
27
30
  collection do
@@ -287,9 +290,9 @@ Spree::Core::Engine.add_routes do
287
290
  resource :store, only: [:show, :update], controller: 'store' do
288
291
  get :data_sources
289
292
  end
290
- # The store's own custom fields, addressed by its id like every other
293
+ # The store's own custom fields and translations, addressed by its id like every other
291
294
  # parent. The controller only ever resolves the current store.
292
- resources :stores, only: [], concerns: :custom_fieldable
295
+ resources :stores, only: [], concerns: [:custom_fieldable, :translatable]
293
296
 
294
297
  # Staff & access (invitations, admin users, roles, API keys)
295
298
  resources :admin_users, only: [:index, :show, :update, :destroy]
@@ -95,8 +95,8 @@ module Spree
95
95
  path.gsub(/:([a-z_]+)/) { "{#{Regexp.last_match(1)}}" }
96
96
  end
97
97
 
98
- # A packaged installation has no docs/ directory, and a body the
99
- # document does not describe is simply passed through unchecked.
98
+ # A body the document does not describe is simply passed through
99
+ # unchecked.
100
100
  #
101
101
  # `AgentWriteSchemas` already holds this file parsed, so it is read
102
102
  # through there rather than a second time: the document is nearly a
@@ -33,12 +33,14 @@ module Spree
33
33
  self.for(key).keys
34
34
  end
35
35
 
36
- # Where the generated document lives, relative to this engine. Resolved
37
- # lazily: the engine class does not exist yet while this file loads.
36
+ # Where the generated document lives: `docs/` in a source checkout,
37
+ # or the copy the release packs into the gem. Resolved lazily: the
38
+ # engine class does not exist yet while this file loads.
38
39
  #
39
40
  # @return [Pathname]
40
41
  def spec_path
41
- Spree::Api::Engine.root.join('..', '..', 'docs', 'api-reference', 'admin.yaml')
42
+ source = Spree::Api::Engine.root.join('..', '..', 'docs', 'api-reference', 'admin.yaml')
43
+ source.exist? ? source : Spree::Api::Engine.root.join('openapi', 'admin.yaml')
42
44
  end
43
45
 
44
46
  # The parsed document, for a reader that needs more of it than the
@@ -63,9 +65,7 @@ module Spree
63
65
  end
64
66
 
65
67
  # A missing or unparseable document is not a boot failure: the generic
66
- # write tools simply offer nothing, which is the safe direction. An
67
- # installation running from the packaged gems has no docs/ directory at
68
- # all.
68
+ # write tools simply offer nothing, which is the safe direction.
69
69
  def load_schemas
70
70
  path = spec_path
71
71
  return {} unless File.exist?(path)
@@ -18,7 +18,7 @@ module Spree
18
18
  product_filter_sort_option media_event return return_line_item exchange exchange_line_item claim
19
19
  claim_line_item return_reason claim_reason digital_asset export gift_card_batch import import_row
20
20
  invitation newsletter_subscriber promotion refund stock_level stock_movement stock_reservation
21
- stock_transfer company company_membership company_invitation seller file_upload file_upload_target].freeze
21
+ stock_transfer company company_membership company_invitation seller file_upload file_upload_target store].freeze
22
22
 
23
23
  ADMIN_SERIALIZERS =
24
24
  %i[country state locale applied_promotion discount tax_line cart delivery_zone delivery_profile
@@ -32,7 +32,7 @@ module Spree
32
32
  exchange exchange_line_item claim claim_line_item package_type policy tax_category tax_rate
33
33
  tax_identifier company company_membership company_invitation catalog catalog_price catalog_price_tier
34
34
  catalog_product price_list_product catalog_assignment catalog_quantity_rule catalog_product_term
35
- catalog_order_minimum tax_exemption_certificate tax_exemption_reason admin_user actor seller_team_member product_submission
35
+ catalog_order_minimum tax_exemption_certificate tax_exemption_reason admin_user actor session seller_team_member product_submission
36
36
  seller seller_requirement seller_requirement_submission seller_requirement_status commission_rate
37
37
  commission_rule commission_line seller_transfer seller_payout seller_balance payment_split payment_capture_event address address_owner
38
38
  channel order_routing_rule product_publication market delivery_method delivery_method_service
@@ -110,7 +110,8 @@ module Spree
110
110
  type = "export type #{name} = (typeof #{constant})[number] | (string & {})"
111
111
  type = "export type #{name} =\n | (typeof #{constant})[number]\n | (string & {})" if type.length > LINE_WIDTH
112
112
 
113
- "\n/** The built-in values of `#{name}`. Extensions may add more. */\n#{line}\n#{type}\n"
113
+ summary = Typelizer::OpenEnums.description(name) || "The built-in values of `#{name}`. Extensions may add more."
114
+ "\n/** #{summary} */\n#{line}\n#{type}\n"
114
115
  end
115
116
  end
116
117
  end
@@ -15,23 +15,11 @@ module Spree
15
15
  # which is why this lives in `spree_api` rather than in any one gem that
16
16
  # happens to need it.
17
17
  module Oauth
18
- # Protected resources, each registered by the surface that owns it as a
19
- # path relative to this application's own origin.
20
- #
21
- # Spree::Api::Oauth.register_resource(:admin, '/api/v3/admin')
22
- #
23
- # A path rather than a full URL because the identifier must equal the
24
- # URL the client was actually pointed at: the origin comes from the
25
- # request where there is one, and from the application's configured
26
- # URL options otherwise. A store's own `url` column is deliberately not
27
- # consulted — it describes the storefront, which is frequently neither
28
- # the host nor the scheme the Admin API answers on.
29
- # Whether a redirect URI points at the machine the client runs on.
18
+ # Whether a redirect URI points at the machine the client runs on, by IP
19
+ # literal (`127.0.0.1`, `[::1]`).
30
20
  #
31
21
  # Decided by parsing the host as an address, the same way Doorkeeper
32
- # does when it ignores the port of a loopback redirect — so `localhost`
33
- # is deliberately not loopback here either, and a registration must use
34
- # the IP literal for an ephemeral port to match.
22
+ # does when it ignores the port of a loopback redirect.
35
23
  #
36
24
  # @param uri [URI, String]
37
25
  # @return [Boolean]
@@ -44,6 +32,37 @@ module Spree
44
32
  false
45
33
  end
46
34
 
35
+ # Whether a redirect URI may use plain http: an IP-literal loopback, or
36
+ # `localhost` with an explicit port.
37
+ #
38
+ # Claude Code only ever calls back on `http://localhost:<port>`. Doorkeeper
39
+ # compares a `localhost` callback exactly, so the host must be lowercase
40
+ # and the port explicit: anything else could never match an
41
+ # authorization.
42
+ #
43
+ # @param uri [URI, String]
44
+ # @return [Boolean]
45
+ def self.local_redirect_uri?(uri)
46
+ uri = URI.parse(uri.to_s) unless uri.respond_to?(:host)
47
+ return true if loopback_uri?(uri)
48
+
49
+ uri.host == 'localhost' &&
50
+ (1..65_535).cover?(uri.port) && uri.port != uri.default_port
51
+ rescue StandardError
52
+ false
53
+ end
54
+
55
+ # Protected resources, each registered by the surface that owns it as a
56
+ # path relative to this application's own origin.
57
+ #
58
+ # Spree::Api::Oauth.register_resource(:admin, '/api/v3/admin')
59
+ #
60
+ # A path rather than a full URL because the identifier must equal the
61
+ # URL the client was actually pointed at: the origin comes from the
62
+ # request where there is one, and from the application's configured
63
+ # URL options otherwise. A store's own `url` column is deliberately not
64
+ # consulted — it describes the storefront, which is frequently neither
65
+ # the host nor the scheme the Admin API answers on.
47
66
  mattr_accessor :resources, default: {}
48
67
 
49
68
  class << self
@@ -191,17 +210,18 @@ module Spree
191
210
 
192
211
  # A redirect URI is where authorization codes are delivered, so a
193
212
  # plain-http one hands them to anyone on the path — except a
194
- # loopback one, which never leaves the machine. A terminal client
195
- # listens on an ephemeral local port and has no certificate for
196
- # it, so RFC 8252 §7.3 makes that the recommended shape, and
197
- # refusing it would lock out Claude Code and Codex entirely.
213
+ # loopback or `localhost` one, which never leaves the machine. A
214
+ # terminal client listens on a local port and has no certificate
215
+ # for it. RFC 8252 §7.3 recommends the IP literal, but Claude Code
216
+ # only sends `localhost`, so refusing it would lock Claude Code
217
+ # out entirely.
198
218
  #
199
219
  # Passed as a callable so the exemption is per URI rather than
200
220
  # per environment: everything else still has to be https.
201
221
  force_ssl_in_redirect_uri do |uri|
202
222
  next false if Rails.env.development?
203
223
 
204
- !Spree::Api::Oauth.loopback_uri?(uri)
224
+ !Spree::Api::Oauth.local_redirect_uri?(uri)
205
225
  end
206
226
 
207
227
  # RFC 8707. Registering the validator is what binds an audience