@burnt-labs/expo-satya-attest 0.1.0 → 0.2.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/PROVIDER_CONFIG.md +520 -0
- package/PROVIDER_REGISTRY.md +116 -152
- package/PUBLISHING.md +32 -2
- package/README.md +86 -15
- package/android/build.gradle +1 -0
- package/android/src/main/java/expo/modules/satyaattest/SatyaAttestModule.kt +483 -8
- package/android/src/main/java/uniffi/satya_ffi/satya_ffi.kt +1 -1
- package/android/src/main/jniLibs/arm64-v8a/libsatya_ffi.so +0 -0
- package/android/src/main/jniLibs/armeabi-v7a/libsatya_ffi.so +0 -0
- package/android/src/main/jniLibs/x86_64/libsatya_ffi.so +0 -0
- package/app.plugin.js +42 -10
- package/build/SatyaAttest.types.d.ts +120 -18
- package/build/SatyaAttest.types.d.ts.map +1 -1
- package/build/SatyaAttest.types.js.map +1 -1
- package/build/SatyaAttestModule.d.ts +15 -0
- package/build/SatyaAttestModule.d.ts.map +1 -1
- package/build/SatyaAttestModule.js.map +1 -1
- package/build/SatyaProviderAttestButton.d.ts +5 -1
- package/build/SatyaProviderAttestButton.d.ts.map +1 -1
- package/build/SatyaProviderAttestButton.js +722 -109
- package/build/SatyaProviderAttestButton.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -1
- package/build/index.js.map +1 -1
- package/build/providerPolicy.d.ts +2 -1
- package/build/providerPolicy.d.ts.map +1 -1
- package/build/providerPolicy.js +33 -4
- package/build/providerPolicy.js.map +1 -1
- package/build/providerRegistry.d.ts +2 -1
- package/build/providerRegistry.d.ts.map +1 -1
- package/build/providerRegistry.js +585 -64
- package/build/providerRegistry.js.map +1 -1
- package/build/providerWebview.d.ts +59 -2
- package/build/providerWebview.d.ts.map +1 -1
- package/build/providerWebview.js +559 -8
- package/build/providerWebview.js.map +1 -1
- package/build/providers.json +2773 -905
- package/ios/Frameworks/libsatya_ffi-rs.xcframework/ios-arm64/Headers/satya_ffi.swift +2 -2
- package/ios/Frameworks/libsatya_ffi-rs.xcframework/ios-arm64/libsatya_ffi.a +0 -0
- package/ios/Generated/satya_ffi.swift +2 -2
- package/ios/SatyaAttestModule.swift +179 -9
- package/package.json +4 -2
- package/src/SatyaAttest.types.ts +125 -18
- package/src/SatyaAttestModule.ts +15 -0
- package/src/SatyaProviderAttestButton.tsx +840 -116
- package/src/index.ts +2 -0
- package/src/providerPolicy.ts +32 -3
- package/src/providerRegistry.ts +724 -87
- package/src/providerWebview.ts +603 -10
- package/src/providers.json +2773 -905
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
# Provider Config
|
|
2
|
+
|
|
3
|
+
Provider configs are trusted policy for the Expo native replay flow. A config decides
|
|
4
|
+
which provider URL can be replayed, which WebView request counts as a match, which
|
|
5
|
+
request headers may be copied into native replay, which response fields can be
|
|
6
|
+
revealed, and which template/parser identities the verifier accepts.
|
|
7
|
+
|
|
8
|
+
The built-in registry lives at [`src/providers.json`](./src/providers.json). It is a
|
|
9
|
+
vendored copy of the centralized provider registry maintained in the
|
|
10
|
+
[`burnt-labs/provider-bank`](https://github.com/burnt-labs/provider-bank) repo at
|
|
11
|
+
`provider-bank/providers.json` — edit providers there and sync the copy here
|
|
12
|
+
byte-identically (then run `npm run test:providers`). Custom registries use the same
|
|
13
|
+
provider shape inside a registry wrapper:
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"schemaVersion": "1.0.0",
|
|
18
|
+
"providers": []
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The registry-level `schemaVersion` is optional in the SDK loader. Each provider entry
|
|
23
|
+
must still carry its own `schemaVersion`.
|
|
24
|
+
|
|
25
|
+
## Minimal Provider
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"schemaVersion": "1.0.0",
|
|
30
|
+
"providerId": "acme.current_user.v1",
|
|
31
|
+
"name": "Acme",
|
|
32
|
+
"iconUrl": "https://acme.example/icon.png",
|
|
33
|
+
"login": {
|
|
34
|
+
"url": "https://acme.example/login"
|
|
35
|
+
},
|
|
36
|
+
"endpoints": [
|
|
37
|
+
{
|
|
38
|
+
"id": "primary",
|
|
39
|
+
"url": "https://acme.example/api/current-user",
|
|
40
|
+
"method": "GET",
|
|
41
|
+
"headers": ["accept", "cookie", "user-agent"],
|
|
42
|
+
"responseType": "json",
|
|
43
|
+
"requestMatchRules": [
|
|
44
|
+
{ "type": "methodEquals", "pattern": "GET" },
|
|
45
|
+
{ "type": "urlContains", "pattern": "/api/current-user" }
|
|
46
|
+
],
|
|
47
|
+
"revealRules": [
|
|
48
|
+
{ "action": "REVEAL", "direction": "SENT", "part": "METHOD" },
|
|
49
|
+
{ "action": "REVEAL", "direction": "SENT", "part": "REQUEST_TARGET" },
|
|
50
|
+
{
|
|
51
|
+
"action": "REVEAL",
|
|
52
|
+
"direction": "RECV",
|
|
53
|
+
"part": "BODY",
|
|
54
|
+
"params": {
|
|
55
|
+
"type": "json",
|
|
56
|
+
"path": "$.accountHolder.name",
|
|
57
|
+
"alias": "accountHolderName"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
],
|
|
63
|
+
"tlsIdentityPolicy": {
|
|
64
|
+
"expectedSni": "acme.example",
|
|
65
|
+
"spkiSha256B64Pins": ["AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="]
|
|
66
|
+
},
|
|
67
|
+
"templateGovernance": {
|
|
68
|
+
"templateHash": "dev-acme-current-user-v1-template",
|
|
69
|
+
"parserHash": "parser-sha256:0000000000000000000000000000000000000000000000000000000000000000"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Replace the example pin and parser hash with real values. `npm run test:providers`
|
|
75
|
+
computes the expected parser hash and fails if the value is stale.
|
|
76
|
+
|
|
77
|
+
## Top-Level Fields
|
|
78
|
+
|
|
79
|
+
Required provider fields:
|
|
80
|
+
|
|
81
|
+
| Field | Description |
|
|
82
|
+
|---|---|
|
|
83
|
+
| `schemaVersion` | Provider schema version string. The built-in registry currently uses `1.0.0`. |
|
|
84
|
+
| `providerId` | Stable provider id, for example `kaggle.current_user.v1`. It must be unique within a registry. |
|
|
85
|
+
| `name` | User-facing provider name. Legacy `label` and `displayName` are accepted, but new configs should use `name`. |
|
|
86
|
+
| `login.url` | HTTPS page opened in the managed WebView for login and request discovery. |
|
|
87
|
+
| `endpoints` | Non-empty array of proof endpoints. If no endpoint id is selected, the SDK uses the first endpoint. |
|
|
88
|
+
| `tlsIdentityPolicy.expectedSni` | Expected HTTPS host/SNI. Every endpoint URL must use this same host. |
|
|
89
|
+
| `tlsIdentityPolicy.spkiSha256B64Pins` | Public SPKI SHA-256 pins for native replay. Built-in providers and device E2E should keep this non-empty. |
|
|
90
|
+
| `templateGovernance.templateHash` | Template identity accepted by the verifier. |
|
|
91
|
+
| `templateGovernance.parserHash` | Parser identity derived from provider id, endpoint ids, response types, and reveal rules. |
|
|
92
|
+
|
|
93
|
+
Optional provider fields:
|
|
94
|
+
|
|
95
|
+
| Field | Description |
|
|
96
|
+
|---|---|
|
|
97
|
+
| `iconUrl` | HTTPS image URL for provider selectors. |
|
|
98
|
+
| `description` | Optional selector/help text. |
|
|
99
|
+
| `label`, `displayName` | Legacy display fields. The loader normalizes them to `name`/`label`/`displayName`. |
|
|
100
|
+
| `version` | Optional version string. If omitted, the SDK infers it from the final `.vN` segment of `providerId`. |
|
|
101
|
+
| `auth` | Optional object for provider-auth metadata. The current SDK preserves it but does not interpret it in the managed flow. |
|
|
102
|
+
| `injection` | Optional JavaScript string, array of strings, or `null`. Runs in the managed WebView on every main and popup page alongside the SDK interceptor. |
|
|
103
|
+
| `target` | Legacy compatibility object. If present, it must be `{ "scheme": "https", "host": expectedSni, "port": 443 }`. The loader generates this when omitted. |
|
|
104
|
+
| `webLogin` | Legacy compatibility object. `login.url` is preferred. If only `webLogin.startUrl` is present, the loader maps it to `login.url`. |
|
|
105
|
+
| `userAgent` | Optional `{ "ios": string | null, "android": string | null }`. Used by the managed WebView when no explicit `webViewUserAgent` prop is supplied. |
|
|
106
|
+
| `tlsIdentityPolicy.allowedAlpn` | Optional ALPN metadata. Native replay measures negotiated ALPN and verifier policy checks the artifact. |
|
|
107
|
+
| `tlsIdentityPolicy.spkiSha256Hex` | Legacy/pass-through metadata. Prefer `spkiSha256B64Pins`. |
|
|
108
|
+
| `tlsIdentityPolicy.tlsTrustStorePolicy` | Optional, but if present it must be `web-pki-os-roots-no-user-roots`. The loader fills this value when omitted. |
|
|
109
|
+
| `tlsIdentityPolicy.userRootsAllowed` | Optional, but if present it must be `false`. The loader fills `false` when omitted. |
|
|
110
|
+
| `tlsIdentityPolicy.redirectPolicyId` | Optional redirect-policy metadata. Native replay currently blocks redirects. |
|
|
111
|
+
| `templateGovernance.fixtureSetHash` | Optional fixture-set identity. Defaults to `unversioned-fixtures`. |
|
|
112
|
+
| `templateGovernance.governancePolicyVersion` | Optional governance policy id. Defaults to `embedded-registry`. |
|
|
113
|
+
| `templateGovernance.providerActivationState` | Optional activation state. Defaults to `experimental`; production policy should require governed active templates. |
|
|
114
|
+
|
|
115
|
+
Unknown top-level keys are cloned through the JavaScript loader, but native replay and
|
|
116
|
+
backend verification only rely on documented policy fields. Do not place
|
|
117
|
+
security-critical behavior in an unknown key.
|
|
118
|
+
|
|
119
|
+
## Endpoints
|
|
120
|
+
|
|
121
|
+
Each provider can define multiple endpoints for the same site:
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{
|
|
125
|
+
"id": "primary",
|
|
126
|
+
"url": "https://acme.example/api/current-user",
|
|
127
|
+
"method": "GET",
|
|
128
|
+
"headers": ["accept", "cookie", "user-agent"],
|
|
129
|
+
"responseType": "json",
|
|
130
|
+
"requestMatchRules": [],
|
|
131
|
+
"revealRules": []
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Required endpoint fields:
|
|
136
|
+
|
|
137
|
+
| Field | Description |
|
|
138
|
+
|---|---|
|
|
139
|
+
| `id` | Endpoint id. It must start with a letter and contain only letters, numbers, `_`, or `-`. |
|
|
140
|
+
| `url` | HTTPS replay URL. The host must match `tlsIdentityPolicy.expectedSni`. |
|
|
141
|
+
| `method` | HTTP method. Supported values are `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, and `OPTIONS`. |
|
|
142
|
+
| `headers` | Non-empty request-header allowlist used by native replay. This is the collapsed replacement for `privacyPolicy.allowedRequestHeaders`. |
|
|
143
|
+
| `responseType` | Response parser type. One of `json`, `html`, or `text`. `html`/`text` endpoints disclose values via `regex` reveals (see below). |
|
|
144
|
+
| `requestMatchRules` | Non-empty array of rules used to detect the WebView request before native replay. |
|
|
145
|
+
| `revealRules` | Non-empty array of disclosure rules. It must include at least one `RECV BODY` reveal (`json`, `regex`, or `jsonRegex`). |
|
|
146
|
+
|
|
147
|
+
The raw endpoint schema intentionally does not include `allowedResponseBodyJsonPaths`.
|
|
148
|
+
The loader derives the response reveal allowlist from `revealRules` and emits a
|
|
149
|
+
structured `privacyPolicy.allowedResponseReveals` array (consumed by native replay) plus
|
|
150
|
+
the legacy `allowedResponseBodyJsonPaths` (the JSON-path subset).
|
|
151
|
+
|
|
152
|
+
### Response types & HTML/text capture
|
|
153
|
+
|
|
154
|
+
- `json` — the response body is parsed as JSON; reveals may use `json`, `jsonRegex`, or `regex`.
|
|
155
|
+
- `html` / `text` — the body is treated as raw text; reveals must use `regex` only (a
|
|
156
|
+
JSON path cannot be applied to a non-JSON body).
|
|
157
|
+
|
|
158
|
+
The WebView request interceptor only sees `fetch`/`XHR` traffic, so a `json` endpoint is
|
|
159
|
+
normally captured when the logged-in page makes its own API call. For `html`/`text`
|
|
160
|
+
endpoints the proof page is a top-level navigation, which is not a `fetch`/`XHR`. When the
|
|
161
|
+
managed WebView lands on a URL that matches an `html`/`text` provider's
|
|
162
|
+
`requestMatchRules`, the SDK re-issues that URL as a same-origin credentialed `fetch()` so
|
|
163
|
+
the normal capture → native-replay path runs. A provider `injection` can drive the WebView
|
|
164
|
+
to the proof page after login (the bundled HTML providers do this).
|
|
165
|
+
|
|
166
|
+
When `requireProviderTemplate(providerId, registry, endpointId)` is called with an
|
|
167
|
+
endpoint id, that endpoint is normalized into the compatibility `proofTarget` used by
|
|
168
|
+
the native modules. If no endpoint id is supplied, the first endpoint is selected.
|
|
169
|
+
|
|
170
|
+
### Active-fetch trigger (`triggerFromInjection`)
|
|
171
|
+
|
|
172
|
+
Some endpoints are cookie-authenticated APIs that **no page ever calls on its own** (e.g.
|
|
173
|
+
`reddit.com/api/me.json`, `spotify.com/api/account-settings/v1/profile`,
|
|
174
|
+
`users.roblox.com/v1/users/authenticated`). There is nothing for the interceptor to
|
|
175
|
+
capture. Set `triggerFromInjection: true` and the SDK actively fires the request itself
|
|
176
|
+
from inside the WebView (with cookies) once the user is logged in:
|
|
177
|
+
|
|
178
|
+
```json
|
|
179
|
+
{
|
|
180
|
+
"id": "primary",
|
|
181
|
+
"url": "https://www.reddit.com/api/me.json?raw_json=1",
|
|
182
|
+
"method": "GET",
|
|
183
|
+
"headers": ["accept", "user-agent", "cookie"],
|
|
184
|
+
"responseType": "json",
|
|
185
|
+
"triggerFromInjection": true,
|
|
186
|
+
"requestMatchRules": [
|
|
187
|
+
{ "type": "methodEquals", "pattern": "GET" },
|
|
188
|
+
{ "type": "urlContains", "pattern": "/api/me.json" }
|
|
189
|
+
],
|
|
190
|
+
"revealRules": [ /* … */ ]
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
How it works:
|
|
195
|
+
|
|
196
|
+
- The SDK injects a small poller that calls `fetch(url, { method, headers, body, credentials: 'include' })`
|
|
197
|
+
every ~2s, **skipping login/auth pages**, until it succeeds.
|
|
198
|
+
- **Login gating is automatic via the response status:** the native replay only runs once
|
|
199
|
+
the injected request comes back **2xx**. Pre-login `401`s / redirects are ignored, so it
|
|
200
|
+
never captures before sign-in. (Regular, page-triggered endpoints are unaffected — they
|
|
201
|
+
still capture on the request.)
|
|
202
|
+
- Self-terminating: stops on the first 2xx, after `injectionMaxAttempts` real fetches, or a
|
|
203
|
+
hard poll cap.
|
|
204
|
+
|
|
205
|
+
Optional fields that pair with it:
|
|
206
|
+
|
|
207
|
+
| Field | Description |
|
|
208
|
+
|---|---|
|
|
209
|
+
| `triggerFromInjection` | `true` to enable the active fetch for this endpoint. |
|
|
210
|
+
| `injectionHeaders` | `{ name: value }` extra headers for the fetch. Values may be literals or `{{cookie:NAME}}` templates resolved from `document.cookie` at fire time (for non-httpOnly CSRF / app-id headers). Their names are auto-added to the request-header allowlist for native replay. |
|
|
211
|
+
| `requestBody` | Request body for POST/PUT/PATCH (e.g. a GraphQL query). |
|
|
212
|
+
| `injectionLandingUrl` | HTTPS page to navigate to if the fetch is stuck on a **sibling subdomain** of the proof host (so it becomes same-origin). Defaults to the proof origin root; set a stable signed-in page when the root redirects elsewhere. E.g. after login Spotify lands on `open.spotify.com`, so its endpoint sets `"https://www.spotify.com/account/"`. The poller tries the fetch from wherever it is first (so credentialed cross-origin CORS APIs like Roblox's still need no landing), and only hops after a couple of failures on a same-registrable-domain host — never across registrable domains (won't break a third-party SSO). |
|
|
213
|
+
| `injectionPollMs` | Poll interval in ms (default `2000`, range `500`–`15000`). |
|
|
214
|
+
| `injectionMaxAttempts` | Max real fetch attempts (default `60`, range `1`–`300`). |
|
|
215
|
+
|
|
216
|
+
Examples needing headers / a body:
|
|
217
|
+
|
|
218
|
+
```jsonc
|
|
219
|
+
// Instagram — constant app-id + a CSRF token read from a cookie:
|
|
220
|
+
"triggerFromInjection": true,
|
|
221
|
+
"injectionHeaders": {
|
|
222
|
+
"x-ig-app-id": "936619743392459",
|
|
223
|
+
"x-csrftoken": "{{cookie:csrftoken}}",
|
|
224
|
+
"x-requested-with": "XMLHttpRequest"
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// DoorDash — POST GraphQL with a body:
|
|
228
|
+
"triggerFromInjection": true,
|
|
229
|
+
"injectionHeaders": { "content-type": "application/json" },
|
|
230
|
+
"requestBody": "{\"operationName\":\"consumer\",\"variables\":{},\"query\":\"query consumer { consumer { id isGuest } }\"}"
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Notes:
|
|
234
|
+
|
|
235
|
+
- Use it for APIs the page doesn't call. For endpoints a page already triggers (the proof
|
|
236
|
+
fires naturally), leave it off — both work, but the flag is redundant there.
|
|
237
|
+
- Cross-origin APIs (e.g. `api.skillshare.com` from `www.skillshare.com`) work only if the
|
|
238
|
+
API allows credentialed CORS from the page's origin (the provider's own web app must use
|
|
239
|
+
the same call). Same-origin APIs always work.
|
|
240
|
+
- `httpOnly` cookies can't be read by `{{cookie:NAME}}` (they're sent automatically by
|
|
241
|
+
`credentials:'include'` anyway; only header tokens like CSRF need templating).
|
|
242
|
+
- `triggerFromInjection`, `injectionHeaders`, `requestBody`, and the poll tuning are **not**
|
|
243
|
+
part of the parser hash.
|
|
244
|
+
|
|
245
|
+
### Dynamic URLs (`replayCapturedUrl`)
|
|
246
|
+
|
|
247
|
+
Some endpoints have a **dynamic path segment** — e.g. Garmin's
|
|
248
|
+
`/userprofile-service/socialProfile/{userId}`, where the user id varies per account. A fixed
|
|
249
|
+
template `url` can't represent that, and native replay normally uses the template `url`.
|
|
250
|
+
|
|
251
|
+
Set `"replayCapturedUrl": true` on the endpoint and native replay uses the URL the WebView
|
|
252
|
+
**actually fetched** (the one matched by `requestMatchRules`) instead of the template `url`.
|
|
253
|
+
The captured URL is still constrained to the pinned host (`expectedSni`) and the endpoint's
|
|
254
|
+
match rules, so it can't be redirected to an arbitrary origin. Configure it like:
|
|
255
|
+
|
|
256
|
+
```json
|
|
257
|
+
{
|
|
258
|
+
"url": "https://connect.garmin.com/gc-api/userprofile-service/socialProfile",
|
|
259
|
+
"method": "GET",
|
|
260
|
+
"replayCapturedUrl": true,
|
|
261
|
+
"requestMatchRules": [
|
|
262
|
+
{ "type": "methodEquals", "pattern": "GET" },
|
|
263
|
+
{ "type": "urlContains", "pattern": "/userprofile-service/socialProfile" }
|
|
264
|
+
]
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Here the template `url` is just the stable prefix (used for host/SNI validation and the
|
|
269
|
+
`urlContains` rule); the per-user `…/socialProfile/{userId}` request the page fires is what
|
|
270
|
+
gets captured and replayed. `replayCapturedUrl` is not part of the parser hash.
|
|
271
|
+
|
|
272
|
+
## Request Match Rules
|
|
273
|
+
|
|
274
|
+
Request match rules decide which WebView request should trigger native replay. Every
|
|
275
|
+
endpoint must include a method rule and a URL rule:
|
|
276
|
+
|
|
277
|
+
```json
|
|
278
|
+
[
|
|
279
|
+
{ "type": "methodEquals", "pattern": "POST" },
|
|
280
|
+
{ "type": "urlContains", "pattern": "/api/current-user" }
|
|
281
|
+
]
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Supported rule types:
|
|
285
|
+
|
|
286
|
+
| Rule | Fields | Notes |
|
|
287
|
+
|---|---|---|
|
|
288
|
+
| `methodEquals` | `pattern` | Required. Must match endpoint `method`. |
|
|
289
|
+
| `urlContains` | `pattern` | Required by the current validator. Pattern must be at least four characters and must match the endpoint path or query. |
|
|
290
|
+
| `urlRegex` | `pattern` | Optional. The regex must compile and match either the full endpoint URL or its path/query. Use only when a simple path substring is not enough. |
|
|
291
|
+
| `headerEquals` | `key`, `pattern` | Optional. Supported by the matcher, but avoid it for discovery unless required because WebView-captured auth headers can be inconsistent. `pattern: "*"` means present and non-empty. |
|
|
292
|
+
| `bodyJsonPathEquals` | `path`, `pattern` | Optional. Useful for GraphQL or shared API paths; for example matching `$.operationName`. |
|
|
293
|
+
|
|
294
|
+
Bundled providers intentionally keep matching reachable: most use `methodEquals` plus
|
|
295
|
+
`urlContains`; Uber additionally uses `bodyJsonPathEquals` to distinguish the GraphQL
|
|
296
|
+
operation.
|
|
297
|
+
|
|
298
|
+
## Reveal Rules
|
|
299
|
+
|
|
300
|
+
Reveal rules describe what the artifact may disclose. A `RECV BODY` reveal has a
|
|
301
|
+
`params.type` of `json`, `regex`, or `jsonRegex`.
|
|
302
|
+
|
|
303
|
+
**`json`** — read the value at a JSON path from a JSON body:
|
|
304
|
+
|
|
305
|
+
```json
|
|
306
|
+
{
|
|
307
|
+
"action": "REVEAL",
|
|
308
|
+
"direction": "RECV",
|
|
309
|
+
"part": "BODY",
|
|
310
|
+
"params": { "type": "json", "path": "$.accountHolder.name", "alias": "accountHolderName" }
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
**`regex`** — match a regex against the raw response body (HTML, text, or JSON) and
|
|
315
|
+
disclose a capture group. Requires an explicit `alias`:
|
|
316
|
+
|
|
317
|
+
```json
|
|
318
|
+
{
|
|
319
|
+
"action": "REVEAL",
|
|
320
|
+
"direction": "RECV",
|
|
321
|
+
"part": "BODY",
|
|
322
|
+
"params": { "type": "regex", "pattern": "\"memberSince\":\"([^\"]+)\"", "group": 1, "alias": "member_since" }
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
**`jsonRegex`** — read a string value at a JSON path, then match a regex against it.
|
|
327
|
+
Useful for pulling a substring out of one JSON field. Requires an explicit `alias`:
|
|
328
|
+
|
|
329
|
+
```json
|
|
330
|
+
{
|
|
331
|
+
"action": "REVEAL",
|
|
332
|
+
"direction": "RECV",
|
|
333
|
+
"part": "BODY",
|
|
334
|
+
"params": { "type": "jsonRegex", "path": "$.profile.bio", "pattern": "id:(\\w+)", "alias": "bio_id" }
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Supported disclosure fields:
|
|
339
|
+
|
|
340
|
+
| Field | Description |
|
|
341
|
+
|---|---|
|
|
342
|
+
| `action` | Use `REVEAL` for disclosed values. |
|
|
343
|
+
| `direction` | `SENT` for request metadata or `RECV` for response data. |
|
|
344
|
+
| `part` | Common values are `METHOD`, `REQUEST_TARGET`, and `BODY`. |
|
|
345
|
+
| `params.type` | `json`, `regex`, or `jsonRegex`. |
|
|
346
|
+
| `params.path` | JSON path for `json`/`jsonRegex` reveals, e.g. `$.userProfile.userName` or `$.transactions[0].id`. |
|
|
347
|
+
| `params.pattern` | Regex for `regex`/`jsonRegex` reveals. Must compile and be ≤ 2048 chars. |
|
|
348
|
+
| `params.group` | Optional capture group for `regex`/`jsonRegex`: an integer index in `[0, 99]` (default `1`, falling back to the full match when the pattern has no groups) or a named group string `(?<name>...)`. |
|
|
349
|
+
| `params.alias` | Claim name. Optional for `json` (defaults to the final JSON-path key); **required** for `regex` and `jsonRegex`. |
|
|
350
|
+
|
|
351
|
+
Alias rules:
|
|
352
|
+
|
|
353
|
+
- aliases must start with a letter,
|
|
354
|
+
- aliases can contain letters, numbers, `_`, or `-`,
|
|
355
|
+
- aliases are limited to 80 characters,
|
|
356
|
+
- duplicate aliases are rejected case-insensitively across all reveal types.
|
|
357
|
+
|
|
358
|
+
Native replay (Android/iOS) applies these reveals to the pinned response and emits each
|
|
359
|
+
match as a disclosed claim. The raw response body is size-bounded (8 MiB) before regex
|
|
360
|
+
runs. Regex patterns come from the trusted provider registry, never from page content.
|
|
361
|
+
|
|
362
|
+
## Writing Regex Patterns For HTML / Text Bodies
|
|
363
|
+
|
|
364
|
+
`regex` reveals run against the **raw response body** (the exact bytes the pinned replay
|
|
365
|
+
received — HTML, text, or JSON). Use this recipe to keep patterns short and readable.
|
|
366
|
+
|
|
367
|
+
### The recipe: anchor, lazy capture, anchor
|
|
368
|
+
|
|
369
|
+
```text
|
|
370
|
+
<text just before the value>(.*?)<text just before the value ends>
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
- Put a short, unique literal **before** the value and a literal **after** it.
|
|
374
|
+
- Capture the value with a **lazy** group `(.*?)`. Lazy (`*?`) stops at the first
|
|
375
|
+
following anchor; greedy `(.*)` would run to the *last* match on the line and grab too
|
|
376
|
+
much.
|
|
377
|
+
- The captured **group 1** is the disclosed value. Set `params.group` only to use a
|
|
378
|
+
different index or a named group.
|
|
379
|
+
|
|
380
|
+
Examples (the regex is shown first, then how it appears JSON-escaped in `providers.json`):
|
|
381
|
+
|
|
382
|
+
| Goal | Regex | In JSON (`"pattern"`) |
|
|
383
|
+
|---|---|---|
|
|
384
|
+
| HTML meta tag | `octolytics-actor-login" content="(.*?)"` | `"octolytics-actor-login\" content=\"(.*?)\""` |
|
|
385
|
+
| JSON string field | `"memberSince":"(.*?)"` | `"\"memberSince\":\"(.*?)\""` |
|
|
386
|
+
| JSON boolean | `"isPrimeCustomer":(true\|false)` | `"\"isPrimeCustomer\":(true\|false)"` |
|
|
387
|
+
| JSON number | `"site_count":([0-9]+)` | `"\"site_count\":([0-9]+)"` |
|
|
388
|
+
| Single-quoted JS var | `firstName:.*?'(.*?)'` | `"firstName:.*?'(.*?)'"` |
|
|
389
|
+
| Email value | `"email":"(.*?@.*?)"` | `"\"email\":\"(.*?@.*?)\""` |
|
|
390
|
+
|
|
391
|
+
### JSON escaping (the only escaping you usually need)
|
|
392
|
+
|
|
393
|
+
`providers.json` is JSON, so inside a `"pattern"` string:
|
|
394
|
+
|
|
395
|
+
- a double quote `"` becomes `\"`,
|
|
396
|
+
- a backslash `\` becomes `\\` (so the regex `\d` is written `\\d` — prefer `[0-9]` to
|
|
397
|
+
avoid this),
|
|
398
|
+
- everything else is literal.
|
|
399
|
+
|
|
400
|
+
Two tricks to avoid escaping:
|
|
401
|
+
|
|
402
|
+
- Match a **single-quoted** value (`'(.*?)'`) — single quotes need no JSON escaping (see
|
|
403
|
+
Zoom).
|
|
404
|
+
- Bridge punctuation with `.*?` instead of matching it exactly: `userType:.*?'(.*?)'`
|
|
405
|
+
skips the colon/space without you having to write `\s*` or `:`.
|
|
406
|
+
|
|
407
|
+
### Rules and gotchas
|
|
408
|
+
|
|
409
|
+
- **Use `(.*?)`, not `(.*)`.** Greedy captures overshoot.
|
|
410
|
+
- **`.` does not match newlines.** Values are almost always on one line, so `(.*?)` is
|
|
411
|
+
fine. If a value spans lines, use `([\s\S]*?)` instead of `(.*)`.
|
|
412
|
+
- **Pick a unique anchor.** `content="(.*?)"` alone matches the first `content="..."` on
|
|
413
|
+
the page; prefix it with something specific (`octolytics-actor-login" content=`).
|
|
414
|
+
- **`html`/`text` endpoints accept `regex` reveals only.** `json` endpoints accept `json`,
|
|
415
|
+
`regex`, and `jsonRegex`.
|
|
416
|
+
- Patterns must compile and be ≤ 2048 characters. `params.alias` is **required** for
|
|
417
|
+
`regex`/`jsonRegex`.
|
|
418
|
+
|
|
419
|
+
### Advanced: escaped JSON embedded in HTML
|
|
420
|
+
|
|
421
|
+
Some SSR pages embed JSON with the quotes already backslash-escaped (e.g. Facebook's
|
|
422
|
+
`/settings` ships `\"USER_ID\":\"123\"`). Two robust options:
|
|
423
|
+
|
|
424
|
+
- **Bridge with `.*?` and capture by shape** — skip the punctuation entirely:
|
|
425
|
+
`USER_ID.*?([0-9]{5,})` grabs the numeric id whether or not the quotes are escaped.
|
|
426
|
+
- **Match the optional backslash** with `\\?"` (regex `\\?` = an optional literal
|
|
427
|
+
backslash; JSON-escaped as `\\\\?\"`). This is why Facebook's bundled patterns look
|
|
428
|
+
busier than the others.
|
|
429
|
+
|
|
430
|
+
### Testing a pattern
|
|
431
|
+
|
|
432
|
+
Test against a saved copy of the real (logged-in) page body before adding it — the body
|
|
433
|
+
the replay sees is the raw bytes, so test against *view-source*, not the rendered DOM:
|
|
434
|
+
|
|
435
|
+
```js
|
|
436
|
+
// node or browser console
|
|
437
|
+
const body = `...paste the raw response body...`;
|
|
438
|
+
const m = body.match(/octolytics-actor-login" content="(.*?)"/);
|
|
439
|
+
console.log(m && m[1]); // the value that will be disclosed
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
`npm test` validates that every pattern compiles and recomputes the parser hash.
|
|
443
|
+
|
|
444
|
+
## Parser Hash
|
|
445
|
+
|
|
446
|
+
`templateGovernance.parserHash` is not an arbitrary hardcoded id. The provider test
|
|
447
|
+
script derives it from a canonical parser policy:
|
|
448
|
+
|
|
449
|
+
```json
|
|
450
|
+
{
|
|
451
|
+
"schema": "satya-provider-parser-policy-v1",
|
|
452
|
+
"providerId": "<provider id>",
|
|
453
|
+
"endpoints": [
|
|
454
|
+
{
|
|
455
|
+
"id": "<endpoint id>",
|
|
456
|
+
"responseType": "<response type>",
|
|
457
|
+
"revealRules": []
|
|
458
|
+
}
|
|
459
|
+
]
|
|
460
|
+
}
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
The canonical JSON is SHA-256 hashed and stored as:
|
|
464
|
+
|
|
465
|
+
```text
|
|
466
|
+
parser-sha256:<64 lowercase hex characters>
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
Any change to `providerId`, endpoint ids, endpoint order, `responseType`, or
|
|
470
|
+
`revealRules` changes the parser hash. Run this after editing providers:
|
|
471
|
+
|
|
472
|
+
```bash
|
|
473
|
+
cd platform_sdks/expo-satya-attest
|
|
474
|
+
npm run test:providers
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
The client checks verifier policy against the selected template hash and parser hash
|
|
478
|
+
before using verifier-supplied pins. The backend verifier also pins provider id,
|
|
479
|
+
template hash, parser hash, activation state, and SPKI policy before accepting an
|
|
480
|
+
artifact.
|
|
481
|
+
|
|
482
|
+
## Validation Checklist
|
|
483
|
+
|
|
484
|
+
The SDK and provider test script enforce the important invariants:
|
|
485
|
+
|
|
486
|
+
- registry `providers` must be a non-empty array,
|
|
487
|
+
- provider ids must be unique,
|
|
488
|
+
- endpoint arrays cannot be empty and are capped at 20 endpoints per provider,
|
|
489
|
+
- endpoint ids must be unique per provider,
|
|
490
|
+
- login and endpoint URLs must be HTTPS and cannot contain credentials,
|
|
491
|
+
- endpoint URL host must match `tlsIdentityPolicy.expectedSni`,
|
|
492
|
+
- endpoint `headers`, `requestMatchRules`, and `revealRules` cannot be empty,
|
|
493
|
+
- `responseType` must be one of `json`, `html`, or `text`,
|
|
494
|
+
- request matching must bind both method and URL,
|
|
495
|
+
- reveal rules must include at least one `RECV BODY` reveal (`json`, `regex`, or `jsonRegex`),
|
|
496
|
+
- `regex`/`jsonRegex` reveals require a valid (compilable, ≤ 2048 char) `pattern` and an
|
|
497
|
+
explicit `alias`; `html`/`text` endpoints accept `regex` reveals only,
|
|
498
|
+
- `allowedRequestHeaders`, `allowedResponseBodyJsonPaths`, and raw endpoint
|
|
499
|
+
`privacyPolicy` are rejected in `src/providers.json`,
|
|
500
|
+
- top-level `injection` must be a non-empty string or non-empty string array when
|
|
501
|
+
present,
|
|
502
|
+
- `login.injection` is rejected because injection is top-level provider policy,
|
|
503
|
+
- SPKI pins must be SHA-256 base64 values,
|
|
504
|
+
- parser hashes must match the canonical parser policy.
|
|
505
|
+
|
|
506
|
+
## Provider Authoring Guidance
|
|
507
|
+
|
|
508
|
+
- Prefer `methodEquals` plus a stable API path in `urlContains`.
|
|
509
|
+
- Add `bodyJsonPathEquals` for shared GraphQL endpoints or overloaded API paths.
|
|
510
|
+
- Avoid `headerEquals` for normal discovery unless there is no reliable URL/body
|
|
511
|
+
discriminator.
|
|
512
|
+
- Keep `headers` to the minimum request headers needed for native replay. Include
|
|
513
|
+
`user-agent` when the endpoint is user-agent sensitive (e.g. desktop-only SSR pages) and
|
|
514
|
+
set the provider `userAgent` so the WebView and the replay agree.
|
|
515
|
+
- Keep reveal rules narrow; every `RECV BODY` reveal becomes a disclosed claim. Prefer
|
|
516
|
+
`json` for JSON APIs; use `regex` for HTML/text pages or to pull a substring; anchor
|
|
517
|
+
regex patterns tightly (`"field":"([^"]+)"`) so they match exactly one value.
|
|
518
|
+
- Treat `injection` as trusted code. Do not load registry files from untrusted hosts.
|
|
519
|
+
- Keep backend verifier policy in sync with registry changes before making
|
|
520
|
+
`providerPolicyRefresh="verifier-required"` the default for a provider.
|