facera 0.1.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.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/.DS_Store +0 -0
  3. data/.github/workflows/gem-push.yml +42 -0
  4. data/.github/workflows/ruby.yml +35 -0
  5. data/.gitignore +39 -0
  6. data/.rspec +3 -0
  7. data/CHANGELOG.md +137 -0
  8. data/Gemfile +9 -0
  9. data/LICENSE +21 -0
  10. data/README.md +309 -0
  11. data/Rakefile +6 -0
  12. data/examples/01_core_dsl.rb +132 -0
  13. data/examples/02_facet_system.rb +216 -0
  14. data/examples/03_api_generation.rb +117 -0
  15. data/examples/04_auto_mounting.rb +182 -0
  16. data/examples/05_adapters.rb +196 -0
  17. data/examples/README.md +184 -0
  18. data/examples/server/README.md +376 -0
  19. data/examples/server/adapters/payment_adapter.rb +139 -0
  20. data/examples/server/application.rb +17 -0
  21. data/examples/server/config/facera.rb +33 -0
  22. data/examples/server/config.ru +10 -0
  23. data/examples/server/cores/payment_core.rb +82 -0
  24. data/examples/server/facets/external_facet.rb +38 -0
  25. data/examples/server/facets/internal_facet.rb +33 -0
  26. data/examples/server/facets/operator_facet.rb +48 -0
  27. data/facera.gemspec +30 -0
  28. data/img/facera.png +0 -0
  29. data/lib/facera/adapter.rb +83 -0
  30. data/lib/facera/attribute.rb +95 -0
  31. data/lib/facera/auto_mount.rb +124 -0
  32. data/lib/facera/capability.rb +117 -0
  33. data/lib/facera/capability_access.rb +59 -0
  34. data/lib/facera/configuration.rb +83 -0
  35. data/lib/facera/context.rb +29 -0
  36. data/lib/facera/core.rb +65 -0
  37. data/lib/facera/dsl.rb +41 -0
  38. data/lib/facera/entity.rb +50 -0
  39. data/lib/facera/error_formatter.rb +100 -0
  40. data/lib/facera/errors.rb +40 -0
  41. data/lib/facera/executor.rb +265 -0
  42. data/lib/facera/facet.rb +103 -0
  43. data/lib/facera/field_visibility.rb +69 -0
  44. data/lib/facera/generators/core_generator.rb +23 -0
  45. data/lib/facera/generators/facet_generator.rb +25 -0
  46. data/lib/facera/generators/install_generator.rb +64 -0
  47. data/lib/facera/generators/templates/core.rb.tt +49 -0
  48. data/lib/facera/generators/templates/facet.rb.tt +23 -0
  49. data/lib/facera/grape/api_generator.rb +59 -0
  50. data/lib/facera/grape/endpoint_generator.rb +316 -0
  51. data/lib/facera/grape/entity_generator.rb +89 -0
  52. data/lib/facera/grape.rb +14 -0
  53. data/lib/facera/introspection.rb +111 -0
  54. data/lib/facera/introspection_api.rb +66 -0
  55. data/lib/facera/invariant.rb +26 -0
  56. data/lib/facera/loader.rb +153 -0
  57. data/lib/facera/openapi_generator.rb +338 -0
  58. data/lib/facera/railtie.rb +51 -0
  59. data/lib/facera/registry.rb +34 -0
  60. data/lib/facera/tasks/routes.rake +66 -0
  61. data/lib/facera/version.rb +3 -0
  62. data/lib/facera.rb +35 -0
  63. metadata +137 -0
