@hyodotdev/openiap-commerce-protocol 0.0.0-bootstrap.0 → 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 (42) hide show
  1. package/CONVENTION.md +168 -0
  2. package/DESIGN.md +1056 -0
  3. package/README.md +227 -5
  4. package/SPEC.md +1471 -0
  5. package/conformance/index.d.ts +303 -0
  6. package/conformance/index.mjs +2126 -0
  7. package/conformance/mock-provider.mjs +491 -0
  8. package/examples/entitlement-granted-no-subscription.json +12 -0
  9. package/examples/entitlement-revoked.json +21 -0
  10. package/examples/provider-capabilities.json +209 -0
  11. package/examples/store-event-mapping.json +287 -0
  12. package/examples/subscription-canceled.json +22 -0
  13. package/examples/subscription-product-changed.json +30 -0
  14. package/examples/subscription-renewed.json +29 -0
  15. package/examples/verify-purchase-request.json +6 -0
  16. package/examples/verify-purchase-result.json +7 -0
  17. package/generated/bindings/graphql-operations.json +87 -0
  18. package/generated/bindings/http-binding.json +143 -0
  19. package/generated/bindings/introspection-signature.json +320 -0
  20. package/generated/bindings/operations-sdl.json +4 -0
  21. package/generated/bindings/operations.graphql +366 -0
  22. package/generated/commerce-protocol.graphql +1219 -0
  23. package/generated/openapi/commerce-protocol.openapi.json +1413 -0
  24. package/generated/schemas/commerce-event.schema.json +499 -0
  25. package/generated/schemas/commerce-protocol.bundle.schema.json +1576 -0
  26. package/generated/schemas/operations.schema.json +578 -0
  27. package/generated/schemas/primitives.schema.json +101 -0
  28. package/generated/schemas/provider-capabilities.schema.json +205 -0
  29. package/generated/schemas/store-event-mapping.schema.json +211 -0
  30. package/generated/vectors/lifecycle.json +908 -0
  31. package/generated/vectors/operations.json +1122 -0
  32. package/package.json +62 -12
  33. package/schema/01-primitives.graphql +102 -0
  34. package/schema/02-commerce-event.graphql +195 -0
  35. package/schema/03-provider-capabilities.graphql +139 -0
  36. package/schema/04-store-event-mapping.graphql +98 -0
  37. package/schema/05-operations.graphql +461 -0
  38. package/schema/06-compiler-vocabulary.graphql +139 -0
  39. package/schema/07-protocol-metadata.graphql +76 -0
  40. package/src/index.d.ts +63 -0
  41. package/src/index.mjs +121 -0
  42. package/vectors/signatures.json +139 -0
