@dpdpguard/react-native 1.2.0 → 1.3.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/README.md +58 -4
- package/package.json +1 -1
- package/src/auditHash.ts +63 -0
- package/src/client.ts +35 -0
- package/src/index.ts +2 -0
package/README.md
CHANGED
|
@@ -37,10 +37,18 @@ across both hybrid SDKs.
|
|
|
37
37
|
DPDP Guard's `/api/v1/auth/broker-token`, and hands the resulting
|
|
38
38
|
short-lived access token to the app. `DpdpGuardClient` takes that token
|
|
39
39
|
via `setAccessToken()` / the constructor, nothing more.
|
|
40
|
-
- **No audit
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
- **No keyed audit hash, and no webhook-signature code.** Both require a
|
|
41
|
+
secret the app must not hold (`DPDP_AUDIT_HASH_HMAC_SECRET` / a webhook
|
|
42
|
+
endpoint's own secret) — server-only concerns, unsafe inside a mobile
|
|
43
|
+
client. The *unkeyed* half of the audit hash is available as
|
|
44
|
+
`canonicalAuditString()` (see [Audit-hash canonicalization](#audit-hash-canonicalization));
|
|
45
|
+
it needs no secret.
|
|
46
|
+
- **No offline consent capture.** `dpdpguard-ios-sdk` implements the
|
|
47
|
+
device-side Cryptographic Consent Artifact (`docs/specs/offline-consent-capture.md`
|
|
48
|
+
§4) on top of Secure Enclave key storage and an encrypted local store.
|
|
49
|
+
That needs native code, which this package deliberately doesn't have — and
|
|
50
|
+
a software-only port would produce artifacts that don't prove what the
|
|
51
|
+
spec's artifacts prove.
|
|
44
52
|
|
|
45
53
|
## Usage
|
|
46
54
|
|
|
@@ -97,6 +105,52 @@ The contract version these were generated from is recorded in
|
|
|
97
105
|
`src/CONTRACT_VERSION`; `src/contractVersion.test.ts` fails if an install
|
|
98
106
|
drifts off it.
|
|
99
107
|
|
|
108
|
+
### Endpoints without a facade method
|
|
109
|
+
|
|
110
|
+
The DPDP Guard API is wider than the Data-Principal surface this SDK
|
|
111
|
+
curates. `call()` reaches the rest, with the same auth, URL joining, and
|
|
112
|
+
`DpdpGuardApiError` mapping as every other method:
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
import type { components } from '@dpdpguard/react-native';
|
|
116
|
+
|
|
117
|
+
type Decision = components['schemas']['ConsentGateDecision'];
|
|
118
|
+
const { decisions } = await client.call<{ decisions: Decision[] }>(
|
|
119
|
+
'GET',
|
|
120
|
+
'/api/v1/consent/gate/decisions?limit=50',
|
|
121
|
+
);
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
This widens reach, not privilege: the client still sends only the brokered
|
|
125
|
+
principal token, so org-scoped endpoints will correctly 401 from an app.
|
|
126
|
+
|
|
127
|
+
### Audit-hash canonicalization
|
|
128
|
+
|
|
129
|
+
`canonicalAuditString()` builds the `|`-joined canonical string that the
|
|
130
|
+
`consentAuditTrail.auditHash` is computed over
|
|
131
|
+
(`@dpdpguard/contract`'s `conformance/audit-hash-spec.md`), checked against
|
|
132
|
+
that package's golden vectors:
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
import { canonicalAuditString } from '@dpdpguard/react-native';
|
|
136
|
+
|
|
137
|
+
canonicalAuditString({
|
|
138
|
+
organizationId: 'org_abc123',
|
|
139
|
+
noticeId: 'notice_v1',
|
|
140
|
+
noticeVersion: 1,
|
|
141
|
+
purpose: 'Marketing',
|
|
142
|
+
dataTypes: ['phone', 'email'], // sorted for you
|
|
143
|
+
givenAt: 1700000001000,
|
|
144
|
+
source: 'brokered',
|
|
145
|
+
});
|
|
146
|
+
// 'org_abc123|notice_v1|1|Marketing|email,phone|1700000001000|brokered'
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
The hash itself is `HMAC-SHA256` of this string under a secret that stays
|
|
150
|
+
server-side, so this package stops at the canonical form — hand it to your
|
|
151
|
+
backend to sign, or use it to check which event a hash your backend
|
|
152
|
+
returned refers to.
|
|
153
|
+
|
|
100
154
|
## Contract
|
|
101
155
|
|
|
102
156
|
Depends on
|
package/package.json
CHANGED
package/src/auditHash.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-002 D5 audit-hash canonicalization, ported from
|
|
3
|
+
* `@dpdpguard/contract`'s `conformance/audit-hash-spec.md` and pinned by the
|
|
4
|
+
* golden vectors in `conformance/audit-hash-vectors.json`.
|
|
5
|
+
*
|
|
6
|
+
* **This module deliberately stops at the canonical string.** The full
|
|
7
|
+
* `auditHash` is `HMAC-SHA256(canonical, DPDP_AUDIT_HASH_HMAC_SECRET)`, and
|
|
8
|
+
* that secret is server-side-only — the spec's "Key management" section is
|
|
9
|
+
* explicit that it never leaves DPDP Guard's infrastructure, and anything
|
|
10
|
+
* shipped in a React Native bundle is extractable from the app binary. So
|
|
11
|
+
* unlike `dpdpguard-ios-sdk` and `dpdpguard-android-sdk`, which both expose
|
|
12
|
+
* `computeAuditHash(input, secret)` because a server-side caller may
|
|
13
|
+
* legitimately hold the key, this package exposes only the unkeyed half.
|
|
14
|
+
*
|
|
15
|
+
* What that half is good for: producing the exact bytes your own backend
|
|
16
|
+
* will sign, and re-deriving the canonical form of a record so you can
|
|
17
|
+
* check a hash your backend computed refers to the event you think it does.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** ADR-004 D5 auth provenance for a consent event. */
|
|
21
|
+
export type ConsentSource = "brokered" | "direct" | "anonymous" | "linked";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The exact fields the canonicalization is computed over. Every field is
|
|
25
|
+
* always present in the canonical string — an absent {@link source}
|
|
26
|
+
* serializes as the literal `"null"`, never an omitted position — so this is
|
|
27
|
+
* a *versioned*, safety-critical algorithm. Changing field order or the
|
|
28
|
+
* `"null"` sentinel is a major bump requiring fresh DPO sign-off.
|
|
29
|
+
*/
|
|
30
|
+
export interface AuditHashInput {
|
|
31
|
+
organizationId: string;
|
|
32
|
+
noticeId: string;
|
|
33
|
+
noticeVersion: number;
|
|
34
|
+
purpose: string;
|
|
35
|
+
dataTypes: string[];
|
|
36
|
+
/** Unix epoch milliseconds. Client-authoritative per ADR-003 D3. */
|
|
37
|
+
givenAt: number;
|
|
38
|
+
source?: ConsentSource | null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The `|`-joined canonical string for `input`, per the spec's "Canonical
|
|
43
|
+
* form":
|
|
44
|
+
*
|
|
45
|
+
* ```
|
|
46
|
+
* organizationId|noticeId|noticeVersion|purpose|dataTypes(sorted,joined by ",")|givenAt|source("null" if absent)
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
* `dataTypes` is sorted lexicographically, so call order never changes the
|
|
50
|
+
* result. The input array is not mutated.
|
|
51
|
+
*/
|
|
52
|
+
export function canonicalAuditString(input: AuditHashInput): string {
|
|
53
|
+
const sortedDataTypes = [...input.dataTypes].sort().join(",");
|
|
54
|
+
return [
|
|
55
|
+
input.organizationId,
|
|
56
|
+
input.noticeId,
|
|
57
|
+
String(input.noticeVersion),
|
|
58
|
+
input.purpose,
|
|
59
|
+
sortedDataTypes,
|
|
60
|
+
String(input.givenAt),
|
|
61
|
+
input.source ?? "null",
|
|
62
|
+
].join("|");
|
|
63
|
+
}
|
package/src/client.ts
CHANGED
|
@@ -97,6 +97,41 @@ export class DpdpGuardClient {
|
|
|
97
97
|
return json as T;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Escape hatch for an `/api/v1` endpoint this client has no facade
|
|
102
|
+
* method for — the DPDP Guard API is wider than the Data-Principal
|
|
103
|
+
* surface this SDK curates (see "What's here vs. what's not" in
|
|
104
|
+
* README.md), and `src/generated/api-types.ts` types all of it.
|
|
105
|
+
*
|
|
106
|
+
* `dpdpguard-ios-sdk` ships its whole generated `DPDPGuardConsentAPI`
|
|
107
|
+
* target next to the hand-written facade so nothing is unreachable;
|
|
108
|
+
* this is the equivalent, without a second HTTP stack.
|
|
109
|
+
*
|
|
110
|
+
* Auth, base-URL joining, and {@link DpdpGuardApiError} mapping are the
|
|
111
|
+
* same as every facade method. Note this changes nothing about *which*
|
|
112
|
+
* credential the client holds: it still sends only the brokered
|
|
113
|
+
* principal token from {@link setAccessToken}, never a service API key,
|
|
114
|
+
* so org-scoped endpoints will correctly 401.
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* ```ts
|
|
118
|
+
* import type { components } from '@dpdpguard/react-native';
|
|
119
|
+
*
|
|
120
|
+
* type Decision = components['schemas']['ConsentGateDecision'];
|
|
121
|
+
* const { decisions } = await client.call<{ decisions: Decision[] }>(
|
|
122
|
+
* 'GET',
|
|
123
|
+
* '/api/v1/consent/gate/decisions?limit=50',
|
|
124
|
+
* );
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
call<T>(
|
|
128
|
+
method: string,
|
|
129
|
+
path: string,
|
|
130
|
+
options: { body?: unknown; auth?: "none" | "bearer" } = {},
|
|
131
|
+
): Promise<T> {
|
|
132
|
+
return this.request<T>(method, path, options);
|
|
133
|
+
}
|
|
134
|
+
|
|
100
135
|
// --- Public reads (no auth) ---
|
|
101
136
|
|
|
102
137
|
getOrganization(slug: string): Promise<OrgSummary> {
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,8 @@ export { DpdpGuardApiError, ERROR_CATALOG } from './errorCatalog';
|
|
|
4
4
|
export type { ApiErrorCode, ErrorCatalogEntry } from './errorCatalog';
|
|
5
5
|
export { hasConsent } from './consentGate';
|
|
6
6
|
export type { ConsentRecord } from './consentGate';
|
|
7
|
+
export { canonicalAuditString } from './auditHash';
|
|
8
|
+
export type { AuditHashInput, ConsentSource } from './auditHash';
|
|
7
9
|
|
|
8
10
|
// The openapi-typescript output generated from the installed
|
|
9
11
|
// @dpdpguard/contract's openapi/v1.yaml, re-exported so consumers can name
|