@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.
- package/CONVENTION.md +168 -0
- package/DESIGN.md +1056 -0
- package/README.md +227 -5
- package/SPEC.md +1471 -0
- package/conformance/index.d.ts +303 -0
- package/conformance/index.mjs +2126 -0
- package/conformance/mock-provider.mjs +491 -0
- package/examples/entitlement-granted-no-subscription.json +12 -0
- package/examples/entitlement-revoked.json +21 -0
- package/examples/provider-capabilities.json +209 -0
- package/examples/store-event-mapping.json +287 -0
- package/examples/subscription-canceled.json +22 -0
- package/examples/subscription-product-changed.json +30 -0
- package/examples/subscription-renewed.json +29 -0
- package/examples/verify-purchase-request.json +6 -0
- package/examples/verify-purchase-result.json +7 -0
- package/generated/bindings/graphql-operations.json +87 -0
- package/generated/bindings/http-binding.json +143 -0
- package/generated/bindings/introspection-signature.json +320 -0
- package/generated/bindings/operations-sdl.json +4 -0
- package/generated/bindings/operations.graphql +366 -0
- package/generated/commerce-protocol.graphql +1219 -0
- package/generated/openapi/commerce-protocol.openapi.json +1413 -0
- package/generated/schemas/commerce-event.schema.json +499 -0
- package/generated/schemas/commerce-protocol.bundle.schema.json +1576 -0
- package/generated/schemas/operations.schema.json +578 -0
- package/generated/schemas/primitives.schema.json +101 -0
- package/generated/schemas/provider-capabilities.schema.json +205 -0
- package/generated/schemas/store-event-mapping.schema.json +211 -0
- package/generated/vectors/lifecycle.json +908 -0
- package/generated/vectors/operations.json +1122 -0
- package/package.json +62 -12
- package/schema/01-primitives.graphql +102 -0
- package/schema/02-commerce-event.graphql +195 -0
- package/schema/03-provider-capabilities.graphql +139 -0
- package/schema/04-store-event-mapping.graphql +98 -0
- package/schema/05-operations.graphql +461 -0
- package/schema/06-compiler-vocabulary.graphql +139 -0
- package/schema/07-protocol-metadata.graphql +76 -0
- package/src/index.d.ts +63 -0
- package/src/index.mjs +121 -0
- package/vectors/signatures.json +139 -0
package/package.json
CHANGED
|
@@ -1,21 +1,71 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyodotdev/openiap-commerce-protocol",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"types": "./src/index.d.ts",
|
|
6
|
+
"main": "./src/index.mjs",
|
|
7
|
+
"description": "OpenIAP Commerce Protocol: a vendor-neutral server-side contract for purchase verification, entitlements, lifecycle events, and REST and GraphQL bindings",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./src/index.d.ts",
|
|
11
|
+
"default": "./src/index.mjs"
|
|
12
|
+
},
|
|
13
|
+
"./commerce-protocol.graphql": "./generated/commerce-protocol.graphql",
|
|
14
|
+
"./generated/schemas/*": "./generated/schemas/*",
|
|
15
|
+
"./generated/vectors/*": "./generated/vectors/*",
|
|
16
|
+
"./examples/*": "./examples/*",
|
|
17
|
+
"./vectors/*": "./vectors/*",
|
|
18
|
+
"./generated/bindings/*": "./generated/bindings/*",
|
|
19
|
+
"./generated/openapi/*": "./generated/openapi/*",
|
|
20
|
+
"./conformance": {
|
|
21
|
+
"types": "./conformance/index.d.ts",
|
|
22
|
+
"default": "./conformance/index.mjs"
|
|
23
|
+
},
|
|
24
|
+
"./schema/*": "./schema/*"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "node scripts/assemble-schema.mjs && node scripts/build-json-schemas.mjs && node scripts/build-bundle.mjs && node scripts/build-lifecycle-vectors.mjs && node scripts/build-operations.mjs",
|
|
28
|
+
"test": "node scripts/assemble-schema.mjs --check && node scripts/build-json-schemas.mjs --check && node scripts/build-bundle.mjs --check && node scripts/build-lifecycle-vectors.mjs --check && node scripts/build-operations.mjs --check && prettier --check \"**/*.{graphql,json,md,mjs,ts}\" && vitest run test && node scripts/quickstart.mjs",
|
|
29
|
+
"quickstart": "node scripts/quickstart.mjs"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"openiap",
|
|
33
|
+
"iap",
|
|
34
|
+
"in-app-purchase",
|
|
35
|
+
"commerce",
|
|
36
|
+
"webhook",
|
|
37
|
+
"specification"
|
|
38
|
+
],
|
|
6
39
|
"author": "hyodotdev",
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public",
|
|
43
|
+
"registry": "https://registry.npmjs.org/"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"ajv": "^8.17.1",
|
|
47
|
+
"graphql": "^16.11.0",
|
|
48
|
+
"prettier": "^3.6.2",
|
|
49
|
+
"vitest": "^4.1.11"
|
|
50
|
+
},
|
|
51
|
+
"packageManager": "bun@1.3.13",
|
|
52
|
+
"files": [
|
|
53
|
+
"LICENSE",
|
|
54
|
+
"README.md",
|
|
55
|
+
"CONVENTION.md",
|
|
56
|
+
"SPEC.md",
|
|
57
|
+
"DESIGN.md",
|
|
58
|
+
"schema",
|
|
59
|
+
"generated",
|
|
60
|
+
"examples",
|
|
61
|
+
"vectors",
|
|
62
|
+
"src",
|
|
63
|
+
"conformance"
|
|
64
|
+
],
|
|
7
65
|
"repository": {
|
|
8
66
|
"type": "git",
|
|
9
67
|
"url": "git+https://github.com/hyodotdev/openiap.git",
|
|
10
68
|
"directory": "specs/commerce-protocol"
|
|
11
69
|
},
|
|
12
|
-
"homepage": "https://openiap.dev"
|
|
13
|
-
"files": [
|
|
14
|
-
"README.md",
|
|
15
|
-
"LICENSE"
|
|
16
|
-
],
|
|
17
|
-
"publishConfig": {
|
|
18
|
-
"access": "public",
|
|
19
|
-
"registry": "https://registry.npmjs.org/"
|
|
20
|
-
}
|
|
70
|
+
"homepage": "https://openiap.dev/commerce-protocol"
|
|
21
71
|
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# The OpenIAP Commerce Protocol contract, in the GraphQL SDL dialect the
|
|
2
|
+
# compiler in scripts/ understands. This directory is the authored source:
|
|
3
|
+
# each file is one logical layer, assembled in filename order into the
|
|
4
|
+
# published single-file contract commerce-protocol.graphql.
|
|
5
|
+
#
|
|
6
|
+
# This is not a served API. The custom directives retain the JSON wire
|
|
7
|
+
# constraints that standard GraphQL cannot express, and each Query/Mutation
|
|
8
|
+
# field's @operation directive carries its transport binding. The compiler
|
|
9
|
+
# emits the JSON Schemas used by validators, the HTTP binding manifest, the
|
|
10
|
+
# executable GraphQL projection, the OpenAPI document, and the operation
|
|
11
|
+
# conformance vectors.
|
|
12
|
+
#
|
|
13
|
+
# Wire presence is explicit: T! is required and non-null, T is required and
|
|
14
|
+
# nullable, and @optional permits omission without changing nullability.
|
|
15
|
+
# Operation input and result types never combine nullable with @optional:
|
|
16
|
+
# the GraphQL binding cannot express omitted-vs-null, so an omitted member
|
|
17
|
+
# and a selected-but-null member must mean the same thing there.
|
|
18
|
+
|
|
19
|
+
# Published primitives -------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
"""
|
|
22
|
+
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.
|
|
23
|
+
"""
|
|
24
|
+
scalar Store
|
|
25
|
+
@definition(schema: "primitives")
|
|
26
|
+
@jsonString(
|
|
27
|
+
pattern: "^[a-z][a-z0-9_]*$"
|
|
28
|
+
examples: ["apple", "google", "horizon", "amazon"]
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
"""
|
|
32
|
+
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.
|
|
33
|
+
"""
|
|
34
|
+
scalar Environment
|
|
35
|
+
@definition(schema: "primitives")
|
|
36
|
+
@jsonString(minLength: 1, examples: ["production", "sandbox", "xcode"])
|
|
37
|
+
|
|
38
|
+
"""
|
|
39
|
+
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.
|
|
40
|
+
"""
|
|
41
|
+
scalar Timestamp @definition(schema: "primitives") @jsonInteger(minimum: 0)
|
|
42
|
+
|
|
43
|
+
"""
|
|
44
|
+
An opaque, implementation-assigned identifier. Consumers MUST treat it as an opaque string and MUST NOT parse structure out of it.
|
|
45
|
+
"""
|
|
46
|
+
scalar Identifier
|
|
47
|
+
@definition(schema: "primitives")
|
|
48
|
+
@jsonString(minLength: 1, maxLength: 256)
|
|
49
|
+
|
|
50
|
+
"""
|
|
51
|
+
Where a value came from. An implementation MUST NOT present an inferred value as store-authoritative.
|
|
52
|
+
"""
|
|
53
|
+
enum DataProvenance @definition(schema: "primitives") {
|
|
54
|
+
store
|
|
55
|
+
catalog
|
|
56
|
+
inferred
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
"""
|
|
60
|
+
A monetary amount. A missing Money means the amount is unknown; it never means zero.
|
|
61
|
+
"""
|
|
62
|
+
type Money
|
|
63
|
+
@definition(schema: "primitives")
|
|
64
|
+
@jsonObject(additionalProperties: true) {
|
|
65
|
+
"""
|
|
66
|
+
ISO 4217 alphabetic code, uppercase.
|
|
67
|
+
"""
|
|
68
|
+
currency: CurrencyCode!
|
|
69
|
+
|
|
70
|
+
"""
|
|
71
|
+
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.
|
|
72
|
+
"""
|
|
73
|
+
amountMicros: NonNegativeLong!
|
|
74
|
+
|
|
75
|
+
provenance: DataProvenance!
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
"""
|
|
79
|
+
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.
|
|
80
|
+
"""
|
|
81
|
+
enum SubscriptionState @definition(schema: "primitives") {
|
|
82
|
+
Active
|
|
83
|
+
InGracePeriod
|
|
84
|
+
InBillingRetry
|
|
85
|
+
Paused
|
|
86
|
+
Expired
|
|
87
|
+
Revoked
|
|
88
|
+
Refunded
|
|
89
|
+
Unknown
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
"""
|
|
93
|
+
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.
|
|
94
|
+
"""
|
|
95
|
+
scalar Extensions
|
|
96
|
+
@definition(schema: "primitives")
|
|
97
|
+
@jsonMap(
|
|
98
|
+
valueType: "BoundedExtensionValue"
|
|
99
|
+
maxProperties: 24
|
|
100
|
+
keyMinLength: 1
|
|
101
|
+
keyMaxLength: 64
|
|
102
|
+
)
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# Commerce event -------------------------------------------------------------
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
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.
|
|
5
|
+
"""
|
|
6
|
+
scalar CommerceEventType
|
|
7
|
+
@jsonString(
|
|
8
|
+
pattern: "^[a-z]+\\.[a-z_]+$"
|
|
9
|
+
examples: [
|
|
10
|
+
"subscription.started"
|
|
11
|
+
"subscription.renewed"
|
|
12
|
+
"subscription.recovered"
|
|
13
|
+
"subscription.entered_grace_period"
|
|
14
|
+
"subscription.entered_billing_retry"
|
|
15
|
+
"subscription.expired"
|
|
16
|
+
"subscription.canceled"
|
|
17
|
+
"subscription.uncanceled"
|
|
18
|
+
"subscription.revoked"
|
|
19
|
+
"subscription.refunded"
|
|
20
|
+
"subscription.product_changed"
|
|
21
|
+
"subscription.price_changed"
|
|
22
|
+
"subscription.deferred"
|
|
23
|
+
"subscription.paused"
|
|
24
|
+
"subscription.resumed"
|
|
25
|
+
"entitlement.granted"
|
|
26
|
+
"entitlement.revoked"
|
|
27
|
+
]
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
"""
|
|
31
|
+
One normalized commerce lifecycle fact. A consumer reads this without knowing which store produced it and without parsing any store-native payload.
|
|
32
|
+
"""
|
|
33
|
+
type CommerceEvent
|
|
34
|
+
@jsonObject(additionalProperties: true)
|
|
35
|
+
@eventInvariant(
|
|
36
|
+
eventTypes: ["entitlement.granted", "entitlement.revoked"]
|
|
37
|
+
require: ["userId", "productId"]
|
|
38
|
+
)
|
|
39
|
+
@eventInvariant(eventTypes: ["entitlement.granted"], snapshotActive: true)
|
|
40
|
+
@eventInvariant(eventTypes: ["entitlement.revoked"], snapshotActive: false)
|
|
41
|
+
@eventInvariant(
|
|
42
|
+
eventTypes: [
|
|
43
|
+
"subscription.started"
|
|
44
|
+
"subscription.renewed"
|
|
45
|
+
"subscription.recovered"
|
|
46
|
+
"subscription.resumed"
|
|
47
|
+
]
|
|
48
|
+
snapshotState: "Active"
|
|
49
|
+
)
|
|
50
|
+
@eventInvariant(
|
|
51
|
+
eventTypes: ["subscription.entered_grace_period"]
|
|
52
|
+
snapshotState: "InGracePeriod"
|
|
53
|
+
)
|
|
54
|
+
@eventInvariant(
|
|
55
|
+
eventTypes: ["subscription.entered_billing_retry"]
|
|
56
|
+
snapshotState: "InBillingRetry"
|
|
57
|
+
snapshotActive: false
|
|
58
|
+
)
|
|
59
|
+
@eventInvariant(
|
|
60
|
+
eventTypes: ["subscription.expired"]
|
|
61
|
+
snapshotState: "Expired"
|
|
62
|
+
snapshotActive: false
|
|
63
|
+
)
|
|
64
|
+
@eventInvariant(
|
|
65
|
+
eventTypes: ["subscription.revoked"]
|
|
66
|
+
snapshotState: "Revoked"
|
|
67
|
+
snapshotActive: false
|
|
68
|
+
)
|
|
69
|
+
@eventInvariant(
|
|
70
|
+
eventTypes: ["subscription.refunded"]
|
|
71
|
+
snapshotState: "Refunded"
|
|
72
|
+
snapshotActive: false
|
|
73
|
+
)
|
|
74
|
+
@eventInvariant(
|
|
75
|
+
eventTypes: ["subscription.paused"]
|
|
76
|
+
snapshotState: "Paused"
|
|
77
|
+
snapshotActive: false
|
|
78
|
+
) {
|
|
79
|
+
"""
|
|
80
|
+
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.
|
|
81
|
+
"""
|
|
82
|
+
eventId: Identifier!
|
|
83
|
+
|
|
84
|
+
"""
|
|
85
|
+
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.
|
|
86
|
+
"""
|
|
87
|
+
eventType: CommerceEventType!
|
|
88
|
+
|
|
89
|
+
"""
|
|
90
|
+
Schema version of this body, as MAJOR.MINOR. Consumers pin on the major.
|
|
91
|
+
"""
|
|
92
|
+
eventVersion: MajorMinor!
|
|
93
|
+
|
|
94
|
+
"""
|
|
95
|
+
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.
|
|
96
|
+
"""
|
|
97
|
+
occurredAt: Timestamp!
|
|
98
|
+
|
|
99
|
+
"""
|
|
100
|
+
When the emitter derived this event. Always greater than or equal to occurredAt in practice, but not guaranteed by this specification.
|
|
101
|
+
"""
|
|
102
|
+
processedAt: Timestamp!
|
|
103
|
+
|
|
104
|
+
store: Store!
|
|
105
|
+
environment: Environment!
|
|
106
|
+
|
|
107
|
+
"""
|
|
108
|
+
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.
|
|
109
|
+
"""
|
|
110
|
+
projectId: Identifier!
|
|
111
|
+
|
|
112
|
+
"""
|
|
113
|
+
A finer-grained scope within `projectId`, when the emitter models one. Opaque to the consumer.
|
|
114
|
+
"""
|
|
115
|
+
applicationId: Identifier! @optional
|
|
116
|
+
|
|
117
|
+
"""
|
|
118
|
+
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.
|
|
119
|
+
"""
|
|
120
|
+
userId: Identifier! @optional
|
|
121
|
+
|
|
122
|
+
"""
|
|
123
|
+
Store product identifier this event concerns. Required on every entitlement event so a consumer knows which access decision to apply.
|
|
124
|
+
"""
|
|
125
|
+
productId: NonEmptyString! @optional
|
|
126
|
+
|
|
127
|
+
"""
|
|
128
|
+
The product that was canonical before this event, when the event applies a product switch.
|
|
129
|
+
"""
|
|
130
|
+
previousProductId: NonEmptyString! @optional
|
|
131
|
+
|
|
132
|
+
"""
|
|
133
|
+
Store transaction identity for this specific economic event, where the store exposes one.
|
|
134
|
+
"""
|
|
135
|
+
transactionId: NonEmptyString! @optional
|
|
136
|
+
|
|
137
|
+
"""
|
|
138
|
+
Store identity of the first transaction in this subscription chain, where the store exposes one.
|
|
139
|
+
"""
|
|
140
|
+
originalTransactionId: NonEmptyString! @optional
|
|
141
|
+
|
|
142
|
+
"""
|
|
143
|
+
The subscription as it stood immediately after this event. Absent for stores that keep no canonical subscription record.
|
|
144
|
+
"""
|
|
145
|
+
subscription: SubscriptionSnapshot! @optional
|
|
146
|
+
|
|
147
|
+
"""
|
|
148
|
+
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.
|
|
149
|
+
"""
|
|
150
|
+
price: Money! @optional
|
|
151
|
+
|
|
152
|
+
"""
|
|
153
|
+
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.
|
|
154
|
+
"""
|
|
155
|
+
sourceStoreEventId: NonEmptyString! @optional
|
|
156
|
+
|
|
157
|
+
extensions: Extensions! @optional
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
"""
|
|
161
|
+
The subscription as it stood immediately after this event. Absent for stores that keep no canonical subscription record.
|
|
162
|
+
"""
|
|
163
|
+
type SubscriptionSnapshot @inline @jsonObject(additionalProperties: true) {
|
|
164
|
+
state: SubscriptionState!
|
|
165
|
+
|
|
166
|
+
"""
|
|
167
|
+
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.
|
|
168
|
+
"""
|
|
169
|
+
productId: NonEmptyString!
|
|
170
|
+
|
|
171
|
+
"""
|
|
172
|
+
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.
|
|
173
|
+
"""
|
|
174
|
+
expiresAt: Timestamp! @optional
|
|
175
|
+
|
|
176
|
+
"""
|
|
177
|
+
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.
|
|
178
|
+
"""
|
|
179
|
+
renewsAt: Timestamp! @optional
|
|
180
|
+
|
|
181
|
+
"""
|
|
182
|
+
Whether the store will attempt another billing period. False after a cancellation, while access may still be live.
|
|
183
|
+
"""
|
|
184
|
+
willRenew: Boolean! @optional
|
|
185
|
+
|
|
186
|
+
"""
|
|
187
|
+
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.
|
|
188
|
+
"""
|
|
189
|
+
cancellationReason: NonEmptyString! @optional
|
|
190
|
+
|
|
191
|
+
"""
|
|
192
|
+
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.
|
|
193
|
+
"""
|
|
194
|
+
active: Boolean!
|
|
195
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Provider capabilities ------------------------------------------------------
|
|
2
|
+
|
|
3
|
+
scalar StoreCapabilitiesMap
|
|
4
|
+
@jsonMap(
|
|
5
|
+
valueType: "StoreCapabilities"
|
|
6
|
+
minProperties: 1
|
|
7
|
+
keyPattern: "^[a-z][a-z0-9_]*$"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
"""
|
|
11
|
+
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.
|
|
12
|
+
"""
|
|
13
|
+
type ProviderCapabilities @jsonObject(additionalProperties: true) {
|
|
14
|
+
"""
|
|
15
|
+
OpenIAP Commerce Protocol version this declaration was written against, as MAJOR.MINOR.
|
|
16
|
+
"""
|
|
17
|
+
specVersion: MajorMinor!
|
|
18
|
+
|
|
19
|
+
"""
|
|
20
|
+
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.
|
|
21
|
+
"""
|
|
22
|
+
implementation: Implementation! @optional
|
|
23
|
+
|
|
24
|
+
"""
|
|
25
|
+
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.
|
|
26
|
+
"""
|
|
27
|
+
eventTypes: [NamespacedEventType!]! @jsonArray(minItems: 1, uniqueItems: true)
|
|
28
|
+
|
|
29
|
+
"""
|
|
30
|
+
Keyed by store, using the same open value space as the event envelope. An implementation declares only the stores it actually integrates.
|
|
31
|
+
"""
|
|
32
|
+
stores: StoreCapabilitiesMap!
|
|
33
|
+
|
|
34
|
+
"""
|
|
35
|
+
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.
|
|
36
|
+
"""
|
|
37
|
+
profiles: ProfileVersionMap! @optional
|
|
38
|
+
|
|
39
|
+
"""
|
|
40
|
+
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.
|
|
41
|
+
"""
|
|
42
|
+
bindings: BindingVersionMap! @optional
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
scalar ProfileVersionMap
|
|
46
|
+
@jsonMap(
|
|
47
|
+
valueType: "MajorMinor"
|
|
48
|
+
minProperties: 1
|
|
49
|
+
keyPattern: "^[a-z][a-zA-Z0-9]*$"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
scalar BindingVersionMap
|
|
53
|
+
@jsonMap(
|
|
54
|
+
valueType: "MajorMinor"
|
|
55
|
+
minProperties: 1
|
|
56
|
+
keyPattern: "^[a-z][a-zA-Z0-9]*$"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
"""
|
|
60
|
+
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.
|
|
61
|
+
"""
|
|
62
|
+
type Implementation @inline @jsonObject(additionalProperties: true) {
|
|
63
|
+
"""
|
|
64
|
+
Name of the backend making this declaration, for an operator reading a descriptor without knowing where it came from.
|
|
65
|
+
"""
|
|
66
|
+
name: NonEmptyString! @optional
|
|
67
|
+
|
|
68
|
+
"""
|
|
69
|
+
Version of that backend, when it publishes one. Not a specification version — that is `specVersion`.
|
|
70
|
+
"""
|
|
71
|
+
version: NonEmptyString! @optional
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
"""
|
|
75
|
+
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.
|
|
76
|
+
"""
|
|
77
|
+
type StoreCapabilities
|
|
78
|
+
@definition(schema: "provider-capabilities")
|
|
79
|
+
@jsonObject(additionalProperties: true) {
|
|
80
|
+
"""
|
|
81
|
+
Server-side receipt or token validation on demand.
|
|
82
|
+
"""
|
|
83
|
+
initialValidation: Support!
|
|
84
|
+
"""
|
|
85
|
+
The store pushes lifecycle notifications to the backend.
|
|
86
|
+
"""
|
|
87
|
+
serverNotifications: Support!
|
|
88
|
+
"""
|
|
89
|
+
A canonical subscription record can be maintained for this store.
|
|
90
|
+
"""
|
|
91
|
+
subscriptions: Support!
|
|
92
|
+
"""
|
|
93
|
+
Renewal is observable as a distinct lifecycle event.
|
|
94
|
+
"""
|
|
95
|
+
renewalEvents: Support!
|
|
96
|
+
"""
|
|
97
|
+
Refund or revocation is observable as a distinct lifecycle event.
|
|
98
|
+
"""
|
|
99
|
+
refundEvents: Support!
|
|
100
|
+
"""
|
|
101
|
+
Expiration is observable, rather than only inferred from a timestamp having passed.
|
|
102
|
+
"""
|
|
103
|
+
expiration: Support!
|
|
104
|
+
"""
|
|
105
|
+
A scheduled pass re-reads authoritative store state, so a lost notification can self-heal.
|
|
106
|
+
"""
|
|
107
|
+
reconciliation: Support!
|
|
108
|
+
"""
|
|
109
|
+
Entitlement is derivable for access gating.
|
|
110
|
+
"""
|
|
111
|
+
entitlements: Support!
|
|
112
|
+
"""
|
|
113
|
+
The store asserts an amount that can be attributed to revenue with `store` provenance.
|
|
114
|
+
"""
|
|
115
|
+
revenueAmount: Support!
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
"""
|
|
119
|
+
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.
|
|
120
|
+
"""
|
|
121
|
+
type Support
|
|
122
|
+
@definition(schema: "provider-capabilities")
|
|
123
|
+
@jsonObject(additionalProperties: false)
|
|
124
|
+
@supportInvariant {
|
|
125
|
+
"""
|
|
126
|
+
Whether the store's own API offers this at all. A fact about the store, independent of any backend.
|
|
127
|
+
"""
|
|
128
|
+
provider: Boolean!
|
|
129
|
+
|
|
130
|
+
"""
|
|
131
|
+
Whether this backend consumes it. False against a true `provider` is an implementation gap, not a store limitation.
|
|
132
|
+
"""
|
|
133
|
+
implementation: Boolean!
|
|
134
|
+
|
|
135
|
+
"""
|
|
136
|
+
Required whenever `provider` and `implementation` disagree, or whenever either is false. States the concrete limitation in prose a support engineer can act on.
|
|
137
|
+
"""
|
|
138
|
+
notes: NonEmptyString! @optional
|
|
139
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Store event mapping --------------------------------------------------------
|
|
2
|
+
|
|
3
|
+
scalar StoreMappingMap
|
|
4
|
+
@jsonMap(
|
|
5
|
+
valueType: "StoreMapping"
|
|
6
|
+
minProperties: 1
|
|
7
|
+
keyPattern: "^[a-z][a-z0-9_]*$"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
"""
|
|
11
|
+
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.
|
|
12
|
+
"""
|
|
13
|
+
type StoreEventMapping @jsonObject(additionalProperties: true) {
|
|
14
|
+
"""
|
|
15
|
+
OpenIAP Commerce Protocol version this table was written against, as MAJOR.MINOR.
|
|
16
|
+
"""
|
|
17
|
+
specVersion: MajorMinor!
|
|
18
|
+
|
|
19
|
+
"""
|
|
20
|
+
Keyed by store, using the same open value space as the event envelope. A table lists only the stores it has mappings for.
|
|
21
|
+
"""
|
|
22
|
+
stores: StoreMappingMap!
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
"""
|
|
26
|
+
One store's notification lane: the channel it publishes, the rows that translate its notifications, and what an emitter could still derive without one.
|
|
27
|
+
"""
|
|
28
|
+
type StoreMapping
|
|
29
|
+
@definition(schema: "store-event-mapping")
|
|
30
|
+
@jsonObject(additionalProperties: true)
|
|
31
|
+
@storeMappingInvariant {
|
|
32
|
+
"""
|
|
33
|
+
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.
|
|
34
|
+
"""
|
|
35
|
+
notificationChannel: String
|
|
36
|
+
|
|
37
|
+
"""
|
|
38
|
+
Required when there is no notification channel, or when the store's semantics need a caveat an implementer would otherwise get wrong.
|
|
39
|
+
"""
|
|
40
|
+
notes: NonEmptyString! @optional
|
|
41
|
+
|
|
42
|
+
"""
|
|
43
|
+
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.
|
|
44
|
+
"""
|
|
45
|
+
derivableByPolling: [EntitlementEventType!]!
|
|
46
|
+
@optional
|
|
47
|
+
@jsonArray(uniqueItems: true)
|
|
48
|
+
|
|
49
|
+
"""
|
|
50
|
+
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.
|
|
51
|
+
"""
|
|
52
|
+
mappings: [Mapping!]!
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
"""
|
|
56
|
+
One notification-to-event row. Closed: an unrecognised qualifier would leave a reader selecting this row on fewer conditions than its author intended.
|
|
57
|
+
"""
|
|
58
|
+
type Mapping
|
|
59
|
+
@definition(schema: "store-event-mapping")
|
|
60
|
+
@jsonObject(additionalProperties: false)
|
|
61
|
+
@mappingInvariant {
|
|
62
|
+
"""
|
|
63
|
+
The notification type in the store's own vocabulary, verbatim.
|
|
64
|
+
"""
|
|
65
|
+
storeNotification: NonEmptyString!
|
|
66
|
+
|
|
67
|
+
"""
|
|
68
|
+
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.
|
|
69
|
+
"""
|
|
70
|
+
storeNotificationCode: NonEmptyString! @optional
|
|
71
|
+
|
|
72
|
+
"""
|
|
73
|
+
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.
|
|
74
|
+
"""
|
|
75
|
+
storeSubtype: String @optional
|
|
76
|
+
|
|
77
|
+
"""
|
|
78
|
+
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.
|
|
79
|
+
"""
|
|
80
|
+
whenNoPriorStoreEvent: Boolean! @optional @jsonConst(boolean: true)
|
|
81
|
+
|
|
82
|
+
"""
|
|
83
|
+
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.
|
|
84
|
+
"""
|
|
85
|
+
whenPreviousState: [SubscriptionState!]!
|
|
86
|
+
@optional
|
|
87
|
+
@jsonArray(minItems: 1, uniqueItems: true)
|
|
88
|
+
|
|
89
|
+
"""
|
|
90
|
+
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.
|
|
91
|
+
"""
|
|
92
|
+
event: SubscriptionEventType
|
|
93
|
+
|
|
94
|
+
"""
|
|
95
|
+
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.
|
|
96
|
+
"""
|
|
97
|
+
notes: NonEmptyString! @optional
|
|
98
|
+
}
|