@@ -0,0 +1,1576 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://openiap.dev/schemas/commerce-protocol/1.0/commerce-protocol.bundle.schema.json",
4
+ "title": "OpenIAP Commerce Protocol 1.0 (self-contained)",
5
+ "description": "Every schema in this specification, in one document with no external reference. Validate against #/$defs/CommerceEvent, #/$defs/ProviderCapabilities, #/$defs/StoreEventMapping, or any operation input/result definition. Generated from commerce-protocol.graphql — do not edit this file.",
6
+ "$defs": {
7
+ "Store": {
8
+ "description": "The commerce platform that is authoritative for the purchase. This is a store, not a device platform: one device platform can host several stores. The value space is OPEN. The listed values are the stores this version names, but an implementation that observes commerce on another platform MUST be able to emit it, and a consumer MUST accept and preserve an unrecognised store opaquely rather than reject the event. Use a lowercase, stable, unambiguous token.",
9
+ "type": "string",
10
+ "pattern": "^[a-z][a-z0-9_]*$",
11
+ "examples": [
12
+ "apple",
13
+ "google",
14
+ "horizon",
15
+ "amazon"
16
+ ]
17
+ },
18
+ "Environment": {
19
+ "description": "The store environment the purchase belongs to. The value space is open: receivers MUST accept and preserve an unrecognised value opaquely and MUST NOT reject an event because of it.",
20
+ "type": "string",
21
+ "minLength": 1,
22
+ "examples": [
23
+ "production",
24
+ "sandbox",
25
+ "xcode"
26
+ ]
27
+ },
28
+ "Timestamp": {
29
+ "description": "An instant, as integer milliseconds since the Unix epoch, UTC. Every timestamp in this specification uses this encoding. The one exception is the transport signature timestamp, which is in seconds; see the webhook section of SPEC.md.",
30
+ "type": "integer",
31
+ "minimum": 0
32
+ },
33
+ "Identifier": {
34
+ "description": "An opaque, implementation-assigned identifier. Consumers MUST treat it as an opaque string and MUST NOT parse structure out of it.",
35
+ "type": "string",
36
+ "minLength": 1,
37
+ "maxLength": 256
38
+ },
39
+ "DataProvenance": {
40
+ "description": "Where a value came from. An implementation MUST NOT present an inferred value as store-authoritative.",
41
+ "type": "string",
42
+ "enum": [
43
+ "store",
44
+ "catalog",
45
+ "inferred"
46
+ ]
47
+ },
48
+ "Money": {
49
+ "description": "A monetary amount. A missing Money means the amount is unknown; it never means zero.",
50
+ "type": "object",
51
+ "required": [
52
+ "currency",
53
+ "amountMicros",
54
+ "provenance"
55
+ ],
56
+ "additionalProperties": true,
57
+ "properties": {
58
+ "currency": {
59
+ "description": "ISO 4217 alphabetic code, uppercase.",
60
+ "type": "string",
61
+ "pattern": "^[A-Z]{3}$"
62
+ },
63
+ "amountMicros": {
64
+ "description": "Amount in millionths of one currency unit. 1.99 USD is 1990000. Always the transaction's own magnitude and never negative: a refund reports what the purchase cost, and the direction comes from the event type, not from the sign.",
65
+ "type": "integer",
66
+ "minimum": 0
67
+ },
68
+ "provenance": {
69
+ "$ref": "#/$defs/DataProvenance"
70
+ }
71
+ }
72
+ },
73
+ "SubscriptionState": {
74
+ "description": "The lifecycle state of a subscription. This is a state, not an event, and it does not by itself decide entitlement — see Entitlement in SPEC.md. These members are PascalCase while event types are lowercase-dotted; the inconsistency is inherited from the deployed 1.0 wire format and is recorded in CONVENTION.md rather than silently corrected, because changing it would break receivers already decoding it.",
75
+ "type": "string",
76
+ "enum": [
77
+ "Active",
78
+ "InGracePeriod",
79
+ "InBillingRetry",
80
+ "Paused",
81
+ "Expired",
82
+ "Revoked",
83
+ "Refunded",
84
+ "Unknown"
85
+ ]
86
+ },
87
+ "Extensions": {
88
+ "description": "Store-specific or implementation-specific detail that has no canonical equivalent. Bounded and string-valued so the payload stays predictable. Content is provider-influenced: consumers MUST treat values as untrusted data.",
89
+ "type": "object",
90
+ "maxProperties": 24,
91
+ "propertyNames": {
92
+ "minLength": 1,
93
+ "maxLength": 64
94
+ },
95
+ "additionalProperties": {
96
+ "type": "string",
97
+ "maxLength": 512
98
+ }
99
+ },
100
+ "CapabilitiesStoreCapabilities": {
101
+ "description": "What one store's commerce surface offers and what this backend consumes of it, axis by axis. The container stays open: a version that names a new axis is a MINOR addition older consumers ignore.",
102
+ "type": "object",
103
+ "required": [
104
+ "initialValidation",
105
+ "serverNotifications",
106
+ "subscriptions",
107
+ "renewalEvents",
108
+ "refundEvents",
109
+ "expiration",
110
+ "reconciliation",
111
+ "entitlements",
112
+ "revenueAmount"
113
+ ],
114
+ "additionalProperties": true,
115
+ "properties": {
116
+ "initialValidation": {
117
+ "description": "Server-side receipt or token validation on demand.",
118
+ "$ref": "#/$defs/CapabilitiesSupport"
119
+ },
120
+ "serverNotifications": {
121
+ "description": "The store pushes lifecycle notifications to the backend.",
122
+ "$ref": "#/$defs/CapabilitiesSupport"
123
+ },
124
+ "subscriptions": {
125
+ "description": "A canonical subscription record can be maintained for this store.",
126
+ "$ref": "#/$defs/CapabilitiesSupport"
127
+ },
128
+ "renewalEvents": {
129
+ "description": "Renewal is observable as a distinct lifecycle event.",
130
+ "$ref": "#/$defs/CapabilitiesSupport"
131
+ },
132
+ "refundEvents": {
133
+ "description": "Refund or revocation is observable as a distinct lifecycle event.",
134
+ "$ref": "#/$defs/CapabilitiesSupport"
135
+ },
136
+ "expiration": {
137
+ "description": "Expiration is observable, rather than only inferred from a timestamp having passed.",
138
+ "$ref": "#/$defs/CapabilitiesSupport"
139
+ },
140
+ "reconciliation": {
141
+ "description": "A scheduled pass re-reads authoritative store state, so a lost notification can self-heal.",
142
+ "$ref": "#/$defs/CapabilitiesSupport"
143
+ },
144
+ "entitlements": {
145
+ "description": "Entitlement is derivable for access gating.",
146
+ "$ref": "#/$defs/CapabilitiesSupport"
147
+ },
148
+ "revenueAmount": {
149
+ "description": "The store asserts an amount that can be attributed to revenue with `store` provenance.",
150
+ "$ref": "#/$defs/CapabilitiesSupport"
151
+ }
152
+ }
153
+ },
154
+ "CapabilitiesSupport": {
155
+ "description": "Two axes, deliberately separate. `provider` is what the store's own API offers; `implementation` is what this backend actually consumes. They differ in practice — a store can publish a notification channel that a given backend has not integrated — and collapsing them into one boolean produces a dishonest matrix.",
156
+ "type": "object",
157
+ "required": [
158
+ "provider",
159
+ "implementation"
160
+ ],
161
+ "additionalProperties": false,
162
+ "properties": {
163
+ "provider": {
164
+ "description": "Whether the store's own API offers this at all. A fact about the store, independent of any backend.",
165
+ "type": "boolean"
166
+ },
167
+ "implementation": {
168
+ "description": "Whether this backend consumes it. False against a true `provider` is an implementation gap, not a store limitation.",
169
+ "type": "boolean"
170
+ },
171
+ "notes": {
172
+ "description": "Required whenever `provider` and `implementation` disagree, or whenever either is false. States the concrete limitation in prose a support engineer can act on.",
173
+ "type": "string",
174
+ "minLength": 1
175
+ }
176
+ },
177
+ "anyOf": [
178
+ {
179
+ "properties": {
180
+ "provider": {
181
+ "const": true
182
+ },
183
+ "implementation": {
184
+ "const": true
185
+ }
186
+ }
187
+ },
188
+ {
189
+ "properties": {
190
+ "notes": {
191
+ "type": "string",
192
+ "minLength": 1
193
+ }
194
+ },
195
+ "required": [
196
+ "notes"
197
+ ]
198
+ }
199
+ ],
200
+ "if": {
201
+ "properties": {
202
+ "implementation": {
203
+ "const": true
204
+ }
205
+ },
206
+ "required": [
207
+ "implementation"
208
+ ]
209
+ },
210
+ "then": {
211
+ "properties": {
212
+ "provider": {
213
+ "const": true
214
+ }
215
+ },
216
+ "required": [
217
+ "provider"
218
+ ]
219
+ }
220
+ },
221
+ "MappingStoreMapping": {
222
+ "description": "One store's notification lane: the channel it publishes, the rows that translate its notifications, and what an emitter could still derive without one.",
223
+ "type": "object",
224
+ "required": [
225
+ "notificationChannel",
226
+ "mappings"
227
+ ],
228
+ "additionalProperties": true,
229
+ "properties": {
230
+ "notificationChannel": {
231
+ "description": "The store's own server-to-server notification product, or null when the store publishes none. This is a fact about the STORE, never about any implementation: a channel a store publishes but this version does not map is still named here, with `mappings` empty and `notes` saying why. Recording an unmapped channel as null would collapse a store limitation and a specification gap into the same value, which is the conflation the capability descriptor's two axes exist to prevent.",
232
+ "type": [
233
+ "string",
234
+ "null"
235
+ ]
236
+ },
237
+ "notes": {
238
+ "description": "Required when there is no notification channel, or when the store's semantics need a caveat an implementer would otherwise get wrong.",
239
+ "type": "string",
240
+ "minLength": 1
241
+ },
242
+ "derivableByPolling": {
243
+ "description": "Events an implementation could produce for this store by re-asking an authoritative endpoint on a schedule rather than by consuming a notification. Present so that a store this version maps no notifications for is not mistaken for a store with no possible signal — which is true whether the store publishes a channel that went unmapped or publishes none at all. This version specifies no cadence, so an implementation that derives these MUST still declare its capabilities honestly rather than claim parity with a store whose notifications it consumes.",
244
+ "type": "array",
245
+ "items": {
246
+ "type": "string",
247
+ "pattern": "^entitlement\\.[a-z_]+$"
248
+ },
249
+ "uniqueItems": true
250
+ },
251
+ "mappings": {
252
+ "description": "Notification-to-event rows. Empty when the store publishes no channel, or when it publishes one this version does not map; `notes` MUST distinguish the two.",
253
+ "type": "array",
254
+ "items": {
255
+ "$ref": "#/$defs/MappingMapping"
256
+ }
257
+ }
258
+ },
259
+ "anyOf": [
260
+ {
261
+ "properties": {
262
+ "notificationChannel": {
263
+ "type": "string"
264
+ },
265
+ "mappings": {
266
+ "type": "array",
267
+ "minItems": 1
268
+ }
269
+ },
270
+ "required": [
271
+ "notificationChannel",
272
+ "mappings"
273
+ ]
274
+ },
275
+ {
276
+ "properties": {
277
+ "notes": {
278
+ "type": "string",
279
+ "minLength": 1
280
+ }
281
+ },
282
+ "required": [
283
+ "notes"
284
+ ]
285
+ }
286
+ ],
287
+ "allOf": [
288
+ {
289
+ "if": {
290
+ "properties": {
291
+ "notificationChannel": {
292
+ "type": "null"
293
+ }
294
+ },
295
+ "required": [
296
+ "notificationChannel"
297
+ ]
298
+ },
299
+ "then": {
300
+ "properties": {
301
+ "mappings": {
302
+ "type": "array",
303
+ "maxItems": 0
304
+ }
305
+ },
306
+ "required": [
307
+ "mappings"
308
+ ]
309
+ }
310
+ }
311
+ ]
312
+ },
313
+ "MappingMapping": {
314
+ "description": "One notification-to-event row. Closed: an unrecognised qualifier would leave a reader selecting this row on fewer conditions than its author intended.",
315
+ "type": "object",
316
+ "required": [
317
+ "storeNotification",
318
+ "event"
319
+ ],
320
+ "additionalProperties": false,
321
+ "properties": {
322
+ "storeNotification": {
323
+ "description": "The notification type in the store's own vocabulary, verbatim.",
324
+ "type": "string",
325
+ "minLength": 1
326
+ },
327
+ "storeNotificationCode": {
328
+ "description": "The value the store actually transmits, when it differs from the human-readable name. Google Play sends a numeric notificationType; the names are documentation labels that never appear on the wire.",
329
+ "type": "string",
330
+ "minLength": 1
331
+ },
332
+ "storeSubtype": {
333
+ "description": "A qualifier the store sends alongside the type. Null means this row is the default for that notification: it applies when the store sends no subtype, and also when it sends one no other row claims.",
334
+ "type": [
335
+ "string",
336
+ "null"
337
+ ]
338
+ },
339
+ "whenNoPriorStoreEvent": {
340
+ "description": "Restricts this row to the case where the emitter has seen no earlier store notification for this purchase. That covers both a purchase it has never heard of and one it learned about from a client receipt but never from the store — in each case the notification is the beginning of the story it can tell. A purchase with store history behind it takes the unconditional row instead.",
341
+ "type": "boolean",
342
+ "const": true
343
+ },
344
+ "whenPreviousState": {
345
+ "description": "Restricts this row to the case where the subscription was in one of these states immediately before the notification. Use it only where the store itself provides no qualifier: Google sends SUBSCRIPTION_RECOVERED for both a recovery and a resume from pause and marks nothing, so prior state is the only separator. Where a store does send a qualifier, use `storeSubtype` instead. A row without any condition is the default for its (notification, subtype) pair, and an implementation MUST prefer a conditional row that matches.",
346
+ "type": "array",
347
+ "items": {
348
+ "$ref": "#/$defs/SubscriptionState"
349
+ },
350
+ "minItems": 1,
351
+ "uniqueItems": true
352
+ },
353
+ "event": {
354
+ "description": "The normalized event type to emit, or null when the notification is received but produces no event — an audit-only or informational notification. A null event is a deliberate mapping, not a gap. Never an `entitlement.*` type: SPEC.md 9.2 derives those from the gate, not from a notification.",
355
+ "type": [
356
+ "string",
357
+ "null"
358
+ ],
359
+ "pattern": "^subscription\\.[a-z_]+$"
360
+ },
361
+ "notes": {
362
+ "description": "Why this row is what it is, in prose an implementer can act on. Required wherever the row emits nothing, so a reader can tell a deliberate no-op from an oversight.",
363
+ "type": "string",
364
+ "minLength": 1
365
+ }
366
+ },
367
+ "if": {
368
+ "properties": {
369
+ "event": {
370
+ "type": "null"
371
+ }
372
+ }
373
+ },
374
+ "then": {
375
+ "properties": {
376
+ "notes": {
377
+ "type": "string",
378
+ "minLength": 1
379
+ }
380
+ },
381
+ "required": [
382
+ "notes"
383
+ ]
384
+ },
385
+ "not": {
386
+ "properties": {
387
+ "whenNoPriorStoreEvent": {
388
+ "type": "boolean"
389
+ },
390
+ "whenPreviousState": {
391
+ "type": "array"
392
+ }
393
+ },
394
+ "required": [
395
+ "whenNoPriorStoreEvent",
396
+ "whenPreviousState"
397
+ ]
398
+ }
399
+ },
400
+ "ProtocolError": {
401
+ "description": "One protocol operation failure. Both bindings carry the same code space: REST wraps it in ProtocolErrorResponse, GraphQL carries `code` in `errors[].extensions`. A message is human-readable and MUST NOT contain credentials, store evidence, stack traces, or implementation source paths.",
402
+ "type": "object",
403
+ "required": [
404
+ "code",
405
+ "message"
406
+ ],
407
+ "additionalProperties": false,
408
+ "properties": {
409
+ "code": {
410
+ "description": "A portable protocol error code. The value space is OPEN: a caller MUST tolerate a code it does not recognise and treat it as a failure of the operation, and a MINOR version can add a code. The listed values are the ones this version names; SPEC.md 8 defines each one and its HTTP status.",
411
+ "type": "string",
412
+ "pattern": "^[A-Z][A-Z0-9_]*$",
413
+ "examples": [
414
+ "INVALID_REQUEST",
415
+ "UNAUTHORIZED",
416
+ "FORBIDDEN",
417
+ "NOT_FOUND",
418
+ "PURCHASE_NOT_FOUND",
419
+ "VERIFICATION_FAILED",
420
+ "CONFLICT",
421
+ "RATE_LIMITED",
422
+ "UNSUPPORTED_PROFILE",
423
+ "UNSUPPORTED_STORE",
424
+ "INTERNAL_ERROR"
425
+ ]
426
+ },
427
+ "message": {
428
+ "description": "Human-readable failure summary. Never carries credentials, store evidence, stack traces, or source paths.",
429
+ "type": "string",
430
+ "minLength": 1
431
+ }
432
+ }
433
+ },
434
+ "ProtocolErrorResponse": {
435
+ "description": "The REST error envelope. Every non-success REST response carries exactly this shape — CLOSED, so an error body cannot smuggle members past the tokenless rules (a failure response is the easiest place to hide a leak, because callers rarely inspect it).",
436
+ "type": "object",
437
+ "required": [
438
+ "error"
439
+ ],
440
+ "additionalProperties": false,
441
+ "properties": {
442
+ "error": {
443
+ "$ref": "#/$defs/ProtocolError"
444
+ }
445
+ }
446
+ },
447
+ "AppleEvidence": {
448
+ "description": "Store evidence for one Apple App Store purchase: the signed StoreKit 2 transaction JWS the app received from the store.",
449
+ "type": "object",
450
+ "required": [
451
+ "jws"
452
+ ],
453
+ "additionalProperties": true,
454
+ "properties": {
455
+ "jws": {
456
+ "description": "A compact signed transaction JWS. Bounded so an oversized body cannot be forwarded to a store: an Apple transaction JWS is a few kilobytes.",
457
+ "type": "string",
458
+ "minLength": 1,
459
+ "maxLength": 16384
460
+ }
461
+ }
462
+ },
463
+ "GoogleEvidence": {
464
+ "description": "Store evidence for one Google Play purchase: the opaque purchase token the store issued to the device.",
465
+ "type": "object",
466
+ "required": [
467
+ "purchaseToken"
468
+ ],
469
+ "additionalProperties": true,
470
+ "properties": {
471
+ "purchaseToken": {
472
+ "description": "An opaque store purchase token. Bounded to keep an oversized body off the store API.",
473
+ "type": "string",
474
+ "minLength": 1,
475
+ "maxLength": 4096
476
+ }
477
+ }
478
+ },
479
+ "HorizonEvidence": {
480
+ "description": "Store evidence for one Meta Horizon (Quest) entitlement. Horizon issues no server-verifiable receipt, so verification identifies the entitlement by user and SKU; the provider holds the store credentials.",
481
+ "type": "object",
482
+ "required": [
483
+ "userId",
484
+ "sku"
485
+ ],
486
+ "additionalProperties": true,
487
+ "properties": {
488
+ "userId": {
489
+ "description": "A store-issued user identity, as the store's own API returns it. Distinct from the protocol's opaque `userId`.",
490
+ "type": "string",
491
+ "minLength": 1,
492
+ "maxLength": 512
493
+ },
494
+ "sku": {
495
+ "description": "A store add-on SKU. Bounded to a predictable identifier length.",
496
+ "type": "string",
497
+ "minLength": 1,
498
+ "maxLength": 256
499
+ }
500
+ }
501
+ },
502
+ "AmazonEvidence": {
503
+ "description": "Store evidence for one Amazon Appstore purchase, as returned by the Amazon SDK. `sandbox` selects the RVS sandbox for App Tester receipts where the provider permits it.",
504
+ "type": "object",
505
+ "required": [
506
+ "userId",
507
+ "receiptId"
508
+ ],
509
+ "additionalProperties": true,
510
+ "properties": {
511
+ "userId": {
512
+ "description": "A store-issued user identity, as the store's own API returns it. Distinct from the protocol's opaque `userId`.",
513
+ "type": "string",
514
+ "minLength": 1,
515
+ "maxLength": 512
516
+ },
517
+ "receiptId": {
518
+ "description": "A store receipt identifier. Bounded to keep an oversized body off the store API.",
519
+ "type": "string",
520
+ "minLength": 1,
521
+ "maxLength": 4096
522
+ },
523
+ "sandbox": {
524
+ "description": "Verify against the store's sandbox where the provider permits it. Omitted means production.",
525
+ "type": "boolean"
526
+ }
527
+ }
528
+ },
529
+ "VerifyPurchaseInput": {
530
+ "description": "One purchase's store evidence, discriminated by `store`. The store space stays open: a provider that does not integrate the named store rejects the call with UNSUPPORTED_STORE rather than failing schema validation, so a future store is a MINOR evidence-member addition, not a breaking change. For each store this version names, the matching evidence member is required.",
531
+ "type": "object",
532
+ "required": [
533
+ "store"
534
+ ],
535
+ "additionalProperties": true,
536
+ "properties": {
537
+ "store": {
538
+ "$ref": "#/$defs/Store"
539
+ },
540
+ "apple": {
541
+ "$ref": "#/$defs/AppleEvidence"
542
+ },
543
+ "google": {
544
+ "$ref": "#/$defs/GoogleEvidence"
545
+ },
546
+ "horizon": {
547
+ "$ref": "#/$defs/HorizonEvidence"
548
+ },
549
+ "amazon": {
550
+ "$ref": "#/$defs/AmazonEvidence"
551
+ }
552
+ },
553
+ "allOf": [
554
+ {
555
+ "if": {
556
+ "properties": {
557
+ "store": {
558
+ "const": "apple"
559
+ }
560
+ },
561
+ "required": [
562
+ "store"
563
+ ]
564
+ },
565
+ "then": {
566
+ "properties": {
567
+ "apple": {}
568
+ },
569
+ "required": [
570
+ "apple"
571
+ ]
572
+ }
573
+ },
574
+ {
575
+ "if": {
576
+ "properties": {
577
+ "store": {
578
+ "const": "google"
579
+ }
580
+ },
581
+ "required": [
582
+ "store"
583
+ ]
584
+ },
585
+ "then": {
586
+ "properties": {
587
+ "google": {}
588
+ },
589
+ "required": [
590
+ "google"
591
+ ]
592
+ }
593
+ },
594
+ {
595
+ "if": {
596
+ "properties": {
597
+ "store": {
598
+ "const": "horizon"
599
+ }
600
+ },
601
+ "required": [
602
+ "store"
603
+ ]
604
+ },
605
+ "then": {
606
+ "properties": {
607
+ "horizon": {}
608
+ },
609
+ "required": [
610
+ "horizon"
611
+ ]
612
+ }
613
+ },
614
+ {
615
+ "if": {
616
+ "properties": {
617
+ "store": {
618
+ "const": "amazon"
619
+ }
620
+ },
621
+ "required": [
622
+ "store"
623
+ ]
624
+ },
625
+ "then": {
626
+ "properties": {
627
+ "amazon": {}
628
+ },
629
+ "required": [
630
+ "amazon"
631
+ ]
632
+ }
633
+ }
634
+ ]
635
+ },
636
+ "VerifyPurchaseResult": {
637
+ "description": "The verification verdict. `isValid` is the authoritative acceptance gate: a caller MUST read it and MUST NOT re-derive acceptance from `state`. A verdict is a statement about the evidence at verification time, never an account mutation — verification binds no user.",
638
+ "type": "object",
639
+ "required": [
640
+ "store",
641
+ "isValid",
642
+ "state"
643
+ ],
644
+ "additionalProperties": true,
645
+ "properties": {
646
+ "store": {
647
+ "$ref": "#/$defs/Store"
648
+ },
649
+ "isValid": {
650
+ "description": "Whether the provider accepts the evidence. The single authoritative acceptance gate.",
651
+ "type": "boolean"
652
+ },
653
+ "state": {
654
+ "description": "The verification verdict for one piece of purchase evidence, as a normalized token. This is the purchase-validation axis SPEC.md 2.3 warns about — a CANCELED verification verdict and a canceled-but-still-entitled subscription are different facts. The value space is OPEN: a caller MUST tolerate a token it does not know and gate on `isValid` alone.",
655
+ "type": "string",
656
+ "pattern": "^[A-Z][A-Z0-9_]*$",
657
+ "examples": [
658
+ "ENTITLED",
659
+ "PENDING_ACKNOWLEDGMENT",
660
+ "PENDING",
661
+ "CANCELED",
662
+ "EXPIRED",
663
+ "READY_TO_CONSUME",
664
+ "CONSUMED",
665
+ "UNKNOWN",
666
+ "INAUTHENTIC"
667
+ ]
668
+ },
669
+ "productId": {
670
+ "description": "The product the store verified, when its response exposes one. Never the caller's claim.",
671
+ "type": "string",
672
+ "minLength": 1
673
+ },
674
+ "environment": {
675
+ "description": "The store environment the provider verified against, using the same open value space as the event envelope.",
676
+ "$ref": "#/$defs/Environment"
677
+ }
678
+ }
679
+ },
680
+ "BindPurchaseInput": {
681
+ "description": "Connects verified store evidence to the adopter's own opaque user identity. Binding is a server-side account mutation: only the server role may call it, and possession of a purchase token is deliberately not proof of ownership — a provider MUST refuse to move an existing binding through this operation.",
682
+ "type": "object",
683
+ "required": [
684
+ "userId",
685
+ "store"
686
+ ],
687
+ "additionalProperties": true,
688
+ "properties": {
689
+ "userId": {
690
+ "$ref": "#/$defs/Identifier"
691
+ },
692
+ "store": {
693
+ "$ref": "#/$defs/Store"
694
+ },
695
+ "apple": {
696
+ "$ref": "#/$defs/AppleEvidence"
697
+ },
698
+ "google": {
699
+ "$ref": "#/$defs/GoogleEvidence"
700
+ },
701
+ "horizon": {
702
+ "$ref": "#/$defs/HorizonEvidence"
703
+ },
704
+ "amazon": {
705
+ "$ref": "#/$defs/AmazonEvidence"
706
+ }
707
+ },
708
+ "allOf": [
709
+ {
710
+ "if": {
711
+ "properties": {
712
+ "store": {
713
+ "const": "apple"
714
+ }
715
+ },
716
+ "required": [
717
+ "store"
718
+ ]
719
+ },
720
+ "then": {
721
+ "properties": {
722
+ "apple": {}
723
+ },
724
+ "required": [
725
+ "apple"
726
+ ]
727
+ }
728
+ },
729
+ {
730
+ "if": {
731
+ "properties": {
732
+ "store": {
733
+ "const": "google"
734
+ }
735
+ },
736
+ "required": [
737
+ "store"
738
+ ]
739
+ },
740
+ "then": {
741
+ "properties": {
742
+ "google": {}
743
+ },
744
+ "required": [
745
+ "google"
746
+ ]
747
+ }
748
+ },
749
+ {
750
+ "if": {
751
+ "properties": {
752
+ "store": {
753
+ "const": "horizon"
754
+ }
755
+ },
756
+ "required": [
757
+ "store"
758
+ ]
759
+ },
760
+ "then": {
761
+ "properties": {
762
+ "horizon": {}
763
+ },
764
+ "required": [
765
+ "horizon"
766
+ ]
767
+ }
768
+ },
769
+ {
770
+ "if": {
771
+ "properties": {
772
+ "store": {
773
+ "const": "amazon"
774
+ }
775
+ },
776
+ "required": [
777
+ "store"
778
+ ]
779
+ },
780
+ "then": {
781
+ "properties": {
782
+ "amazon": {}
783
+ },
784
+ "required": [
785
+ "amazon"
786
+ ]
787
+ }
788
+ }
789
+ ]
790
+ },
791
+ "BindPurchaseResult": {
792
+ "description": "Whether a binding now exists between this user and the evidenced purchase. `bound: false` covers every non-binding outcome — unknown evidence, evidence bound to a different user, or a store the provider cannot bind — without distinguishing them, so the operation cannot be used to probe whether someone else's purchase exists. Re-binding the same user to the same evidence is idempotent and reports `bound: true`.",
793
+ "type": "object",
794
+ "required": [
795
+ "bound"
796
+ ],
797
+ "additionalProperties": true,
798
+ "properties": {
799
+ "bound": {
800
+ "description": "True when a binding between this user and the evidenced purchase now exists.",
801
+ "type": "boolean"
802
+ }
803
+ }
804
+ },
805
+ "SubscriptionStatusInput": {
806
+ "description": "Selects the user whose subscription standing the caller's backend reads.",
807
+ "type": "object",
808
+ "required": [
809
+ "userId"
810
+ ],
811
+ "additionalProperties": true,
812
+ "properties": {
813
+ "userId": {
814
+ "description": "The opaque app-scoped user identity the caller's backend selected, in the identity space shared by provider and caller.",
815
+ "$ref": "#/$defs/Identifier"
816
+ }
817
+ }
818
+ },
819
+ "SubscriptionStatusSnapshot": {
820
+ "description": "One subscription as the provider currently records it, shaped for a server-side read. It is tokenless by construction: this object is CLOSED, so the schema itself rejects any member beyond those listed — no purchase token, store transaction identity, signed receipt, or provider-internal record identifier can appear, and the response is safe to hold in a developer backend.",
821
+ "type": "object",
822
+ "required": [
823
+ "productId",
824
+ "state",
825
+ "active"
826
+ ],
827
+ "additionalProperties": false,
828
+ "properties": {
829
+ "productId": {
830
+ "description": "Store product identifier of the subscription.",
831
+ "type": "string",
832
+ "minLength": 1
833
+ },
834
+ "state": {
835
+ "$ref": "#/$defs/SubscriptionState"
836
+ },
837
+ "active": {
838
+ "description": "The entitlement gate for this subscription, evaluated with the predicate in SPEC.md 2.3 at the provider's read time. A caller gates access on this member, never on `state`.",
839
+ "type": "boolean"
840
+ },
841
+ "store": {
842
+ "$ref": "#/$defs/Store"
843
+ },
844
+ "expiresAt": {
845
+ "$ref": "#/$defs/Timestamp"
846
+ },
847
+ "renewsAt": {
848
+ "$ref": "#/$defs/Timestamp"
849
+ },
850
+ "willRenew": {
851
+ "description": "Whether the store will attempt another billing period, when the provider records it.",
852
+ "type": "boolean"
853
+ },
854
+ "cancellationReason": {
855
+ "description": "Why the subscription stopped renewing, as the same open normalized token space the event envelope uses. Advisory, never a billing fact.",
856
+ "type": "string",
857
+ "minLength": 1
858
+ },
859
+ "startedAt": {
860
+ "$ref": "#/$defs/Timestamp"
861
+ },
862
+ "updatedAt": {
863
+ "$ref": "#/$defs/Timestamp"
864
+ }
865
+ }
866
+ },
867
+ "SubscriptionStatusResult": {
868
+ "description": "The user's current subscription standing. `active` answers the gate for the user as a whole; `subscription` is the most relevant record — the current entitling subscription when one exists, otherwise the provider's most recent record as context. It is omitted when the provider has no record for this user. A provider that cannot enumerate the user's records completely MUST fail the operation instead of answering from a partial read.",
869
+ "type": "object",
870
+ "required": [
871
+ "active"
872
+ ],
873
+ "additionalProperties": false,
874
+ "properties": {
875
+ "active": {
876
+ "description": "Whether the user is entitled to anything right now. The gate for simple access checks.",
877
+ "type": "boolean"
878
+ },
879
+ "subscription": {
880
+ "$ref": "#/$defs/SubscriptionStatusSnapshot"
881
+ }
882
+ }
883
+ },
884
+ "EntitlementsInput": {
885
+ "description": "Selects the user whose access decision the caller's backend reads.",
886
+ "type": "object",
887
+ "required": [
888
+ "userId"
889
+ ],
890
+ "additionalProperties": true,
891
+ "properties": {
892
+ "userId": {
893
+ "description": "The opaque app-scoped user identity the caller's backend selected, in the identity space shared by provider and caller.",
894
+ "$ref": "#/$defs/Identifier"
895
+ }
896
+ }
897
+ },
898
+ "EntitlementsResult": {
899
+ "description": "Every product the user may access right now, with the entitling subscription records. Unknown, expired, and ambiguous records contribute nothing: a product appears only when its gate is open at the provider's read time. A provider that cannot enumerate the user's records completely MUST fail the operation instead of answering from a partial read.",
900
+ "type": "object",
901
+ "required": [
902
+ "userId",
903
+ "productIds",
904
+ "subscriptions"
905
+ ],
906
+ "additionalProperties": false,
907
+ "properties": {
908
+ "userId": {
909
+ "$ref": "#/$defs/Identifier"
910
+ },
911
+ "productIds": {
912
+ "description": "Every product the user may access right now, deduplicated.",
913
+ "type": "array",
914
+ "items": {
915
+ "type": "string",
916
+ "minLength": 1
917
+ },
918
+ "uniqueItems": true
919
+ },
920
+ "subscriptions": {
921
+ "description": "The subscription records whose open gates produced `productIds`.",
922
+ "type": "array",
923
+ "items": {
924
+ "$ref": "#/$defs/SubscriptionStatusSnapshot"
925
+ }
926
+ }
927
+ }
928
+ },
929
+ "EraseUserInput": {
930
+ "description": "Selects the user identity to erase from the provider's records.",
931
+ "type": "object",
932
+ "required": [
933
+ "userId"
934
+ ],
935
+ "additionalProperties": true,
936
+ "properties": {
937
+ "userId": {
938
+ "description": "The opaque app-scoped user identity to remove from the provider's subscription records and protocol event identity.",
939
+ "$ref": "#/$defs/Identifier"
940
+ }
941
+ }
942
+ },
943
+ "EraseUserResult": {
944
+ "description": "Acknowledgement of an erasure request. Erasure removes the user identity from the provider's own records and event store; it is asynchronous where the provider processes it as a job, and re-requesting the same user is idempotent — it reports the current job rather than failing. A provider CANNOT unsend events: copies already delivered to the caller's own systems are the caller's responsibility to erase.",
945
+ "type": "object",
946
+ "required": [
947
+ "accepted"
948
+ ],
949
+ "additionalProperties": true,
950
+ "properties": {
951
+ "accepted": {
952
+ "description": "Whether the provider accepted the erasure request.",
953
+ "type": "boolean"
954
+ },
955
+ "jobId": {
956
+ "$ref": "#/$defs/Identifier"
957
+ },
958
+ "status": {
959
+ "description": "The lifecycle state of a user-erasure job. The value space is open; `completed` is the only terminal token this version names.",
960
+ "type": "string",
961
+ "pattern": "^[a-z][a-z_]*$",
962
+ "examples": [
963
+ "queued",
964
+ "running",
965
+ "completed"
966
+ ]
967
+ }
968
+ }
969
+ },
970
+ "CommerceEvent": {
971
+ "title": "OpenIAP Commerce Protocol event",
972
+ "description": "One normalized commerce lifecycle fact. A consumer reads this without knowing which store produced it and without parsing any store-native payload.",
973
+ "type": "object",
974
+ "required": [
975
+ "eventId",
976
+ "eventType",
977
+ "eventVersion",
978
+ "occurredAt",
979
+ "processedAt",
980
+ "store",
981
+ "environment",
982
+ "projectId"
983
+ ],
984
+ "additionalProperties": true,
985
+ "properties": {
986
+ "eventId": {
987
+ "description": "Unique identity of this event, assigned by the emitter. It is the receiver's deduplication key. An emitter MUST NOT reuse an eventId and MUST NOT change the eventId of an event it has already delivered.",
988
+ "$ref": "#/$defs/Identifier"
989
+ },
990
+ "eventType": {
991
+ "description": "What happened. See the taxonomy in SPEC.md. The value space is OPEN: §12 adds an event type in a MINOR version, so a consumer pinned on the major MUST tolerate a type it does not recognise and MUST NOT act on it. The listed values are the ones this version names.",
992
+ "type": "string",
993
+ "pattern": "^[a-z]+\\.[a-z_]+$",
994
+ "examples": [
995
+ "subscription.started",
996
+ "subscription.renewed",
997
+ "subscription.recovered",
998
+ "subscription.entered_grace_period",
999
+ "subscription.entered_billing_retry",
1000
+ "subscription.expired",
1001
+ "subscription.canceled",
1002
+ "subscription.uncanceled",
1003
+ "subscription.revoked",
1004
+ "subscription.refunded",
1005
+ "subscription.product_changed",
1006
+ "subscription.price_changed",
1007
+ "subscription.deferred",
1008
+ "subscription.paused",
1009
+ "subscription.resumed",
1010
+ "entitlement.granted",
1011
+ "entitlement.revoked"
1012
+ ]
1013
+ },
1014
+ "eventVersion": {
1015
+ "description": "Schema version of this body, as MAJOR.MINOR. Consumers pin on the major.",
1016
+ "type": "string",
1017
+ "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)$"
1018
+ },
1019
+ "occurredAt": {
1020
+ "description": "The best authoritative time for the commerce fact. Use the store-asserted transition time when one exists. When polling only reveals that a value changed since the last observation, use the time the emitter observed the new value and do not invent a more precise instant.",
1021
+ "$ref": "#/$defs/Timestamp"
1022
+ },
1023
+ "processedAt": {
1024
+ "description": "When the emitter derived this event. Always greater than or equal to occurredAt in practice, but not guaranteed by this specification.",
1025
+ "$ref": "#/$defs/Timestamp"
1026
+ },
1027
+ "store": {
1028
+ "$ref": "#/$defs/Store"
1029
+ },
1030
+ "environment": {
1031
+ "$ref": "#/$defs/Environment"
1032
+ },
1033
+ "projectId": {
1034
+ "description": "The emitter-side scope this event belongs to, opaque to the consumer. A scope is whatever boundary the emitter organises commerce by — a tenant, a project, an application, or a single constant when the emitter serves exactly one. It is NOT an identifier issued by any central registry, and an implementation MUST NOT be required to obtain one from a third party. The member name is inherited from the deployed 1.0 wire format.",
1035
+ "$ref": "#/$defs/Identifier"
1036
+ },
1037
+ "applicationId": {
1038
+ "description": "A finer-grained scope within `projectId`, when the emitter models one. Opaque to the consumer.",
1039
+ "$ref": "#/$defs/Identifier"
1040
+ },
1041
+ "userId": {
1042
+ "description": "The app user this purchase is bound to, expressed in the identity space shared by the emitter and its consumer. It is an opaque correlation handle: there is no global user directory, and no central identity resolution is implied or required. Absent when no binding exists; required on every entitlement event.",
1043
+ "$ref": "#/$defs/Identifier"
1044
+ },
1045
+ "productId": {
1046
+ "description": "Store product identifier this event concerns. Required on every entitlement event so a consumer knows which access decision to apply.",
1047
+ "type": "string",
1048
+ "minLength": 1
1049
+ },
1050
+ "previousProductId": {
1051
+ "description": "The product that was canonical before this event, when the event applies a product switch.",
1052
+ "type": "string",
1053
+ "minLength": 1
1054
+ },
1055
+ "transactionId": {
1056
+ "description": "Store transaction identity for this specific economic event, where the store exposes one.",
1057
+ "type": "string",
1058
+ "minLength": 1
1059
+ },
1060
+ "originalTransactionId": {
1061
+ "description": "Store identity of the first transaction in this subscription chain, where the store exposes one.",
1062
+ "type": "string",
1063
+ "minLength": 1
1064
+ },
1065
+ "subscription": {
1066
+ "description": "The subscription as it stood immediately after this event. Absent for stores that keep no canonical subscription record.",
1067
+ "type": "object",
1068
+ "required": [
1069
+ "state",
1070
+ "productId",
1071
+ "active"
1072
+ ],
1073
+ "additionalProperties": true,
1074
+ "properties": {
1075
+ "state": {
1076
+ "$ref": "#/$defs/SubscriptionState"
1077
+ },
1078
+ "productId": {
1079
+ "description": "The product the subscription is on after this event. Not always the event's top-level `productId`: where a store defers a product change to the next period — Apple's scheduled downgrades do, Google's item changes do not — the top level names the incoming product while this stays on the outgoing one until it renews. A consumer gating access reads this member; one reporting on what the notification announced reads the top level.",
1080
+ "type": "string",
1081
+ "minLength": 1
1082
+ },
1083
+ "expiresAt": {
1084
+ "description": "When access ends. This is the boundary the entitlement predicate compares against, and the comparison is exclusive — at this instant the subscription is no longer entitled. During a grace period it is the end of the grace window, not of the period that failed to renew; an emitter that reports the failed period here revokes access at the moment grace begins.",
1085
+ "$ref": "#/$defs/Timestamp"
1086
+ },
1087
+ "renewsAt": {
1088
+ "description": "When the store will next attempt to bill, when it reports one. Distinct from `expiresAt`, which is when access ends: they coincide on a healthy subscription and diverge while a grace period extends access past the billing date. Absent once renewal is no longer expected.",
1089
+ "$ref": "#/$defs/Timestamp"
1090
+ },
1091
+ "willRenew": {
1092
+ "description": "Whether the store will attempt another billing period. False after a cancellation, while access may still be live.",
1093
+ "type": "boolean"
1094
+ },
1095
+ "cancellationReason": {
1096
+ "description": "Why the subscription stopped renewing, as a normalized token — not the store's own wording, which the emitter translates from whatever shape that store uses. This version names `UserCanceled`, `BillingError`, `PriceIncreaseDeclined`, `ProductUnavailable`, `Refunded` and `Other`; the space is open, so a consumer MUST tolerate a token it does not know, and no store yields every token. An emitter that has no store-asserted reason omits this member. Unlike `price`, it has no provenance marker, so treat a present value as advisory rather than as a fact to bill or report on.",
1097
+ "type": "string",
1098
+ "minLength": 1
1099
+ },
1100
+ "active": {
1101
+ "description": "The entitlement gate after this event. Where this member is present it is the field to read for access — never a re-derivation from `state`. A store that keeps no canonical subscription record omits the whole `subscription` member; there an `entitlement.granted` or `entitlement.revoked` event carries the decision in its type, while a `subscription.*` event carries none.",
1102
+ "type": "boolean"
1103
+ }
1104
+ }
1105
+ },
1106
+ "price": {
1107
+ "description": "Amount associated with the transaction this event concerns, carried at whatever provenance it has — only `store` means the store asserted it. Always the transaction's own magnitude and never negative. It is context rather than a charge record: stores repeat the same figure across successive notifications about one subscription, so a consumer MUST NOT sum every priced event. See SPEC.md 9.3. Absent means the amount is unknown, never zero.",
1108
+ "$ref": "#/$defs/Money"
1109
+ },
1110
+ "sourceStoreEventId": {
1111
+ "description": "The store's own notification identifier this event was derived from, for support triage against the store console. Absent when the event did not originate from a store notification.",
1112
+ "type": "string",
1113
+ "minLength": 1
1114
+ },
1115
+ "extensions": {
1116
+ "$ref": "#/$defs/Extensions"
1117
+ }
1118
+ },
1119
+ "allOf": [
1120
+ {
1121
+ "if": {
1122
+ "properties": {
1123
+ "eventType": {
1124
+ "enum": [
1125
+ "entitlement.granted",
1126
+ "entitlement.revoked"
1127
+ ]
1128
+ }
1129
+ },
1130
+ "required": [
1131
+ "eventType"
1132
+ ]
1133
+ },
1134
+ "then": {
1135
+ "properties": {
1136
+ "userId": {},
1137
+ "productId": {}
1138
+ },
1139
+ "required": [
1140
+ "userId",
1141
+ "productId"
1142
+ ]
1143
+ }
1144
+ },
1145
+ {
1146
+ "if": {
1147
+ "properties": {
1148
+ "eventType": {
1149
+ "enum": [
1150
+ "entitlement.granted"
1151
+ ]
1152
+ },
1153
+ "subscription": {
1154
+ "type": "object"
1155
+ }
1156
+ },
1157
+ "required": [
1158
+ "eventType",
1159
+ "subscription"
1160
+ ]
1161
+ },
1162
+ "then": {
1163
+ "properties": {
1164
+ "subscription": {
1165
+ "type": "object",
1166
+ "properties": {
1167
+ "active": {
1168
+ "const": true
1169
+ }
1170
+ },
1171
+ "required": [
1172
+ "active"
1173
+ ]
1174
+ }
1175
+ }
1176
+ }
1177
+ },
1178
+ {
1179
+ "if": {
1180
+ "properties": {
1181
+ "eventType": {
1182
+ "enum": [
1183
+ "entitlement.revoked"
1184
+ ]
1185
+ },
1186
+ "subscription": {
1187
+ "type": "object"
1188
+ }
1189
+ },
1190
+ "required": [
1191
+ "eventType",
1192
+ "subscription"
1193
+ ]
1194
+ },
1195
+ "then": {
1196
+ "properties": {
1197
+ "subscription": {
1198
+ "type": "object",
1199
+ "properties": {
1200
+ "active": {
1201
+ "const": false
1202
+ }
1203
+ },
1204
+ "required": [
1205
+ "active"
1206
+ ]
1207
+ }
1208
+ }
1209
+ }
1210
+ },
1211
+ {
1212
+ "if": {
1213
+ "properties": {
1214
+ "eventType": {
1215
+ "enum": [
1216
+ "subscription.started",
1217
+ "subscription.renewed",
1218
+ "subscription.recovered",
1219
+ "subscription.resumed"
1220
+ ]
1221
+ },
1222
+ "subscription": {
1223
+ "type": "object"
1224
+ }
1225
+ },
1226
+ "required": [
1227
+ "eventType",
1228
+ "subscription"
1229
+ ]
1230
+ },
1231
+ "then": {
1232
+ "properties": {
1233
+ "subscription": {
1234
+ "type": "object",
1235
+ "properties": {
1236
+ "state": {
1237
+ "const": "Active"
1238
+ }
1239
+ },
1240
+ "required": [
1241
+ "state"
1242
+ ]
1243
+ }
1244
+ }
1245
+ }
1246
+ },
1247
+ {
1248
+ "if": {
1249
+ "properties": {
1250
+ "eventType": {
1251
+ "enum": [
1252
+ "subscription.entered_grace_period"
1253
+ ]
1254
+ },
1255
+ "subscription": {
1256
+ "type": "object"
1257
+ }
1258
+ },
1259
+ "required": [
1260
+ "eventType",
1261
+ "subscription"
1262
+ ]
1263
+ },
1264
+ "then": {
1265
+ "properties": {
1266
+ "subscription": {
1267
+ "type": "object",
1268
+ "properties": {
1269
+ "state": {
1270
+ "const": "InGracePeriod"
1271
+ }
1272
+ },
1273
+ "required": [
1274
+ "state"
1275
+ ]
1276
+ }
1277
+ }
1278
+ }
1279
+ },
1280
+ {
1281
+ "if": {
1282
+ "properties": {
1283
+ "eventType": {
1284
+ "enum": [
1285
+ "subscription.entered_billing_retry"
1286
+ ]
1287
+ },
1288
+ "subscription": {
1289
+ "type": "object"
1290
+ }
1291
+ },
1292
+ "required": [
1293
+ "eventType",
1294
+ "subscription"
1295
+ ]
1296
+ },
1297
+ "then": {
1298
+ "properties": {
1299
+ "subscription": {
1300
+ "type": "object",
1301
+ "properties": {
1302
+ "state": {
1303
+ "const": "InBillingRetry"
1304
+ },
1305
+ "active": {
1306
+ "const": false
1307
+ }
1308
+ },
1309
+ "required": [
1310
+ "state",
1311
+ "active"
1312
+ ]
1313
+ }
1314
+ }
1315
+ }
1316
+ },
1317
+ {
1318
+ "if": {
1319
+ "properties": {
1320
+ "eventType": {
1321
+ "enum": [
1322
+ "subscription.expired"
1323
+ ]
1324
+ },
1325
+ "subscription": {
1326
+ "type": "object"
1327
+ }
1328
+ },
1329
+ "required": [
1330
+ "eventType",
1331
+ "subscription"
1332
+ ]
1333
+ },
1334
+ "then": {
1335
+ "properties": {
1336
+ "subscription": {
1337
+ "type": "object",
1338
+ "properties": {
1339
+ "state": {
1340
+ "const": "Expired"
1341
+ },
1342
+ "active": {
1343
+ "const": false
1344
+ }
1345
+ },
1346
+ "required": [
1347
+ "state",
1348
+ "active"
1349
+ ]
1350
+ }
1351
+ }
1352
+ }
1353
+ },
1354
+ {
1355
+ "if": {
1356
+ "properties": {
1357
+ "eventType": {
1358
+ "enum": [
1359
+ "subscription.revoked"
1360
+ ]
1361
+ },
1362
+ "subscription": {
1363
+ "type": "object"
1364
+ }
1365
+ },
1366
+ "required": [
1367
+ "eventType",
1368
+ "subscription"
1369
+ ]
1370
+ },
1371
+ "then": {
1372
+ "properties": {
1373
+ "subscription": {
1374
+ "type": "object",
1375
+ "properties": {
1376
+ "state": {
1377
+ "const": "Revoked"
1378
+ },
1379
+ "active": {
1380
+ "const": false
1381
+ }
1382
+ },
1383
+ "required": [
1384
+ "state",
1385
+ "active"
1386
+ ]
1387
+ }
1388
+ }
1389
+ }
1390
+ },
1391
+ {
1392
+ "if": {
1393
+ "properties": {
1394
+ "eventType": {
1395
+ "enum": [
1396
+ "subscription.refunded"
1397
+ ]
1398
+ },
1399
+ "subscription": {
1400
+ "type": "object"
1401
+ }
1402
+ },
1403
+ "required": [
1404
+ "eventType",
1405
+ "subscription"
1406
+ ]
1407
+ },
1408
+ "then": {
1409
+ "properties": {
1410
+ "subscription": {
1411
+ "type": "object",
1412
+ "properties": {
1413
+ "state": {
1414
+ "const": "Refunded"
1415
+ },
1416
+ "active": {
1417
+ "const": false
1418
+ }
1419
+ },
1420
+ "required": [
1421
+ "state",
1422
+ "active"
1423
+ ]
1424
+ }
1425
+ }
1426
+ }
1427
+ },
1428
+ {
1429
+ "if": {
1430
+ "properties": {
1431
+ "eventType": {
1432
+ "enum": [
1433
+ "subscription.paused"
1434
+ ]
1435
+ },
1436
+ "subscription": {
1437
+ "type": "object"
1438
+ }
1439
+ },
1440
+ "required": [
1441
+ "eventType",
1442
+ "subscription"
1443
+ ]
1444
+ },
1445
+ "then": {
1446
+ "properties": {
1447
+ "subscription": {
1448
+ "type": "object",
1449
+ "properties": {
1450
+ "state": {
1451
+ "const": "Paused"
1452
+ },
1453
+ "active": {
1454
+ "const": false
1455
+ }
1456
+ },
1457
+ "required": [
1458
+ "state",
1459
+ "active"
1460
+ ]
1461
+ }
1462
+ }
1463
+ }
1464
+ }
1465
+ ]
1466
+ },
1467
+ "ProviderCapabilities": {
1468
+ "title": "OpenIAP Commerce Protocol implementation descriptor",
1469
+ "description": "What an implementation supports: the specification version it speaks, the event types it can emit, and what it can actually observe per store. This is the document a consumer, an operator, or a tool reads to determine compatibility without guessing and without reading prose. It contains no commerce data.",
1470
+ "type": "object",
1471
+ "required": [
1472
+ "specVersion",
1473
+ "eventTypes",
1474
+ "stores"
1475
+ ],
1476
+ "additionalProperties": true,
1477
+ "properties": {
1478
+ "specVersion": {
1479
+ "description": "OpenIAP Commerce Protocol version this declaration was written against, as MAJOR.MINOR.",
1480
+ "type": "string",
1481
+ "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)$"
1482
+ },
1483
+ "implementation": {
1484
+ "description": "Free-form name and version of the backend making this declaration. Present so an operator can tell two backends apart; it carries no normative meaning.",
1485
+ "type": "object",
1486
+ "additionalProperties": true,
1487
+ "properties": {
1488
+ "name": {
1489
+ "description": "Name of the backend making this declaration, for an operator reading a descriptor without knowing where it came from.",
1490
+ "type": "string",
1491
+ "minLength": 1
1492
+ },
1493
+ "version": {
1494
+ "description": "Version of that backend, when it publishes one. Not a specification version — that is `specVersion`.",
1495
+ "type": "string",
1496
+ "minLength": 1
1497
+ }
1498
+ }
1499
+ },
1500
+ "eventTypes": {
1501
+ "description": "Event types this implementation can emit. A consumer uses it to know which lifecycle signals to expect; absence of a type here means this implementation never produces it, which is different from a type that simply has not occurred yet.",
1502
+ "type": "array",
1503
+ "items": {
1504
+ "type": "string",
1505
+ "pattern": "^[a-z]+\\.[a-z_]+$"
1506
+ },
1507
+ "minItems": 1,
1508
+ "uniqueItems": true
1509
+ },
1510
+ "stores": {
1511
+ "description": "Keyed by store, using the same open value space as the event envelope. An implementation declares only the stores it actually integrates.",
1512
+ "type": "object",
1513
+ "minProperties": 1,
1514
+ "propertyNames": {
1515
+ "pattern": "^[a-z][a-z0-9_]*$"
1516
+ },
1517
+ "additionalProperties": {
1518
+ "$ref": "#/$defs/CapabilitiesStoreCapabilities"
1519
+ }
1520
+ },
1521
+ "profiles": {
1522
+ "description": "Operation profiles this implementation serves, keyed by profile name with the profile version as the value. This version names `verification`, `entitlements`, `events`, and `accountLifecycle`; the key space is open, so a consumer MUST ignore a profile it does not recognise. An implementation MUST declare only profiles it implements and passes conformance for, and MUST NOT declare a profile it partially implements. Absent on a descriptor from an events-only emitter that predates the operation surface.",
1523
+ "type": "object",
1524
+ "minProperties": 1,
1525
+ "propertyNames": {
1526
+ "pattern": "^[a-z][a-zA-Z0-9]*$"
1527
+ },
1528
+ "additionalProperties": {
1529
+ "type": "string",
1530
+ "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)$"
1531
+ }
1532
+ },
1533
+ "bindings": {
1534
+ "description": "Transport bindings this implementation serves for the declared profiles, keyed by binding name with the binding version as the value. This version names `rest` and `graphql`; the key space is open. Declaring a binding means every declared profile operation is reachable over it. An implementation MAY serve one binding only. Absent on a descriptor from an events-only emitter.",
1535
+ "type": "object",
1536
+ "minProperties": 1,
1537
+ "propertyNames": {
1538
+ "pattern": "^[a-z][a-zA-Z0-9]*$"
1539
+ },
1540
+ "additionalProperties": {
1541
+ "type": "string",
1542
+ "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)$"
1543
+ }
1544
+ }
1545
+ }
1546
+ },
1547
+ "StoreEventMapping": {
1548
+ "title": "OpenIAP Commerce Protocol store event mapping",
1549
+ "description": "How each store's own notification vocabulary maps onto the normalized event types. This is the reference an implementer follows to normalize a store notification without reverse-engineering an existing backend, and the reference a consumer reads to understand why a given store produces the events it does. A row states what a notification means, not that an emitter can always act on it: one that lacks the state to interpret a notification — a redelivery it already applied, a purchase whose product it cannot resolve, a token the store has superseded — emits nothing, and that is conformant.",
1550
+ "type": "object",
1551
+ "required": [
1552
+ "specVersion",
1553
+ "stores"
1554
+ ],
1555
+ "additionalProperties": true,
1556
+ "properties": {
1557
+ "specVersion": {
1558
+ "description": "OpenIAP Commerce Protocol version this table was written against, as MAJOR.MINOR.",
1559
+ "type": "string",
1560
+ "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)$"
1561
+ },
1562
+ "stores": {
1563
+ "description": "Keyed by store, using the same open value space as the event envelope. A table lists only the stores it has mappings for.",
1564
+ "type": "object",
1565
+ "minProperties": 1,
1566
+ "propertyNames": {
1567
+ "pattern": "^[a-z][a-z0-9_]*$"
1568
+ },
1569
+ "additionalProperties": {
1570
+ "$ref": "#/$defs/MappingStoreMapping"
1571
+ }
1572
+ }
1573
+ }
1574
+ }
1575
+ }
1576
+ }