@@ -0,0 +1,376 @@
1
+ # Facera Payment API Server
2
+
3
+ A real-world example of a multi-facet API using Facera's auto-mounting feature.
4
+
5
+ ## Structure
6
+
7
+ ```
8
+ server/
9
+ ├── config.ru # Rack server (just 15 lines!)
10
+ ├── application.rb # Application builder (34 lines)
11
+ ├── config/
12
+ │ └── facera.rb # Facera configuration (33 lines)
13
+ ├── cores/
14
+ │ └── payment_core.rb # Payment domain model
15
+ └── facets/
16
+ ├── external_facet.rb # Public API
17
+ ├── internal_facet.rb # Service-to-service API
18
+ └── operator_facet.rb # Admin/support API
19
+ ```
20
+
21
+ **Clean separation:**
22
+ - `config.ru` - Just loads and runs (15 lines)
23
+ - `application.rb` - Builds the Rack app
24
+ - `config/facera.rb` - Facera configuration
25
+ - `cores/` and `facets/` - Auto-discovered!
26
+ - No manual requires needed!
27
+
28
+ ## Quick Start
29
+
30
+ ```bash
31
+ # Start the server
32
+ rackup -p 9292
33
+
34
+ # Or with specific environment
35
+ RACK_ENV=development rackup -p 9292
36
+ ```
37
+
38
+ The server automatically:
39
+ 1. Loads configuration from `config/facera.rb`
40
+ 2. Discovers all cores in `cores/`
41
+ 3. Discovers all facets in `facets/`
42
+ 4. Mounts APIs at configured paths
43
+
44
+ ## Configuration
45
+
46
+ All Facera configuration is in `config/facera.rb`:
47
+
48
+ ```ruby
49
+ Facera.configure do |config|
50
+ config.base_path = '/api'
51
+ config.version = 'v1'
52
+
53
+ # Custom paths
54
+ config.facet_path :external, '/v1'
55
+ config.facet_path :internal, '/internal/v1'
56
+
57
+ # Conditional features
58
+ config.disable_facet :operator unless ENV['ENABLE_OPERATOR_API']
59
+ end
60
+ ```
61
+
62
+ ### Environment-Based Configuration
63
+
64
+ ```bash
65
+ # Disable operator API in production
66
+ ENABLE_OPERATOR_API=false rackup -p 9292
67
+
68
+ # Custom port
69
+ rackup -p 8080
70
+ ```
71
+
72
+ ## API Facets
73
+
74
+ ### External API (Public)
75
+
76
+ **Path:** `/api/v1`
77
+ **Audience:** External clients, mobile apps, web browsers
78
+
79
+ ```bash
80
+ # Health check
81
+ curl http://localhost:9292/api/v1/health
82
+
83
+ # Create payment
84
+ curl -X POST http://localhost:9292/api/v1/payments \
85
+ -H 'Content-Type: application/json' \
86
+ -d '{
87
+ "amount": 100.0,
88
+ "currency": "USD",
89
+ "merchant_id": "550e8400-e29b-41d4-a716-446655440000",
90
+ "customer_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
91
+ }'
92
+
93
+ # Get payment (limited fields)
94
+ curl http://localhost:9292/api/v1/payments/{id}
95
+
96
+ # List payments
97
+ curl http://localhost:9292/api/v1/payments?limit=10
98
+ ```
99
+
100
+ **Field Visibility:** 6/11 fields (secure)
101
+ **Capabilities:** Create, Read, List only
102
+ **Error Detail:** Minimal
103
+
104
+ ### Internal API (Service-to-Service)
105
+
106
+ **Path:** `/api/internal/v1`
107
+ **Audience:** Internal microservices
108
+
109
+ ```bash
110
+ # Health check
111
+ curl http://localhost:9292/api/internal/v1/health
112
+
113
+ # Get payment (all fields + computed)
114
+ curl http://localhost:9292/api/internal/v1/payments/{id}
115
+
116
+ # Confirm payment (internal only)
117
+ curl -X POST http://localhost:9292/api/internal/v1/payments/{id}/confirm
118
+
119
+ # Cancel payment (internal only)
120
+ curl -X POST http://localhost:9292/api/internal/v1/payments/{id}/cancel
121
+ ```
122
+
123
+ **Field Visibility:** All fields + computed
124
+ **Capabilities:** Full access
125
+ **Error Detail:** Detailed
126
+
127
+ ### Operator API (Admin/Support)
128
+
129
+ **Path:** `/api/operator/v1`
130
+ **Audience:** Support staff, admin tools
131
+
132
+ ```bash
133
+ # Health check
134
+ curl http://localhost:9292/api/operator/v1/health
135
+
136
+ # Get payment with operator fields
137
+ curl http://localhost:9292/api/operator/v1/payments/{id}
138
+ # Includes: customer_name, merchant_name, time_in_current_state
139
+
140
+ # Full operation access
141
+ curl -X POST http://localhost:9292/api/operator/v1/payments/{id}/confirm
142
+ ```
143
+
144
+ **Field Visibility:** All fields + admin computed
145
+ **Capabilities:** Full access
146
+ **Error Detail:** Detailed + structured
147
+
148
+ ## Adding a New Facet
149
+
150
+ 1. **Create the facet file** in `facets/`:
151
+
152
+ ```ruby
153
+ # facets/partner_facet.rb
154
+ Facera.define_facet(:partner, core: :payment) do
155
+ description "Partner integration API"
156
+
157
+ expose :payment do
158
+ fields :id, :amount, :status
159
+ end
160
+
161
+ allow_capabilities :get_payment, :list_payments
162
+ error_verbosity :minimal
163
+ end
164
+ ```
165
+
166
+ 2. **Restart server** - that's it!
167
+
168
+ The new facet is **automatically**:
169
+ - ✓ Discovered from `facets/` directory
170
+ - ✓ Loaded into the registry
171
+ - ✓ Mounted at `/api/partner/v1`
172
+ - ✓ Documented in startup logs
173
+
174
+ No manual loading, no config changes needed!
175
+
176
+ (Optional) Configure custom path in `config/facera.rb`:
177
+ ```ruby
178
+ config.facet_path :partner, '/partners/v1'
179
+ ```
180
+
181
+ ## Key Files Explained
182
+
183
+ ### `config.ru` (15 lines)
184
+
185
+ Super simple - just loads the app and runs it:
186
+
187
+ ```ruby
188
+ require_relative '../../lib/facera'
189
+ require_relative 'config/facera'
190
+ require_relative 'application'
191
+
192
+ run PaymentAPI::Application.build
193
+ ```
194
+
195
+ ### `application.rb` (34 lines)
196
+
197
+ Builds the Rack application with middleware and auto-mounted facets:
198
+ 1. Middleware (Reloader, Logger)
199
+ 2. Auto-mount all facets
200
+ 3. Root endpoint with API info
201
+
202
+ ### `config/facera.rb`
203
+
204
+ Single source of truth for Facera configuration.
205
+
206
+ ## Deployment
207
+
208
+ This is a standard Rack application. Works with:
209
+
210
+ ### Puma (recommended)
211
+
212
+ ```ruby
213
+ # config/puma.rb
214
+ workers ENV.fetch('WEB_CONCURRENCY', 2)
215
+ threads_count = ENV.fetch('RAILS_MAX_THREADS', 5)
216
+ threads threads_count, threads_count
217
+
218
+ port ENV.fetch('PORT', 9292)
219
+ environment ENV.fetch('RACK_ENV', 'development')
220
+ ```
221
+
222
+ ```bash
223
+ puma -C config/puma.rb
224
+ ```
225
+
226
+ ### Unicorn
227
+
228
+ ```ruby
229
+ # config/unicorn.rb
230
+ worker_processes 4
231
+ listen 9292
232
+ ```
233
+
234
+ ```bash
235
+ unicorn -c config/unicorn.rb
236
+ ```
237
+
238
+ ### Docker
239
+
240
+ ```dockerfile
241
+ FROM ruby:3.2
242
+
243
+ WORKDIR /app
244
+ COPY Gemfile* ./
245
+ RUN bundle install
246
+
247
+ COPY . .
248
+
249
+ EXPOSE 9292
250
+ CMD ["rackup", "-o", "0.0.0.0", "-p", "9292"]
251
+ ```
252
+
253
+ ## Production Considerations
254
+
255
+ ### 1. Authentication
256
+
257
+ Add authentication handlers in `config/facera.rb`:
258
+
259
+ ```ruby
260
+ Facera.configure do |config|
261
+ config.authenticate :external do |request|
262
+ token = request.headers['Authorization']&.sub(/^Bearer /, '')
263
+ User.find_by_token(token) or raise Facera::UnauthorizedError
264
+ end
265
+
266
+ config.authenticate :internal do |request|
267
+ service_token = request.headers['X-Service-Token']
268
+ Service.verify_token(service_token) or raise Facera::UnauthorizedError
269
+ end
270
+ end
271
+ ```
272
+
273
+ ### 2. Rate Limiting
274
+
275
+ Already configured in `facets/external_facet.rb`:
276
+
277
+ ```ruby
278
+ rate_limit requests: 1000, per: :hour
279
+ ```
280
+
281
+ ### 3. Monitoring
282
+
283
+ Enable audit logging (already on for internal/operator):
284
+
285
+ ```ruby
286
+ audit_all_operations user: :current_user
287
+ ```
288
+
289
+ ### 4. Environment Variables
290
+
291
+ ```bash
292
+ # Required
293
+ DATABASE_URL=postgresql://...
294
+ REDIS_URL=redis://...
295
+
296
+ # Optional
297
+ ENABLE_OPERATOR_API=true
298
+ RACK_ENV=production
299
+ PORT=9292
300
+ ```
301
+
302
+ ## Architecture Benefits
303
+
304
+ ### Before Facera
305
+
306
+ ```
307
+ 3 separate APIs × 5 endpoints = 15 implementations
308
+ + 3 serializers
309
+ + 3 authentication systems
310
+ + 3 sets of tests
311
+ = ~2000 lines of code
312
+ ```
313
+
314
+ ### With Facera
315
+
316
+ ```
317
+ 1 core definition
318
+ + 3 facet files
319
+ + 1 config file
320
+ = ~300 lines of code
321
+ ```
322
+
323
+ **85% less code, 100% consistency guaranteed**
324
+
325
+ ## Development Workflow
326
+
327
+ 1. **Define domain model** in `cores/payment_core.rb`
328
+ 2. **Create facet projections** in `facets/`
329
+ 3. **Configure paths** in `config/facera.rb`
330
+ 4. **Run** - APIs are auto-generated!
331
+
332
+ No route definitions, no serializer boilerplate, no duplication.
333
+
334
+ ## Testing
335
+
336
+ ```bash
337
+ # All facets respond
338
+ curl http://localhost:9292/api/v1/health
339
+ curl http://localhost:9292/api/internal/v1/health
340
+ curl http://localhost:9292/api/operator/v1/health
341
+
342
+ # Field visibility working
343
+ curl http://localhost:9292/api/v1/payments/{id}
344
+ # Returns: id, amount, currency, status (6 fields)
345
+
346
+ curl http://localhost:9292/api/internal/v1/payments/{id}
347
+ # Returns: all 11 fields + computed
348
+
349
+ # Access control working
350
+ curl -X POST http://localhost:9292/api/v1/payments/{id}/confirm
351
+ # 404 - not available in external facet
352
+
353
+ curl -X POST http://localhost:9292/api/internal/v1/payments/{id}/confirm
354
+ # 200 - available in internal facet
355
+ ```
356
+
357
+ ## Troubleshooting
358
+
359
+ ### Facet not mounting?
360
+
361
+ Check auto-mount logs:
362
+ ```
363
+ I, INFO -- : ✓ external → /api/v1 (4 endpoints)
364
+ ```
365
+
366
+ ### Wrong path?
367
+
368
+ Check `config/facera.rb` paths.
369
+
370
+ ### Missing capabilities?
371
+
372
+ Check facet's `allow_capabilities` / `deny_capabilities`.
373
+
374
+ ### Field not showing?
375
+
376
+ Check facet's `expose` block and field visibility rules.
@@ -0,0 +1,139 @@
1
+ # Payment Adapter
2
+ # Implements business logic for payment core capabilities
3
+ #
4
+ # This adapter uses in-memory storage for demo purposes.
5
+ # In production, you would use ActiveRecord, Sequel, or your preferred ORM.
6
+
7
+ class PaymentAdapter
8
+ include Facera::Adapter
9
+
10
+ # In-memory storage (for demo purposes)
11
+ @@payments = {}
12
+
13
+ def create_payment(params)
14
+ payment = {
15
+ id: SecureRandom.uuid,
16
+ amount: params[:amount],
17
+ currency: params[:currency],
18
+ merchant_id: params[:merchant_id],
19
+ customer_id: params[:customer_id],
20
+ status: :pending,
21
+ created_at: Time.now
22
+ }
23
+
24
+ @@payments[payment[:id]] = payment
25
+
26
+ # In production, you might:
27
+ # - Save to database: Payment.create!(params)
28
+ # - Validate with external service
29
+ # - Send notification
30
+ # - Log audit trail
31
+
32
+ payment
33
+ end
34
+
35
+ def get_payment(params)
36
+ payment = @@payments[params[:id]]
37
+
38
+ raise Facera::NotFoundError, "Payment not found" unless payment
39
+
40
+ payment
41
+ end
42
+
43
+ def list_payments(params)
44
+ payments = @@payments.values
45
+
46
+ # Apply filters
47
+ if params[:merchant_id]
48
+ payments = payments.select { |p| p[:merchant_id] == params[:merchant_id] }
49
+ end
50
+
51
+ if params[:customer_id]
52
+ payments = payments.select { |p| p[:customer_id] == params[:customer_id] }
53
+ end
54
+
55
+ if params[:status]
56
+ payments = payments.select { |p| p[:status].to_s == params[:status].to_s }
57
+ end
58
+
59
+ # Apply pagination
60
+ limit = (params[:limit] || 20).to_i
61
+ offset = (params[:offset] || 0).to_i
62
+
63
+ {
64
+ data: payments[offset, limit] || [],
65
+ meta: {
66
+ total: payments.count,
67
+ limit: limit,
68
+ offset: offset
69
+ }
70
+ }
71
+ end
72
+
73
+ def confirm_payment(params)
74
+ payment = get_payment(params)
75
+
76
+ # Validate precondition (already done by framework, but good practice)
77
+ if payment[:status] != :pending
78
+ raise Facera::PreconditionError, "Payment must be pending to confirm"
79
+ end
80
+
81
+ # Update payment
82
+ payment[:status] = :confirmed
83
+ payment[:confirmed_at] = Time.now
84
+
85
+ # In production, you might:
86
+ # - Call payment gateway API
87
+ # - Send confirmation email: PaymentMailer.confirmation(payment).deliver_later
88
+ # - Publish event: PaymentEvents.publish(:payment_confirmed, payment)
89
+ # - Update accounting system
90
+ # - Trigger fulfillment workflow
91
+
92
+ payment
93
+ end
94
+
95
+ def cancel_payment(params)
96
+ payment = get_payment(params)
97
+
98
+ if payment[:status] != :pending
99
+ raise Facera::PreconditionError, "Payment must be pending to cancel"
100
+ end
101
+
102
+ payment[:status] = :cancelled
103
+ payment[:cancelled_at] = Time.now
104
+
105
+ # In production:
106
+ # - Refund if already charged
107
+ # - Send cancellation email
108
+ # - Log cancellation reason
109
+ # - Update metrics
110
+
111
+ payment
112
+ end
113
+
114
+ def refund_payment(params)
115
+ payment = get_payment(params)
116
+
117
+ if payment[:status] != :confirmed
118
+ raise Facera::PreconditionError, "Payment must be confirmed to refund"
119
+ end
120
+
121
+ refund_amount = params[:refund_amount] || payment[:amount]
122
+
123
+ if refund_amount > payment[:amount]
124
+ raise Facera::ValidationError, "Refund amount cannot exceed payment amount"
125
+ end
126
+
127
+ payment[:status] = :refunded
128
+ payment[:refunded_at] = Time.now
129
+ payment[:refund_amount] = refund_amount
130
+
131
+ # In production:
132
+ # - Process refund with payment gateway
133
+ # - Send refund confirmation
134
+ # - Update accounting
135
+ # - Notify merchant
136
+
137
+ payment
138
+ end
139
+ end
@@ -0,0 +1,17 @@
1
+ # Application Builder
2
+ # Constructs the Rack application with middleware and auto-mounted facets
3
+
4
+ module PaymentAPI
5
+ class Application
6
+ def self.build
7
+ Rack::Builder.new do
8
+ # Middleware
9
+ use Rack::Reloader, 0 if ENV['RACK_ENV'] == 'development'
10
+ use Rack::CommonLogger
11
+
12
+ # Auto-mount all Facera facets and introspection API
13
+ Facera.auto_mount!(self)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,33 @@
1
+ # Facera Initializer
2
+ # Configure Facera behavior and facet paths
3
+
4
+ Facera.configure do |config|
5
+ # Base path for all APIs
6
+ config.base_path = '/api'
7
+
8
+ # API version
9
+ config.version = 'v1'
10
+
11
+ # Custom paths for different facets
12
+ config.facet_path :external, '/v1' # /api/v1
13
+ config.facet_path :internal, '/internal/v1' # /api/internal/v1
14
+ config.facet_path :operator, '/operator/v1' # /api/operator/v1
15
+
16
+ # Enable/disable features
17
+ config.dashboard = false # Not implemented yet
18
+ config.generate_docs = true
19
+
20
+ # Disable facets based on environment
21
+ # config.disable_facet :operator unless ENV['ENABLE_OPERATOR_API']
22
+
23
+ # Authentication handlers (example)
24
+ # config.authenticate :external do |request|
25
+ # token = request.headers['Authorization']&.sub(/^Bearer /, '')
26
+ # User.find_by_token(token)
27
+ # end
28
+ #
29
+ # config.authenticate :internal do |request|
30
+ # service_token = request.headers['X-Service-Token']
31
+ # Service.verify_token(service_token)
32
+ # end
33
+ end
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env rackup
2
+ # Facera Multi-Facet Payment API
3
+ # Run: rackup -p 9292
4
+
5
+ require_relative '../../lib/facera'
6
+ require_relative 'config/facera'
7
+ require_relative 'application'
8
+
9
+ # Run the application
10
+ run PaymentAPI::Application.build
@@ -0,0 +1,82 @@
1
+ # Payment Core
2
+ # Defines the semantic model for payment operations
3
+
4
+ Facera.define_core(:payment) do
5
+ # Payment entity with all attributes
6
+ entity :payment do
7
+ # Immutable identifiers
8
+ attribute :id, :uuid, immutable: true
9
+ attribute :created_at, :timestamp, immutable: true
10
+
11
+ # Required payment information
12
+ attribute :amount, :money, required: true
13
+ attribute :currency, :string, required: true
14
+ attribute :merchant_id, :uuid, required: true
15
+ attribute :customer_id, :uuid, required: true
16
+
17
+ # Optional information
18
+ attribute :description, :string
19
+ attribute :metadata, :hash
20
+
21
+ # State tracking
22
+ attribute :status, :enum, values: [:pending, :confirmed, :cancelled]
23
+ attribute :confirmed_at, :timestamp
24
+ attribute :cancelled_at, :timestamp
25
+ end
26
+
27
+ # Business invariants
28
+ invariant :positive_amount, description: "Payment amount must be positive" do
29
+ amount.nil? || amount > 0
30
+ end
31
+
32
+ invariant :valid_status_transitions, description: "Only valid state transitions allowed" do
33
+ # This would check against actual previous state in a real implementation
34
+ true
35
+ end
36
+
37
+ # Create a new payment
38
+ capability :create_payment, type: :create do
39
+ entity :payment
40
+ requires :amount, :currency, :merchant_id, :customer_id
41
+ optional :description, :metadata
42
+
43
+ validates do
44
+ amount > 0
45
+ end
46
+ end
47
+
48
+ # Retrieve a specific payment
49
+ capability :get_payment, type: :get do
50
+ entity :payment
51
+ requires :id
52
+ end
53
+
54
+ # List payments with optional filters
55
+ capability :list_payments, type: :list do
56
+ entity :payment
57
+ optional :limit, :offset, :merchant_id, :customer_id, :status
58
+ filterable :merchant_id, :customer_id, :status
59
+ end
60
+
61
+ # Confirm a pending payment
62
+ capability :confirm_payment, type: :action do
63
+ entity :payment
64
+ requires :id
65
+ optional :confirmation_code
66
+
67
+ precondition { status == :pending }
68
+ transitions_to :confirmed
69
+ sets confirmed_at: -> { Time.now }
70
+ end
71
+
72
+ # Cancel a pending payment
73
+ capability :cancel_payment, type: :action do
74
+ entity :payment
75
+ requires :id
76
+ optional :reason
77
+
78
+ precondition { status == :pending }
79
+ transitions_to :cancelled
80
+ sets cancelled_at: -> { Time.now }
81
+ end
82
+ end
@@ -0,0 +1,38 @@
1
+ # External Facet
2
+ # Public-facing API for external clients
3
+ # Exposes limited fields and capabilities for security
4
+
5
+ Facera.define_facet(:external, core: :payment) do
6
+ description "Public API for external clients"
7
+
8
+ # Expose only safe fields to external consumers
9
+ expose :payment do
10
+ fields :id, :amount, :currency, :status, :description, :created_at
11
+
12
+ # Hide sensitive internal fields
13
+ hide :merchant_id, :customer_id, :metadata, :confirmed_at, :cancelled_at
14
+
15
+ # Use camelCase for external API (common in JavaScript/TypeScript clients)
16
+ alias_field :created_at, as: :createdAt
17
+ end
18
+
19
+ # Limit capabilities to read operations and creation
20
+ allow_capabilities :create_payment, :get_payment, :list_payments
21
+
22
+ # Explicitly deny admin operations
23
+ deny_capabilities :confirm_payment, :cancel_payment
24
+
25
+ # Scope list operations to current customer
26
+ # In a real app, this would use authentication context
27
+ scope :list_payments do
28
+ # This would be: { customer_id: current_user.id }
29
+ # For demo purposes, we'll just pass through
30
+ {}
31
+ end
32
+
33
+ # Minimal error messages for security
34
+ error_verbosity :minimal
35
+
36
+ # Optional: Rate limiting configuration
37
+ rate_limit requests: 1000, per: :hour
38
+ end