@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,461 @@
1
+ # Portable operations --------------------------------------------------------
2
+
3
+ """
4
+ 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.
5
+ """
6
+ scalar ProtocolErrorCode
7
+ @jsonString(
8
+ pattern: "^[A-Z][A-Z0-9_]*$"
9
+ examples: [
10
+ "INVALID_REQUEST"
11
+ "UNAUTHORIZED"
12
+ "FORBIDDEN"
13
+ "NOT_FOUND"
14
+ "PURCHASE_NOT_FOUND"
15
+ "VERIFICATION_FAILED"
16
+ "CONFLICT"
17
+ "RATE_LIMITED"
18
+ "UNSUPPORTED_PROFILE"
19
+ "UNSUPPORTED_STORE"
20
+ "INTERNAL_ERROR"
21
+ ]
22
+ )
23
+
24
+ """
25
+ 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.
26
+ """
27
+ type ProtocolError
28
+ @definition(schema: "operations")
29
+ @jsonObject(additionalProperties: false) {
30
+ code: ProtocolErrorCode!
31
+ """
32
+ Human-readable failure summary. Never carries credentials, store evidence, stack traces, or source paths.
33
+ """
34
+ message: NonEmptyString!
35
+ }
36
+
37
+ """
38
+ 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).
39
+ """
40
+ type ProtocolErrorResponse
41
+ @definition(schema: "operations")
42
+ @jsonObject(additionalProperties: false) {
43
+ error: ProtocolError!
44
+ }
45
+
46
+ """
47
+ 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.
48
+ """
49
+ scalar PurchaseVerificationState
50
+ @jsonString(
51
+ pattern: "^[A-Z][A-Z0-9_]*$"
52
+ examples: [
53
+ "ENTITLED"
54
+ "PENDING_ACKNOWLEDGMENT"
55
+ "PENDING"
56
+ "CANCELED"
57
+ "EXPIRED"
58
+ "READY_TO_CONSUME"
59
+ "CONSUMED"
60
+ "UNKNOWN"
61
+ "INAUTHENTIC"
62
+ ]
63
+ )
64
+
65
+ """
66
+ The lifecycle state of a user-erasure job. The value space is open; `completed` is the only terminal token this version names.
67
+ """
68
+ scalar ErasureJobStatus
69
+ @jsonString(
70
+ pattern: "^[a-z][a-z_]*$"
71
+ examples: ["queued", "running", "completed"]
72
+ )
73
+
74
+ """
75
+ A compact signed transaction JWS. Bounded so an oversized body cannot be forwarded to a store: an Apple transaction JWS is a few kilobytes.
76
+ """
77
+ scalar AppleJws @jsonString(minLength: 1, maxLength: 16384)
78
+
79
+ """
80
+ An opaque store purchase token. Bounded to keep an oversized body off the store API.
81
+ """
82
+ scalar GooglePurchaseToken @jsonString(minLength: 1, maxLength: 4096)
83
+
84
+ """
85
+ A store-issued user identity, as the store's own API returns it. Distinct from the protocol's opaque `userId`.
86
+ """
87
+ scalar StoreUserIdentity @jsonString(minLength: 1, maxLength: 512)
88
+
89
+ """
90
+ A store add-on SKU. Bounded to a predictable identifier length.
91
+ """
92
+ scalar StoreSku @jsonString(minLength: 1, maxLength: 256)
93
+
94
+ """
95
+ A store receipt identifier. Bounded to keep an oversized body off the store API.
96
+ """
97
+ scalar StoreReceiptId @jsonString(minLength: 1, maxLength: 4096)
98
+
99
+ """
100
+ Store evidence for one Apple App Store purchase: the signed StoreKit 2 transaction JWS the app received from the store.
101
+ """
102
+ input AppleEvidence
103
+ @definition(schema: "operations")
104
+ @jsonObject(additionalProperties: true) {
105
+ """
106
+ The compact signed transaction JWS, verbatim as the store handed it to the app.
107
+ """
108
+ jws: AppleJws!
109
+ }
110
+
111
+ """
112
+ Store evidence for one Google Play purchase: the opaque purchase token the store issued to the device.
113
+ """
114
+ input GoogleEvidence
115
+ @definition(schema: "operations")
116
+ @jsonObject(additionalProperties: true) {
117
+ """
118
+ The opaque purchase token, verbatim as the store issued it to the device.
119
+ """
120
+ purchaseToken: GooglePurchaseToken!
121
+ }
122
+
123
+ """
124
+ 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.
125
+ """
126
+ input HorizonEvidence
127
+ @definition(schema: "operations")
128
+ @jsonObject(additionalProperties: true) {
129
+ """
130
+ The store's own user identity for the entitlement holder — not the protocol's opaque `userId`.
131
+ """
132
+ userId: StoreUserIdentity!
133
+ """
134
+ The add-on SKU as configured in the store's developer dashboard.
135
+ """
136
+ sku: StoreSku!
137
+ }
138
+
139
+ """
140
+ 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.
141
+ """
142
+ input AmazonEvidence
143
+ @definition(schema: "operations")
144
+ @jsonObject(additionalProperties: true) {
145
+ """
146
+ The store's own user identity from the purchase response — not the protocol's opaque `userId`.
147
+ """
148
+ userId: StoreUserIdentity!
149
+ """
150
+ The receipt identifier from the purchase or purchase-updates response.
151
+ """
152
+ receiptId: StoreReceiptId!
153
+ """
154
+ Verify against the store's sandbox where the provider permits it. Omitted means production.
155
+ """
156
+ sandbox: Boolean! @optional
157
+ }
158
+
159
+ """
160
+ 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.
161
+ """
162
+ input VerifyPurchaseInput
163
+ @definition(schema: "operations")
164
+ @jsonObject(additionalProperties: true)
165
+ @storeEvidence(store: "apple", member: "apple")
166
+ @storeEvidence(store: "google", member: "google")
167
+ @storeEvidence(store: "horizon", member: "horizon")
168
+ @storeEvidence(store: "amazon", member: "amazon") {
169
+ store: Store!
170
+ apple: AppleEvidence! @optional
171
+ google: GoogleEvidence! @optional
172
+ horizon: HorizonEvidence! @optional
173
+ amazon: AmazonEvidence! @optional
174
+ }
175
+
176
+ """
177
+ 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.
178
+ """
179
+ type VerifyPurchaseResult
180
+ @definition(schema: "operations")
181
+ @jsonObject(additionalProperties: true) {
182
+ store: Store!
183
+ """
184
+ Whether the provider accepts the evidence. The single authoritative acceptance gate.
185
+ """
186
+ isValid: Boolean!
187
+ state: PurchaseVerificationState!
188
+
189
+ """
190
+ The product the store verified, when its response exposes one. Never the caller's claim.
191
+ """
192
+ productId: NonEmptyString! @optional
193
+
194
+ """
195
+ The store environment the provider verified against, using the same open value space as the event envelope.
196
+ """
197
+ environment: Environment! @optional
198
+ }
199
+
200
+ """
201
+ 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.
202
+ """
203
+ input BindPurchaseInput
204
+ @definition(schema: "operations")
205
+ @jsonObject(additionalProperties: true)
206
+ @storeEvidence(store: "apple", member: "apple")
207
+ @storeEvidence(store: "google", member: "google")
208
+ @storeEvidence(store: "horizon", member: "horizon")
209
+ @storeEvidence(store: "amazon", member: "amazon") {
210
+ userId: Identifier!
211
+ store: Store!
212
+ apple: AppleEvidence! @optional
213
+ google: GoogleEvidence! @optional
214
+ horizon: HorizonEvidence! @optional
215
+ amazon: AmazonEvidence! @optional
216
+ }
217
+
218
+ """
219
+ 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`.
220
+ """
221
+ type BindPurchaseResult
222
+ @definition(schema: "operations")
223
+ @jsonObject(additionalProperties: true) {
224
+ """
225
+ True when a binding between this user and the evidenced purchase now exists.
226
+ """
227
+ bound: Boolean!
228
+ }
229
+
230
+ """
231
+ Selects the user whose subscription standing the caller's backend reads.
232
+ """
233
+ input SubscriptionStatusInput
234
+ @definition(schema: "operations")
235
+ @jsonObject(additionalProperties: true) {
236
+ """
237
+ The opaque app-scoped user identity the caller's backend selected, in the identity space shared by provider and caller.
238
+ """
239
+ userId: Identifier!
240
+ }
241
+
242
+ """
243
+ 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.
244
+ """
245
+ type SubscriptionStatusSnapshot
246
+ @definition(schema: "operations")
247
+ @jsonObject(additionalProperties: false) {
248
+ """
249
+ Store product identifier of the subscription.
250
+ """
251
+ productId: NonEmptyString!
252
+ state: SubscriptionState!
253
+
254
+ """
255
+ 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`.
256
+ """
257
+ active: Boolean!
258
+ store: Store! @optional
259
+ expiresAt: Timestamp! @optional
260
+ renewsAt: Timestamp! @optional
261
+ """
262
+ Whether the store will attempt another billing period, when the provider records it.
263
+ """
264
+ willRenew: Boolean! @optional
265
+ """
266
+ Why the subscription stopped renewing, as the same open normalized token space the event envelope uses. Advisory, never a billing fact.
267
+ """
268
+ cancellationReason: NonEmptyString! @optional
269
+ startedAt: Timestamp! @optional
270
+ updatedAt: Timestamp! @optional
271
+ }
272
+
273
+ """
274
+ 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.
275
+ """
276
+ type SubscriptionStatusResult
277
+ @definition(schema: "operations")
278
+ @jsonObject(additionalProperties: false) {
279
+ """
280
+ Whether the user is entitled to anything right now. The gate for simple access checks.
281
+ """
282
+ active: Boolean!
283
+ subscription: SubscriptionStatusSnapshot! @optional
284
+ }
285
+
286
+ """
287
+ Selects the user whose access decision the caller's backend reads.
288
+ """
289
+ input EntitlementsInput
290
+ @definition(schema: "operations")
291
+ @jsonObject(additionalProperties: true) {
292
+ """
293
+ The opaque app-scoped user identity the caller's backend selected, in the identity space shared by provider and caller.
294
+ """
295
+ userId: Identifier!
296
+ }
297
+
298
+ """
299
+ 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.
300
+ """
301
+ type EntitlementsResult
302
+ @definition(schema: "operations")
303
+ @jsonObject(additionalProperties: false) {
304
+ userId: Identifier!
305
+ """
306
+ Every product the user may access right now, deduplicated.
307
+ """
308
+ productIds: [NonEmptyString!]! @jsonArray(uniqueItems: true)
309
+ """
310
+ The subscription records whose open gates produced `productIds`.
311
+ """
312
+ subscriptions: [SubscriptionStatusSnapshot!]!
313
+ }
314
+
315
+ """
316
+ Selects the user identity to erase from the provider's records.
317
+ """
318
+ input EraseUserInput
319
+ @definition(schema: "operations")
320
+ @jsonObject(additionalProperties: true) {
321
+ """
322
+ The opaque app-scoped user identity to remove from the provider's subscription records and protocol event identity.
323
+ """
324
+ userId: Identifier!
325
+ }
326
+
327
+ """
328
+ 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.
329
+ """
330
+ type EraseUserResult
331
+ @definition(schema: "operations")
332
+ @jsonObject(additionalProperties: true) {
333
+ """
334
+ Whether the provider accepted the erasure request.
335
+ """
336
+ accepted: Boolean!
337
+ jobId: Identifier! @optional
338
+ status: ErasureJobStatus! @optional
339
+ }
340
+
341
+ type Query {
342
+ """
343
+ What this provider implements: protocol version, profiles, bindings, and per-store capability. Public and free of commerce data, so a consumer or a conformance runner reads it without credentials and without any central registry.
344
+ """
345
+ providerCapabilities: ProviderCapabilities!
346
+ @operation(
347
+ profile: "core"
348
+ auth: "none"
349
+ method: "GET"
350
+ path: "/commerce/v1/capabilities"
351
+ successStatus: 200
352
+ idempotent: true
353
+ errors: ["RATE_LIMITED", "INTERNAL_ERROR"]
354
+ )
355
+
356
+ """
357
+ A developer backend reads one authenticated user's subscription standing. Server role only: a shipped app must not be able to walk arbitrary user identities, which is why the verification role is refused here.
358
+ """
359
+ subscriptionStatus(
360
+ input: SubscriptionStatusInput!
361
+ ): SubscriptionStatusResult!
362
+ @operation(
363
+ profile: "entitlements"
364
+ auth: "server"
365
+ method: "GET"
366
+ path: "/commerce/v1/subscriptions/status"
367
+ successStatus: 200
368
+ idempotent: true
369
+ errors: [
370
+ "INVALID_REQUEST"
371
+ "UNAUTHORIZED"
372
+ "FORBIDDEN"
373
+ "RATE_LIMITED"
374
+ "INTERNAL_ERROR"
375
+ ]
376
+ )
377
+
378
+ """
379
+ A developer backend reads one authenticated user's access decision. Server role only, and fail-close: anything the provider cannot classify as currently entitled is absent from the answer.
380
+ """
381
+ entitlements(input: EntitlementsInput!): EntitlementsResult!
382
+ @operation(
383
+ profile: "entitlements"
384
+ auth: "server"
385
+ method: "GET"
386
+ path: "/commerce/v1/entitlements"
387
+ successStatus: 200
388
+ idempotent: true
389
+ errors: [
390
+ "INVALID_REQUEST"
391
+ "UNAUTHORIZED"
392
+ "FORBIDDEN"
393
+ "RATE_LIMITED"
394
+ "INTERNAL_ERROR"
395
+ ]
396
+ )
397
+ }
398
+
399
+ type Mutation {
400
+ """
401
+ Verifies store evidence and returns the verdict. The verification role suffices: this operation binds no user, reads no account, and repeating it with the same evidence is safe. VERIFICATION_FAILED means the provider could not obtain a verdict — it never stands in for the store rejecting the evidence, which is a successful result with `isValid: false`.
402
+ """
403
+ verifyPurchase(input: VerifyPurchaseInput!): VerifyPurchaseResult!
404
+ @operation(
405
+ profile: "verification"
406
+ auth: "verification"
407
+ method: "POST"
408
+ path: "/commerce/v1/purchases/verify"
409
+ successStatus: 200
410
+ idempotent: true
411
+ errors: [
412
+ "INVALID_REQUEST"
413
+ "UNAUTHORIZED"
414
+ "UNSUPPORTED_STORE"
415
+ "VERIFICATION_FAILED"
416
+ "RATE_LIMITED"
417
+ "INTERNAL_ERROR"
418
+ ]
419
+ )
420
+
421
+ """
422
+ Binds a verified purchase to the adopter's opaque user identity. Server role only; retrying is idempotent.
423
+ """
424
+ bindPurchase(input: BindPurchaseInput!): BindPurchaseResult!
425
+ @operation(
426
+ profile: "accountLifecycle"
427
+ auth: "server"
428
+ method: "POST"
429
+ path: "/commerce/v1/purchases/bind"
430
+ successStatus: 200
431
+ idempotent: true
432
+ errors: [
433
+ "INVALID_REQUEST"
434
+ "UNAUTHORIZED"
435
+ "FORBIDDEN"
436
+ "UNSUPPORTED_STORE"
437
+ "RATE_LIMITED"
438
+ "INTERNAL_ERROR"
439
+ ]
440
+ )
441
+
442
+ """
443
+ Removes a user identity from the provider's subscription records and protocol event identity. Server role only; re-requesting the same user is idempotent and reports the current job.
444
+ """
445
+ eraseUser(input: EraseUserInput!): EraseUserResult!
446
+ @operation(
447
+ profile: "accountLifecycle"
448
+ auth: "server"
449
+ method: "POST"
450
+ path: "/commerce/v1/users/erase"
451
+ successStatus: 202
452
+ idempotent: true
453
+ errors: [
454
+ "INVALID_REQUEST"
455
+ "UNAUTHORIZED"
456
+ "FORBIDDEN"
457
+ "RATE_LIMITED"
458
+ "INTERNAL_ERROR"
459
+ ]
460
+ )
461
+ }
@@ -0,0 +1,139 @@
1
+ # Compiler vocabulary --------------------------------------------------------
2
+
3
+ scalar MajorMinor @jsonString(pattern: "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)$")
4
+ scalar NonEmptyString @jsonString(minLength: 1)
5
+ scalar CurrencyCode @jsonString(pattern: "^[A-Z]{3}$")
6
+ scalar NamespacedEventType @jsonString(pattern: "^[a-z]+\\.[a-z_]+$")
7
+ scalar SubscriptionEventType @jsonString(pattern: "^subscription\\.[a-z_]+$")
8
+ scalar EntitlementEventType @jsonString(pattern: "^entitlement\\.[a-z_]+$")
9
+ scalar NonNegativeLong @jsonInteger(minimum: 0)
10
+ scalar BoundedExtensionValue @jsonString(maxLength: 512)
11
+
12
+ """
13
+ Names the protocol version and JSON Schema identifier base.
14
+ """
15
+ directive @protocol(version: String!, baseId: String!) on SCHEMA
16
+
17
+ """
18
+ Declares one generated JSON Schema document.
19
+ """
20
+ directive @document(
21
+ name: String!
22
+ title: String!
23
+ description: String!
24
+ root: String
25
+ ) repeatable on SCHEMA
26
+
27
+ """
28
+ Emits this named type under the selected document's $defs.
29
+ """
30
+ directive @definition(schema: String!) on SCALAR | ENUM | OBJECT | INPUT_OBJECT
31
+
32
+ """
33
+ Embeds this object at each field that references it.
34
+ """
35
+ directive @inline on OBJECT | INPUT_OBJECT
36
+
37
+ """
38
+ Permits the wire member to be absent.
39
+ """
40
+ directive @optional on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
41
+
42
+ directive @jsonObject(additionalProperties: Boolean!) on OBJECT | INPUT_OBJECT
43
+
44
+ directive @jsonString(
45
+ pattern: String
46
+ minLength: Int
47
+ maxLength: Int
48
+ examples: [String!]
49
+ ) on SCALAR
50
+
51
+ directive @jsonInteger(minimum: Float, maximum: Float) on SCALAR
52
+
53
+ directive @jsonArray(
54
+ minItems: Int
55
+ maxItems: Int
56
+ uniqueItems: Boolean
57
+ ) on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
58
+
59
+ directive @jsonMap(
60
+ valueType: String!
61
+ minProperties: Int
62
+ maxProperties: Int
63
+ keyPattern: String
64
+ keyMinLength: Int
65
+ keyMaxLength: Int
66
+ ) on SCALAR
67
+
68
+ directive @jsonConst(
69
+ boolean: Boolean!
70
+ ) on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
71
+
72
+ """
73
+ Requires notes unless both support axes are true, and forbids implementation support without provider support.
74
+ """
75
+ directive @supportInvariant on OBJECT
76
+
77
+ """
78
+ Requires an explanation for an empty mapping lane and keeps mappings empty when no notification channel exists.
79
+ """
80
+ directive @storeMappingInvariant on OBJECT
81
+
82
+ """
83
+ Requires notes for a null event and makes the two history conditions mutually exclusive.
84
+ """
85
+ directive @mappingInvariant on OBJECT
86
+
87
+ """
88
+ Applies cross-field requirements to selected event types. Snapshot constraints apply only when the event carries a subscription snapshot.
89
+ """
90
+ directive @eventInvariant(
91
+ eventTypes: [String!]!
92
+ require: [String!]
93
+ snapshotState: String
94
+ snapshotActive: Boolean
95
+ ) repeatable on OBJECT
96
+
97
+ """
98
+ Requires the named evidence member whenever `store` equals the named store.
99
+ """
100
+ directive @storeEvidence(
101
+ store: String!
102
+ member: String!
103
+ ) repeatable on OBJECT | INPUT_OBJECT
104
+
105
+ """
106
+ Declares one operation's transport binding: its profile, auth role, HTTP method and path, success status, idempotency, and the protocol error codes it can return. Required on every Query and Mutation field.
107
+ """
108
+ directive @operation(
109
+ profile: String!
110
+ auth: String!
111
+ method: String!
112
+ path: String!
113
+ successStatus: Int!
114
+ idempotent: Boolean!
115
+ errors: [String!]!
116
+ ) on FIELD_DEFINITION
117
+
118
+ """
119
+ Declares one operation profile: a named, versioned group of obligations a provider implements as a unit.
120
+ """
121
+ directive @profile(
122
+ name: String!
123
+ version: String!
124
+ description: String!
125
+ ) repeatable on SCHEMA
126
+
127
+ """
128
+ Declares one transport binding the specification defines for the operation surface.
129
+ """
130
+ directive @binding(
131
+ name: String!
132
+ version: String!
133
+ description: String!
134
+ ) repeatable on SCHEMA
135
+
136
+ """
137
+ Maps one protocol error code to the HTTP status the REST binding returns for it. Authoritative: the manifest, OpenAPI document, and conformance runner all derive their status table from these.
138
+ """
139
+ directive @errorStatus(code: String!, http: Int!) repeatable on SCHEMA
@@ -0,0 +1,76 @@
1
+ # Protocol metadata ---------------------------------------------------------
2
+
3
+ extend schema
4
+ @protocol(
5
+ version: "1.0"
6
+ baseId: "https://openiap.dev/schemas/commerce-protocol"
7
+ )
8
+ @document(
9
+ name: "primitives"
10
+ title: "OpenIAP Commerce Protocol primitives"
11
+ description: "Shared value types. Every other schema in this specification references these definitions rather than restating them."
12
+ )
13
+ @document(
14
+ name: "commerce-event"
15
+ title: "OpenIAP Commerce Protocol event"
16
+ description: "One normalized commerce lifecycle fact. A consumer reads this without knowing which store produced it and without parsing any store-native payload."
17
+ root: "CommerceEvent"
18
+ )
19
+ @document(
20
+ name: "provider-capabilities"
21
+ title: "OpenIAP Commerce Protocol implementation descriptor"
22
+ 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."
23
+ root: "ProviderCapabilities"
24
+ )
25
+ @document(
26
+ name: "operations"
27
+ title: "OpenIAP Commerce Protocol operations"
28
+ description: "Input and result documents for the portable operation surface, plus the protocol error shape both transport bindings share. Operations travel over the REST and GraphQL bindings; these schemas validate their JSON projections."
29
+ )
30
+ @profile(
31
+ name: "verification"
32
+ version: "1.0"
33
+ description: "Purchase Verification: the verifyPurchase operation over store evidence."
34
+ )
35
+ @profile(
36
+ name: "entitlements"
37
+ version: "1.0"
38
+ description: "Entitlement Access: the subscriptionStatus and entitlements server reads."
39
+ )
40
+ @profile(
41
+ name: "events"
42
+ version: "1.0"
43
+ description: "Event Delivery: the normalized event envelope, lifecycle taxonomy, entitlement deltas, signed webhook transport, retry, idempotency, ordering, and destination-safety rules."
44
+ )
45
+ @profile(
46
+ name: "accountLifecycle"
47
+ version: "1.0"
48
+ description: "Account Lifecycle: the bindPurchase and eraseUser account operations."
49
+ )
50
+ @binding(
51
+ name: "rest"
52
+ version: "1.0"
53
+ description: "HTTP/JSON binding under /commerce/v1, described by the generated HTTP manifest and OpenAPI document."
54
+ )
55
+ @binding(
56
+ name: "graphql"
57
+ version: "1.0"
58
+ description: "Executable GraphQL binding serving the generated schema projection at one HTTP endpoint."
59
+ )
60
+ @errorStatus(code: "INVALID_REQUEST", http: 400)
61
+ @errorStatus(code: "UNAUTHORIZED", http: 401)
62
+ @errorStatus(code: "FORBIDDEN", http: 403)
63
+ @errorStatus(code: "NOT_FOUND", http: 404)
64
+ @errorStatus(code: "PURCHASE_NOT_FOUND", http: 404)
65
+ @errorStatus(code: "CONFLICT", http: 409)
66
+ @errorStatus(code: "UNSUPPORTED_STORE", http: 422)
67
+ @errorStatus(code: "RATE_LIMITED", http: 429)
68
+ @errorStatus(code: "INTERNAL_ERROR", http: 500)
69
+ @errorStatus(code: "UNSUPPORTED_PROFILE", http: 501)
70
+ @errorStatus(code: "VERIFICATION_FAILED", http: 502)
71
+ @document(
72
+ name: "store-event-mapping"
73
+ title: "OpenIAP Commerce Protocol store event mapping"
74
+ 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."
75
+ root: "StoreEventMapping"
76
+ )