@pawmaskrelease/psecure 0.1.0-beta → 1.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/CHANGELOG.md +49 -0
- package/LICENSE +44 -44
- package/README.md +560 -175
- package/bin/psecure.mjs +13 -0
- package/dist/adapters/desktop.d.ts +26 -26
- package/dist/adapters/desktop.js +25 -12
- package/dist/adapters/index.js +1 -1
- package/dist/adapters/mobile-bridge.d.ts +28 -3
- package/dist/adapters/mobile-bridge.js +23 -12
- package/dist/adapters/react-native.d.ts +9 -15
- package/dist/adapters/react-native.js +15 -9
- package/dist/adapters/web-shield.d.ts +1 -1
- package/dist/adapters/web-shield.js +13 -11
- package/dist/adapters/web-vault.d.ts +58 -6
- package/dist/adapters/web-vault.js +97 -21
- package/dist/cli/doctor.d.ts +31 -0
- package/dist/cli/doctor.js +108 -0
- package/dist/client/index.js +1 -1
- package/dist/client/psecure-client.d.ts +56 -14
- package/dist/client/psecure-client.js +179 -38
- package/dist/crypto/block-ciphers.js +1 -1
- package/dist/crypto/chacha-cfb.js +33 -33
- package/dist/crypto/index.js +1 -1
- package/dist/crypto/mba.js +1 -1
- package/dist/crypto/primitives.js +1 -1
- package/dist/crypto/threefish.js +22 -22
- package/dist/crypto/wasm-coprocessor.d.ts +10 -9
- package/dist/crypto/wasm-coprocessor.js +40 -35
- package/dist/crypto/whitebox-aes.js +32 -32
- package/dist/dist-manifest.json +130 -100
- package/dist/errors.d.ts +14 -0
- package/dist/errors.js +53 -0
- package/dist/fetch/index.d.ts +40 -0
- package/dist/fetch/index.js +93 -0
- package/dist/fetch/load-client.d.ts +1 -0
- package/dist/fetch/load-client.js +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -1
- package/dist/protocol/canonical.d.ts +47 -0
- package/dist/protocol/canonical.js +73 -0
- package/dist/protocol/codecs.d.ts +1 -0
- package/dist/protocol/codecs.js +72 -63
- package/dist/protocol/constants.d.ts +1 -1
- package/dist/protocol/constants.js +3 -3
- package/dist/protocol/dvp-probes.js +51 -51
- package/dist/protocol/ed25519.d.ts +3 -8
- package/dist/protocol/ed25519.js +59 -46
- package/dist/protocol/index.js +1 -1
- package/dist/protocol/types.d.ts +8 -2
- package/dist/protocol/types.js +1 -1
- package/dist/protocol/wire.d.ts +2 -13
- package/dist/protocol/wire.js +98 -85
- package/dist/rasp/auto-collector.js +29 -27
- package/dist/rasp/behavior-stream.js +47 -47
- package/dist/rasp/dvp-consistency.js +9 -9
- package/dist/rasp/hardware-binding.js +7 -7
- package/dist/rasp/index.js +1 -1
- package/dist/rasp/klepto-seal.js +1 -1
- package/dist/rasp/mobile-native-probes.js +8 -8
- package/dist/rasp/probes.js +13 -13
- package/dist/rasp/web-integrity-probes.js +47 -44
- package/dist/server/authority-types.d.ts +113 -9
- package/dist/server/authority-types.js +4 -2
- package/dist/server/edge.d.ts +20 -0
- package/dist/server/edge.js +54 -0
- package/dist/server/express.d.ts +40 -0
- package/dist/server/express.js +65 -0
- package/dist/server/fastify.d.ts +35 -0
- package/dist/server/fastify.js +45 -0
- package/dist/server/guard.d.ts +129 -0
- package/dist/server/guard.js +121 -0
- package/dist/server/index.d.ts +5 -5
- package/dist/server/index.js +6 -6
- package/dist/server/middleware.d.ts +13 -3
- package/dist/server/middleware.js +50 -34
- package/dist/server/remote-authority-client.d.ts +4 -4
- package/dist/server/remote-authority-client.js +52 -45
- package/dist/server/verdicts.d.ts +19 -0
- package/dist/server/verdicts.js +49 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +3 -0
- package/dist/vm/dynamic-ob.d.ts +2 -2
- package/dist/vm/dynamic-ob.js +48 -48
- package/dist/vm/env-key.js +5 -5
- package/dist/vm/iabc-isa.js +13 -13
- package/dist/vm/index.d.ts +0 -1
- package/dist/vm/index.js +1 -2
- package/dist/vm/psecure-vm.js +54 -58
- package/dist/web/psecure-polymorphic-loader.obf.js +5 -3
- package/dist/web/psecure-vm-kernel.d.ts +118 -0
- package/dist/web/psecure-vm-kernel.js +142 -78
- package/dist/web/psecure-web-shield.bundle.js +148 -78
- package/package.json +154 -122
- package/dist/server/authority.d.ts +0 -42
- package/dist/server/authority.js +0 -89
- package/dist/server/lease-store.d.ts +0 -25
- package/dist/server/lease-store.js +0 -92
- package/dist/server/pop-verifier.d.ts +0 -21
- package/dist/server/pop-verifier.js +0 -98
- package/dist/server/redis-store.d.ts +0 -89
- package/dist/server/redis-store.js +0 -111
- package/dist/server/store.d.ts +0 -90
- package/dist/server/store.js +0 -70
package/README.md
CHANGED
|
@@ -1,175 +1,560 @@
|
|
|
1
|
-
# @
|
|
2
|
-
|
|
3
|
-
**Paw Security (PSecure)** is the client SDK for the [Paw Mask](https://pawmask.com)
|
|
4
|
-
anti-tamper / anti-reverse-engineering platform. It seals every outgoing request from
|
|
5
|
-
your web, Node, mobile, or desktop app with a bytecode-VM-protected, device-bound
|
|
6
|
-
cryptographic envelope, and your backend confirms each request against the hosted
|
|
7
|
-
**PSecure Verify API** — stopping replayed, forged, scripted, and tampered traffic
|
|
8
|
-
before it reaches your business logic.
|
|
9
|
-
|
|
10
|
-
>
|
|
11
|
-
>
|
|
12
|
-
>
|
|
13
|
-
|
|
14
|
-
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
```ts
|
|
46
|
-
import {
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
import
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
**
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
1
|
+
# @pawmaskrelease/psecure
|
|
2
|
+
|
|
3
|
+
**Paw Security (PSecure)** is the client SDK for the [Paw Mask](https://pawmask.com)
|
|
4
|
+
anti-tamper / anti-reverse-engineering platform. It seals every outgoing request from
|
|
5
|
+
your web, Node, mobile, or desktop app with a bytecode-VM-protected, device-bound
|
|
6
|
+
cryptographic envelope, and your backend confirms each request against the hosted
|
|
7
|
+
**PSecure Verify API** — stopping replayed, forged, scripted, and tampered traffic
|
|
8
|
+
before it reaches your business logic.
|
|
9
|
+
|
|
10
|
+
> Verification runs on the Paw Mask Authority (`https://psecure-api.pawmask.com` or your
|
|
11
|
+
> deployed `psecure-authority` binary/container). The package also provides Node.js and
|
|
12
|
+
> Edge remote verification clients & middlewares (`@pawmaskrelease/psecure/server`) that forward
|
|
13
|
+
> sealed envelopes to the Authority. To prevent offline white-box verification oracle
|
|
14
|
+
> attacks, the Split-Key Verification Engine (`PSecureServerAuthority`) is **never**
|
|
15
|
+
> shipped in the public npm client package. `@pawmaskrelease/psecure/server` is backend-only, is
|
|
16
|
+
> not exported from the root barrel, and must never be bundled into browser code.
|
|
17
|
+
|
|
18
|
+
- Full docs: **https://psecure.pawmask.com/docs**
|
|
19
|
+
- Proprietary software — use requires a valid Paw Mask PSecure subscription or sandbox
|
|
20
|
+
agreement. See [LICENSE](./LICENSE).
|
|
21
|
+
|
|
22
|
+
> 🇻🇳 **Lưu ý:** Đây là SDK phía client. Việc xác minh (Verify) chạy trên dịch vụ
|
|
23
|
+
> được Paw Mask lưu trữ tại `https://psecure-api.pawmask.com`; bạn cần giấy phép PSecure còn
|
|
24
|
+
> hiệu lực để sử dụng. Xem tài liệu tại https://psecure.pawmask.com/vi/docs.
|
|
25
|
+
|
|
26
|
+
## Quick start in 5 minutes
|
|
27
|
+
|
|
28
|
+
You need three values from the PSecure console (the app's **SDK** tab has a *Copy configuration* panel that
|
|
29
|
+
prefills every snippet below):
|
|
30
|
+
|
|
31
|
+
| Value | Where | Secret? |
|
|
32
|
+
| --- | --- | --- |
|
|
33
|
+
| App key `psec_app_…` | Applications → your app | no (ships in the client) |
|
|
34
|
+
| Authority public key (64 hex) | Keys → Lease signing keypair | no (ships in the client) |
|
|
35
|
+
| API token `psec_pat_…` | Keys → API tokens → Issue (shown **once**) | **yes**: backend env only |
|
|
36
|
+
|
|
37
|
+
**1. Install** (Node.js >= 20.16 or a modern browser; zero runtime dependencies)
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install @pawmaskrelease/psecure
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**2. Client**: seal the requests your app sends to your API.
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { createPSecureFetch } from '@pawmaskrelease/psecure/fetch';
|
|
47
|
+
|
|
48
|
+
export const psecure = createPSecureFetch({
|
|
49
|
+
appKey: 'psec_app_…',
|
|
50
|
+
serverPublicKey: '<64-hex authority public key>',
|
|
51
|
+
apiOrigins: ['https://api.example.com'],
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
await psecure.fetch('https://api.example.com/api/orders', { method: 'POST', body }); // instead of fetch()
|
|
55
|
+
psecure.setAccount(user?.id ?? null); // on sign-in / sign-out
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
If PSecure is down or misconfigured, `psecure.fetch` sends the request unsealed: your app keeps working and your
|
|
59
|
+
backend decides.
|
|
60
|
+
|
|
61
|
+
**3. Server**: verify every request. Put the secrets in the backend environment:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
PSECURE_SERVER_KEY=psec_pat_… # the API token (secret)
|
|
65
|
+
PSECURE_ORIGINS=https://api.example.com # the origin your browsers call this API on
|
|
66
|
+
PSECURE_APP_KEY=psec_app_… # this backend's app (recommended: refuses leases of your other apps)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Fastify:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { psecureFastify } from '@pawmaskrelease/psecure/fastify';
|
|
73
|
+
await app.register(psecureFastify);
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Express:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import { psecure, psecureRawBody } from '@pawmaskrelease/psecure/express';
|
|
80
|
+
app.use(express.json({ verify: psecureRawBody })); // keep the exact body bytes
|
|
81
|
+
app.use('/api', psecure());
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Next.js (route handler; also Hono, Workers, Bun, Deno):
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import { withPSecure } from '@pawmaskrelease/psecure/edge';
|
|
88
|
+
export const POST = withPSecure(async (request) => Response.json({ ok: true }));
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
If your API is on another origin than your web app, allow the PSecure headers in CORS: request headers
|
|
92
|
+
`PSECURE_REQUEST_HEADERS` and exposed headers `PSECURE_EXPOSED_HEADERS` (both exported by every adapter).
|
|
93
|
+
|
|
94
|
+
**4. Monitor, then Block.** New apps start in **Monitor**: every request is verified and logged
|
|
95
|
+
(`psecure verify` lines, `request.psecure`), nothing is blocked. When the logs look clean, switch the app to
|
|
96
|
+
**Block** in the console: the adapters follow the switch on the next request, no redeploy. (`PSECURE_MODE=monitor |
|
|
97
|
+
enforce | off` overrides the console switch for incidents and tests.)
|
|
98
|
+
|
|
99
|
+
**5. Check the setup.**
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
PSECURE_SERVER_KEY=psec_pat_… npx @pawmaskrelease/psecure doctor \
|
|
103
|
+
--app-key psec_app_… --public-key <64 hex> --origin https://app.example.com
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`doctor` prints ✓ / ✗ with a fix for: authority reachable, pinned key, clock, CORS, a test lease with your app key,
|
|
107
|
+
your server key (never printed) and the app pinning (`PSECURE_APP_KEY`). Every misconfiguration the SDK detects at runtime is a `PSecureConfigError` with
|
|
108
|
+
a stable `code` and a `hint` (see [Errors](#errors)). Full docs: https://psecure.pawmask.com/docs
|
|
109
|
+
|
|
110
|
+
## Install
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
npm install @pawmaskrelease/psecure
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Requirements: Node.js >= 20.16.0 or a modern browser (ES2023). Zero runtime dependencies.
|
|
117
|
+
|
|
118
|
+
## How it works
|
|
119
|
+
|
|
120
|
+
1. Your app **leases** a short-lived, server-signed ticket from the hosted authority.
|
|
121
|
+
2. The client **seals** each request into an envelope using the lease: a 3-basic-block
|
|
122
|
+
bytecode VM, device (hardware-anchor) binding, RASP telemetry, and an HMAC-SHA256
|
|
123
|
+
AEAD tag. The envelope travels in `x-psecure-*` headers.
|
|
124
|
+
3. Your **backend** forwards the envelope to the PSecure **Verify API** with request
|
|
125
|
+
context; Verify returns allow / deny (and policy signals) which your backend enforces.
|
|
126
|
+
|
|
127
|
+
## `PSecureClient` in detail (web / browser)
|
|
128
|
+
|
|
129
|
+
### 1. One-call connect (`PSecureClient.connect`)
|
|
130
|
+
|
|
131
|
+
`connect()` collects device anchors (subject to `telemetry` / consent, see below), creates a
|
|
132
|
+
Proof-of-Possession (PoP) key, acquires a lease from `/psec/lease` on `authorityUrl`, and verifies the
|
|
133
|
+
lease signature against the **pinned** authority key:
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
import { PSecureClient } from '@pawmaskrelease/psecure';
|
|
137
|
+
|
|
138
|
+
// Ship the authority's Ed25519 public key with your build (env / config); never fetch it at runtime.
|
|
139
|
+
const PSECURE_SERVER_PUBLIC_KEY = '<64-hex Ed25519 public key from the Paw Mask console>';
|
|
140
|
+
|
|
141
|
+
const client = await PSecureClient.connect({
|
|
142
|
+
authorityUrl: 'https://psecure-api.pawmask.com',
|
|
143
|
+
serverPublicKey: PSECURE_SERVER_PUBLIC_KEY, // REQUIRED (hex, PEM, KeyObject, or an array during key rotation)
|
|
144
|
+
sealOrigins: ['https://api.example.com'], // origins protectFetch may seal for (default: authority origin)
|
|
145
|
+
appKey: 'psec_app_…', // app key from the console (not a secret)
|
|
146
|
+
accountId,
|
|
147
|
+
// telemetry: 'minimal', onBeforeCollect: (ctx) => userHasConsented, // see "Privacy & consent"
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const response = await client.protectFetch('https://api.example.com/api/order', {
|
|
151
|
+
method: 'POST',
|
|
152
|
+
headers: { 'content-type': 'application/json' },
|
|
153
|
+
body: JSON.stringify({ sku: 1 }),
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// On logout:
|
|
157
|
+
client.dispose();
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
`connect()` rejects:
|
|
161
|
+
- a missing or invalid `serverPublicKey`;
|
|
162
|
+
- a non-https `authorityUrl` (http is allowed only for `localhost`/loopback, or with `allowInsecureHttp: true`
|
|
163
|
+
in development);
|
|
164
|
+
- a `leaseEndpointPath` that is not a relative path, or that resolves to another origin;
|
|
165
|
+
- a lease that does not verify against the pinned key, is for another account/install/PoP key, or is already
|
|
166
|
+
expired.
|
|
167
|
+
|
|
168
|
+
A `serverPublicKeyHex` in the lease response is never used. If it differs from the pinned key, `connect()`
|
|
169
|
+
fails as a likely MITM.
|
|
170
|
+
|
|
171
|
+
**PoP key.** By default (`popKey: 'auto'`) the PoP key is a **non-extractable WebCrypto Ed25519 `CryptoKey`**
|
|
172
|
+
when the platform supports it, with a fallback to an in-memory software key. Clients holding a WebCrypto key
|
|
173
|
+
seal asynchronously: use `protectFetch()` or `sealRequestAsync()`. The synchronous `sealRequest()` and the
|
|
174
|
+
`PSecureWebShield` vault helpers need `popKey: 'software'`.
|
|
175
|
+
|
|
176
|
+
### 2. Manual lease & sealing (advanced)
|
|
177
|
+
|
|
178
|
+
Obtain a lease from the authority on your own channel, then construct the client. The pinned
|
|
179
|
+
`serverPublicKey` comes from your build configuration, not from the lease response.
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
import { PSecureClient } from '@pawmaskrelease/psecure';
|
|
183
|
+
import type { PSecureHardwareAnchors } from '@pawmaskrelease/psecure';
|
|
184
|
+
|
|
185
|
+
const pop = PSecureClient.generateEphemeralPopKeyPair(); // or: await PSecureClient.generateNonExtractablePopKeyPair()
|
|
186
|
+
// ... request a lease bound to pop.clientPopPubKeyHex from your authority ...
|
|
187
|
+
|
|
188
|
+
const client = new PSecureClient({
|
|
189
|
+
accountId,
|
|
190
|
+
installId,
|
|
191
|
+
serverPublicKey: PSECURE_SERVER_PUBLIC_KEY, // REQUIRED, pinned
|
|
192
|
+
leaseToken: lease.compactToken, // SECRET: never log it or put it in URLs
|
|
193
|
+
hardwareAnchors,
|
|
194
|
+
clientPopSeedHex: pop.clientPopSeedHex, // REQUIRED for PoP-bound leases (or clientPopKey: { privateKey, publicKeyHex })
|
|
195
|
+
authorityUrl: 'https://psecure-api.pawmask.com', // default seal origin and reissue origin
|
|
196
|
+
sealOrigins: ['https://api.example.com'],
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Option A: drop-in fetch wrapper (accepts a string, URL or Request)
|
|
200
|
+
const response = await client.protectFetch('https://api.example.com/api/order', {
|
|
201
|
+
method: 'POST',
|
|
202
|
+
body: JSON.stringify({ sku: 1 }),
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Option B: seal manually and attach the headers yourself
|
|
206
|
+
const { envelope, headers } = client.sealRequest({
|
|
207
|
+
method: 'POST',
|
|
208
|
+
origin: 'https://api.example.com', // bound into the AEAD tag and PoP signature (protocol 0.1.1)
|
|
209
|
+
path: '/api/order', // exact path + '?' + raw query, as sent on the wire
|
|
210
|
+
body: JSON.stringify({ sku: 1 }),
|
|
211
|
+
});
|
|
212
|
+
// headers → { 'x-psecure-version', 'x-psecure-envelope', 'x-psecure-aead-tag' }
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
**Reissued leases.** On a `401`, `protectFetch` adopts an `x-psecure-reissued-lease` header only when all of
|
|
216
|
+
these hold:
|
|
217
|
+
- the response comes from an origin in `reissueOrigins` (default: the authority origin; add your API origin if
|
|
218
|
+
your API middleware performs reissue);
|
|
219
|
+
- the lease verifies against the pinned key;
|
|
220
|
+
- it is for the same account, install and PoP key;
|
|
221
|
+
- it is `w2:`-wrapped (authenticated) under the current session key and continues the current lease chain;
|
|
222
|
+
- it is newer than the current lease, has not been used before, and is unexpired.
|
|
223
|
+
|
|
224
|
+
Anything else is ignored and the original `401` is returned. `client.updateLeaseToken(token, { nowMs? })`
|
|
225
|
+
applies the same checks, except that wrapping is required only for header-delivered reissues.
|
|
226
|
+
|
|
227
|
+
### Breaking changes (v1.0.0 release / protocol 0.1.1 security baseline)
|
|
228
|
+
|
|
229
|
+
| Change | Migration |
|
|
230
|
+
| --- | --- |
|
|
231
|
+
| `serverPublicKey` is **required** for `connect()` and `new PSecureClient()`. Auto-discovery from the lease response, `GET /v1/public-key` and `globalThis.__PSEC_TENANT_KERNEL__` was removed, and so was `requirePinnedServerPublicKey` (pinning is always on). | Pass the authority key from build config. An array of keys is accepted for key rotation. |
|
|
232
|
+
| `protectFetch` seals only for `sealOrigins` (default: the origin of `authorityUrl`). Other origins throw. | Add every API origin that must receive sealed requests. |
|
|
233
|
+
| The envelope gains `origin`, and the AEAD/PoP canonical messages changed (protocol 0.1.1). `sealRequest()` needs `input.origin`, or `authorityUrl` / `sealOrigins` on the client. | Upgrade the verifier together with the SDK. |
|
|
234
|
+
| A PoP-bound lease without `clientPopSeedHex` / `clientPopKey` throws. A lease without `publicLeaseToken` throws, so the private token is never sent. | Pass the PoP key the lease was issued for. |
|
|
235
|
+
| Reissued leases are validated (see above). Legacy `w1:` wrapping is rejected. | The authority must emit `w2:` reissues. |
|
|
236
|
+
| `connect()` defaults to a non-extractable WebCrypto PoP key, which makes the sync `sealRequest()` throw. | Use `protectFetch` / `sealRequestAsync`, or pass `popKey: 'software'`. |
|
|
237
|
+
| `createOneTimeSetterBridge` returns a random, non-configurable `handlerKey` that is not deleted after the handshake. `simulatedCallerStack` can only add suspicion. The vault helpers enforce `sealOrigins`. | Pass `handlerKey` to the UI module privately; do not hardcode it. |
|
|
238
|
+
| `PSecureDesktopBridge` IPC packets carry `issuedAtMs`, bound in the AEAD. Packets outside `ipcReplayWindowMs` (default 120 s) are rejected. | Re-seal IPC messages with the new SDK on both ends. |
|
|
239
|
+
| `connect()` requires https for non-loopback authorities and validates `leaseEndpointPath`. | Use https, or `allowInsecureHttp` in development. |
|
|
240
|
+
|
|
241
|
+
### Privacy & consent
|
|
242
|
+
|
|
243
|
+
In `telemetry: 'full'` mode (the default), `connect()` reads device characteristics and stores an identifier:
|
|
244
|
+
- it reads CPU cores, platform, language, screen, the WebGL renderer, and canvas and audio hashes;
|
|
245
|
+
- it stores a random install identifier in `localStorage.__psec_install_id`.
|
|
246
|
+
|
|
247
|
+
In the EU/EEA, this is access to and storage of information on the user's device (ePrivacy Art. 5(3)). It
|
|
248
|
+
typically needs consent, unless you document that it is strictly necessary for a service the user requested.
|
|
249
|
+
Fraud prevention under GDPR Art. 6(1)(f) still needs a legitimate-interest assessment, and usually a DPIA.
|
|
250
|
+
|
|
251
|
+
The SDK gives you these controls:
|
|
252
|
+
- **`onBeforeCollect(ctx)`** is called **before anything is read or stored**. `ctx` lists the signal groups,
|
|
253
|
+
whether an identifier will be persisted, and whether device fingerprinting will run. Return:
|
|
254
|
+
- `false` to abort (nothing is collected and no request is sent);
|
|
255
|
+
- `true` to proceed;
|
|
256
|
+
- `'minimal'` or `'full'` to choose the level.
|
|
257
|
+
- **`telemetry: 'minimal'`** collects only coarse hints (CPU core count, platform, language). It hashes every
|
|
258
|
+
anchor client-side (salted with `anchorSalt` or `appKey`) and uses a per-session install id, so nothing is
|
|
259
|
+
stored. Device binding is correspondingly weaker.
|
|
260
|
+
- **`hashAnchors: true`** also hashes the anchors in `'full'` mode.
|
|
261
|
+
|
|
262
|
+
Other behavior:
|
|
263
|
+
- Sealed envelopes, which carry the anchors, are only sent to `sealOrigins`.
|
|
264
|
+
- RASP integrity probes (automation and tamper indicators) run in both modes.
|
|
265
|
+
- Retention of envelope data is governed by your authority / Paw Mask DPA; document it in your privacy notice.
|
|
266
|
+
|
|
267
|
+
### Security model & limits
|
|
268
|
+
|
|
269
|
+
What the SDK does:
|
|
270
|
+
- The authority key is pinned, and leases signed by any other key are rejected.
|
|
271
|
+
- Secrets live in ECMAScript `#private` fields.
|
|
272
|
+
- `crypto.getRandomValues`, `crypto.subtle`, `fetch`, `Response#text`, `Headers#get` and `JSON` are captured
|
|
273
|
+
when the SDK module is first evaluated, so monkey-patching them afterwards has no effect. Load the SDK before
|
|
274
|
+
third-party scripts.
|
|
275
|
+
|
|
276
|
+
**Limits:**
|
|
277
|
+
- Same-realm malicious script (XSS, main-world extensions) can still call `protectFetch` as an oracle.
|
|
278
|
+
- Scripts that run before the SDK can interpose on primitives.
|
|
279
|
+
- The lease `compactToken` is a bearer secret that contains the session key. Anyone who captures the
|
|
280
|
+
`/psec/lease` response body can recover the session key: from HAR files, proxy or APM logs, or a hooked
|
|
281
|
+
`fetch` that ran first. With a non-extractable PoP key they still cannot produce valid PoP signatures, so
|
|
282
|
+
PoP-enforcing verifiers reject the clones. A full fix (planned) derives the session key by ECDH/HPKE to a
|
|
283
|
+
client key instead of shipping it in the token.
|
|
284
|
+
- The `PSecureWebShield` vault and bridge helpers are defence-in-depth against casual tooling. They are not a
|
|
285
|
+
security boundary.
|
|
286
|
+
|
|
287
|
+
### 3. Verify on your backend
|
|
288
|
+
|
|
289
|
+
> Most backends should use the 1.1 [server adapters](#server-adapters-11) (Fastify, Express, Next.js/Edge). This
|
|
290
|
+
> section documents the lower-level verifier they are built on.
|
|
291
|
+
|
|
292
|
+
`@pawmaskrelease/psecure/server` forwards the sealed request to the hosted authority and returns its verdict.
|
|
293
|
+
Your server key (tenant API key from the PSecure console) is a **secret**: keep it in the server environment only.
|
|
294
|
+
The Split-Key Verification Engine itself is intentionally not in the public npm package (it would be an offline
|
|
295
|
+
white-box oracle).
|
|
296
|
+
|
|
297
|
+
```ts
|
|
298
|
+
import {
|
|
299
|
+
PSecureRemoteAuthorityClient,
|
|
300
|
+
createPSecureExpressMiddleware,
|
|
301
|
+
createPSecureRemoteFetchEdgeMiddleware,
|
|
302
|
+
} from '@pawmaskrelease/psecure/server';
|
|
303
|
+
|
|
304
|
+
const authority = new PSecureRemoteAuthorityClient({
|
|
305
|
+
baseUrl: 'https://psecure-api.pawmask.com',
|
|
306
|
+
apiKey: process.env.PSECURE_SERVER_KEY!, // SECRET
|
|
307
|
+
timeoutMs: 1500, // default 3000
|
|
308
|
+
});
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
**Any Node framework (Fastify, NestJS, custom handlers)** — call the verifier with the request exactly as
|
|
312
|
+
received. `body` must be the raw bytes or string the client sent (not re-serialized JSON):
|
|
313
|
+
|
|
314
|
+
```ts
|
|
315
|
+
const verdict = await authority.verifyHttpRequestAsync({
|
|
316
|
+
method: req.method,
|
|
317
|
+
origin: ['https://api.example.com'], // origins the envelope may be bound to
|
|
318
|
+
path: req.url, // path + '?' + raw query, as received
|
|
319
|
+
body: req.rawBody, // Buffer / Uint8Array / string
|
|
320
|
+
headers: req.headers, // x-psecure-* headers are read from here
|
|
321
|
+
clientIp: req.ip,
|
|
322
|
+
userAgent: req.headers['user-agent'],
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
if (!verdict.allowed) {
|
|
326
|
+
// verdict.action: 'REISSUE_LEASE' | 'STEP_UP_CHALLENGE' | 'BAN_ACCOUNT_AND_HWID' | 'STATE_STORE_UNAVAILABLE';
|
|
327
|
+
// verdict.reason is a generic code.
|
|
328
|
+
// Never return verdict.detail to the client: it is a server-side diagnostic.
|
|
329
|
+
return reply.code(403).send({ error: 'request_rejected', reason: verdict.reason });
|
|
330
|
+
}
|
|
331
|
+
// verdict.shadowWouldHaveAction is set when the app runs in Monitor (shadow) mode.
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
**Express / Connect:**
|
|
335
|
+
|
|
336
|
+
```ts
|
|
337
|
+
app.use(
|
|
338
|
+
'/api',
|
|
339
|
+
createPSecureExpressMiddleware(authority, {
|
|
340
|
+
origins: ['https://api.example.com'],
|
|
341
|
+
excludePaths: ['/api/health'],
|
|
342
|
+
}),
|
|
343
|
+
);
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
**Fetch / Edge runtimes (Cloudflare Workers, Next.js middleware, Deno, Bun):**
|
|
347
|
+
|
|
348
|
+
```ts
|
|
349
|
+
const handler = createPSecureRemoteFetchEdgeMiddleware(
|
|
350
|
+
authority,
|
|
351
|
+
(request, verdict) => fetch(request), // downstream; called only for allowed requests
|
|
352
|
+
{ origins: ['https://api.example.com'] },
|
|
353
|
+
);
|
|
354
|
+
export default { fetch: handler };
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
Roll out in two phases. **Monitor** first: verify every request and record the verdicts, but let denied requests
|
|
358
|
+
through — this is a decision of your backend (for example an environment flag around the `if (!verdict.allowed)`
|
|
359
|
+
branch). **Enforce** once the reports look clean. The application mode in the PSecure console (Monitor / Block)
|
|
360
|
+
classifies the app's telemetry; it does not change verdicts. `verdict.shadowWouldHaveAction` is set only when the
|
|
361
|
+
authority deployment itself runs in shadow mode. See https://psecure.pawmask.com/docs/backend/rollout.
|
|
362
|
+
|
|
363
|
+
The HTTP contract of the authority (including `POST /v1/request/verify`) is published as OpenAPI 3.1 in
|
|
364
|
+
`server/api/openapi.json` of the repository.
|
|
365
|
+
|
|
366
|
+
## Server adapters (1.1)
|
|
367
|
+
|
|
368
|
+
The Fastify plugin, the Express middleware and the Fetch/Edge wrapper share one guard (`createPSecureGuard`, also
|
|
369
|
+
exported for custom integrations). Types are structural: the SDK does not depend on any framework.
|
|
370
|
+
|
|
371
|
+
### Options
|
|
372
|
+
|
|
373
|
+
Every option falls back to an environment variable, so `psecureFastify` / `psecure()` / `withPSecure` work with no
|
|
374
|
+
arguments.
|
|
375
|
+
|
|
376
|
+
| Option | Env | Default | |
|
|
377
|
+
| --- | --- | --- | --- |
|
|
378
|
+
| `serverKey` | `PSECURE_SERVER_KEY` | (required unless `mode: 'off'`) | API token `psec_pat_…`. Never logged. |
|
|
379
|
+
| `origins` | `PSECURE_ORIGINS` (comma separated) | (required) | Public origin(s) your browsers call this API on. The client seals for them. |
|
|
380
|
+
| `mode` | `PSECURE_MODE` | `'auto'` | `auto` follows the console switch; `monitor` / `enforce` / `off` override it. |
|
|
381
|
+
| `authorityUrl` | `PSECURE_AUTHORITY_URL` | `https://psecure-api.pawmask.com` | https only (http for loopback). |
|
|
382
|
+
| `timeoutMs` | `PSECURE_TIMEOUT_MS` | `1500` | Authority call timeout. |
|
|
383
|
+
| `appKey` | `PSECURE_APP_KEY` | none | **Recommended.** The app this backend serves: sent as `expectedAppKey`, so the authority refuses leases of any other app (`app_mismatch`) and the console mode is only taken for this app. Also scopes the mode cache. Per request: `appKey` on the request (Fastify route config `psecureAppKey`). |
|
|
384
|
+
| `protect` | | `'all'` | Routes that fail closed (see below). Matchers: `'/pay'`, `'/api/*'`, `'/orders/:id'`, `'POST /pay'`, `RegExp`, `(info) => boolean`. |
|
|
385
|
+
| `skip` | | `[]` | Routes never checked (health checks, webhooks). `OPTIONS` is always skipped. |
|
|
386
|
+
| `onDecision` | | | `(decision, { method, path, route, ip }) => void` for metrics / audit. `createPSecureAuditThrottle({ sink })` turns it into deduplicated audit events. |
|
|
387
|
+
| `logger` | | console | pino-compatible `{ info, warn, debug? }` (Fastify uses `app.log`); `false` disables. |
|
|
388
|
+
| `blockedResponse` | | `403 { error: 'PSECURE_REJECTED' }` | Your API's error shape for refused requests. |
|
|
389
|
+
|
|
390
|
+
Fastify extras: route config `{ psecure: true }` protects a route, `{ psecure: false }` skips it. Express extras:
|
|
391
|
+
`maxBodyBytes` (default 1 MiB). Edge extras: `maxBodyBytes`, `clientIp(request)`; `options` may be a function of the
|
|
392
|
+
handler's extra arguments (Cloudflare Workers `env`).
|
|
393
|
+
|
|
394
|
+
### Decision table
|
|
395
|
+
|
|
396
|
+
| Outcome | Monitor | Block (enforce), `protect` route | Block, other route |
|
|
397
|
+
| --- | --- | --- | --- |
|
|
398
|
+
| verified | served | served | served |
|
|
399
|
+
| denied (tampered, replayed, forged, banned, lease of another app) or malformed envelope | served, logged | **403** | **403** |
|
|
400
|
+
| lease must be renewed, with a reissued lease | served + `x-psecure-reissued-lease` | **401** + `x-psecure-reissued-lease` | **401** + header |
|
|
401
|
+
| no envelope | served | **403** | served |
|
|
402
|
+
| authority down / timeout / key rejected | served | **403** (fail-closed) | served |
|
|
403
|
+
|
|
404
|
+
`createPSecureFetch` adopts a reissued lease on the 401 and retries once. Each decision is available as
|
|
405
|
+
`request.psecure` (Fastify), `req.psecure` / `res.locals.psecure` (Express), or `getPSecureDecision(request)` (all
|
|
406
|
+
adapters, typed): `{ verified, result, action, reason, effectiveMode, modeSource, blocked, wouldBlock,
|
|
407
|
+
envelopeAccountId, ... }`. `envelopeAccountId` is what the client claims: compare it with your session user before
|
|
408
|
+
trusting it.
|
|
409
|
+
|
|
410
|
+
### Monitor / Block (`mode: 'auto'`)
|
|
411
|
+
|
|
412
|
+
The authority reports the app's console mode on every verdict (`appMode`). In `auto` the guard blocks only while the
|
|
413
|
+
app is in **Block**. When a request carries no verdict (no envelope, authority unreachable), the guard uses the last
|
|
414
|
+
mode it saw for the app, for up to 10 minutes; with nothing known yet (cold start) it behaves as Monitor. Trade-off:
|
|
415
|
+
right after a restart during an authority outage, unsealed requests are served until the first verdict arrives.
|
|
416
|
+
Pin `mode: 'enforce'` (or `PSECURE_MODE=enforce`) where that matters more than availability.
|
|
417
|
+
|
|
418
|
+
Set `PSECURE_APP_KEY`. Without it, the guard follows the mode of whichever app a lease belongs to, so a lease of one
|
|
419
|
+
of your Monitor apps replayed against a Block app's backend would only be logged; the guard warns once at startup
|
|
420
|
+
(`APP_KEY_NOT_PINNED`). With it, that lease is denied (`reason: 'app_mismatch'`) and refused in Block. Each decision says which
|
|
421
|
+
mode applied (`effectiveMode`) and why (`modeSource`: `console`, `cache`, `default` or `config`).
|
|
422
|
+
|
|
423
|
+
### Raw body
|
|
424
|
+
|
|
425
|
+
PSecure hashes the exact bytes the client sealed, so the adapters need the raw body:
|
|
426
|
+
|
|
427
|
+
- **Fastify** captures it itself (a `preParsing` tap: works with every content-type parser, never parses twice). An
|
|
428
|
+
existing `request.rawBody` is used as is.
|
|
429
|
+
- **Express**: `express.json({ verify: psecureRawBody })` (same for `express.text` / `raw` / `urlencoded`), or mount
|
|
430
|
+
`psecure()` before the body parser: it then reads and parses the body once and the parser skips it. A body consumed
|
|
431
|
+
without its bytes is reported as `raw_body_unavailable` (and refused in Block).
|
|
432
|
+
- **Edge**: read from a clone; your handler gets an unread `request`.
|
|
433
|
+
|
|
434
|
+
### CORS
|
|
435
|
+
|
|
436
|
+
```ts
|
|
437
|
+
import { PSECURE_REQUEST_HEADERS, PSECURE_EXPOSED_HEADERS } from '@pawmaskrelease/psecure/fastify';
|
|
438
|
+
await app.register(cors, {
|
|
439
|
+
origin: ['https://app.example.com'],
|
|
440
|
+
allowedHeaders: ['content-type', 'authorization', ...PSECURE_REQUEST_HEADERS],
|
|
441
|
+
exposedHeaders: [...PSECURE_EXPOSED_HEADERS],
|
|
442
|
+
});
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
### Recipes
|
|
446
|
+
|
|
447
|
+
**NestJS** (no Nest dependency): on Express, `const app = await NestFactory.create(AppModule, { rawBody: true });
|
|
448
|
+
app.use(psecure());` (Nest's `rawBody: true` fills `req.rawBody`). On Fastify, `NestFactory.create(AppModule, new
|
|
449
|
+
FastifyAdapter())` then `await app.register(psecureFastify)`. A Nest project compiled to CommonJS needs Node.js
|
|
450
|
+
>= 20.19 or >= 22.12 (`require()` of an ES module).
|
|
451
|
+
|
|
452
|
+
**Hono**:
|
|
453
|
+
|
|
454
|
+
```ts
|
|
455
|
+
import { createPSecureEdge } from '@pawmaskrelease/psecure/edge';
|
|
456
|
+
const ps = createPSecureEdge();
|
|
457
|
+
app.use('/api/*', async (c, next) => {
|
|
458
|
+
const { decision, response } = await ps.check(c.req.raw);
|
|
459
|
+
if (response) return response;
|
|
460
|
+
await next();
|
|
461
|
+
c.res = ps.finish(c.res, decision);
|
|
462
|
+
});
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
**Cloudflare Workers**: `export default { fetch: withPSecure(handler, (env) => ({ serverKey: env.PSECURE_SERVER_KEY,
|
|
466
|
+
origins: env.PSECURE_ORIGINS, clientIp: (r) => r.headers.get('cf-connecting-ip') ?? undefined })) };`
|
|
467
|
+
|
|
468
|
+
## Client helper: `createPSecureFetch` (1.1)
|
|
469
|
+
|
|
470
|
+
`@pawmaskrelease/psecure/fetch` wraps `PSecureClient` for apps that just want a sealing `fetch`:
|
|
471
|
+
|
|
472
|
+
| Option | Default | |
|
|
473
|
+
| --- | --- | --- |
|
|
474
|
+
| `appKey`, `serverPublicKey`, `apiOrigins` | (required) | Public values; `serverPublicKey` may be an array during a key rotation. Only `apiOrigins` are sealed. |
|
|
475
|
+
| `authorityUrl` | `https://psecure-api.pawmask.com` | |
|
|
476
|
+
| `enabled` | `true` | Rollout switch: `false` = plain fetch, SDK never loaded. |
|
|
477
|
+
| `accountId` | `null` | Or later `setAccount(id \| null)`. |
|
|
478
|
+
| `telemetry`, `onBeforeCollect` | `'minimal'` | See [Privacy & consent](#privacy--consent). |
|
|
479
|
+
| `waitForConnectMs` | `2000` | How long the first requests wait for the lease before going out unsealed. |
|
|
480
|
+
| `onError`, `strict` | | Called once per problem; `strict: true` throws configuration errors instead. |
|
|
481
|
+
|
|
482
|
+
Returns `{ fetch, setAccount, status, lastError, ready, start, dispose }`. `status()` is `disabled`,
|
|
483
|
+
`misconfigured`, `idle`, `connecting`, `ready` or `failed`. Behaviour:
|
|
484
|
+
|
|
485
|
+
- Graceful fallback: requests go out unsealed while PSecure is disabled, misconfigured, connecting or failing
|
|
486
|
+
(retried with backoff from 30 s to 5 min). A request that already left and failed on the network is rethrown,
|
|
487
|
+
never re-sent unsealed (no duplicated POST).
|
|
488
|
+
- The lease is refreshed in the background when it expires or the API answers `PSECURE_REJECTED`.
|
|
489
|
+
- The SDK core is loaded lazily on the first sealed request (a dynamic import your bundler puts in its own chunk).
|
|
490
|
+
The eager part of `@pawmaskrelease/psecure/fetch` is about 16 KiB gzip; the lazy core about 112 KiB gzip (1.1.0,
|
|
491
|
+
before your bundler's minification).
|
|
492
|
+
- Browser CSP: add the authority origin to `connect-src`. A blocked request is reported as `CSP_BLOCKED`.
|
|
493
|
+
|
|
494
|
+
## Errors
|
|
495
|
+
|
|
496
|
+
`PSecureConfigError` (`code`, `hint`, `docsUrl`) is thrown at startup by the server adapters and reported by
|
|
497
|
+
`createPSecureFetch` (`lastError()`, `onError`). Runtime problems the guard can only see per request are logged once
|
|
498
|
+
as `psecure config:` lines with the same codes.
|
|
499
|
+
|
|
500
|
+
| Code | Typical cause |
|
|
501
|
+
| --- | --- |
|
|
502
|
+
| `SERVER_KEY_MISSING` | `PSECURE_SERVER_KEY` not set (set `PSECURE_MODE=off` to run without PSecure) |
|
|
503
|
+
| `SERVER_KEY_IS_APP_KEY`, `SERVER_KEY_IN_CLIENT` | app key and API token swapped |
|
|
504
|
+
| `SERVER_KEY_REJECTED` | token revoked/expired, or of another organization (authority answered 401/403) |
|
|
505
|
+
| `PUBLIC_KEY_MISSING`, `PUBLIC_KEY_INVALID`, `PUBLIC_KEY_MISMATCH` | pinned key absent, malformed, or not the authority's key |
|
|
506
|
+
| `APP_KEY_MISSING`, `APP_KEY_INVALID`, `ORG_INACTIVE` | app key absent, unknown to the authority or not of your organization (also `PSECURE_APP_KEY`), organization suspended |
|
|
507
|
+
| `ORIGINS_MISSING`, `ORIGIN_INVALID`, `ORIGIN_MISMATCH` | `PSECURE_ORIGINS` / `apiOrigins` absent, malformed, or not the same on both sides |
|
|
508
|
+
| `AUTHORITY_INSECURE`, `AUTHORITY_INVALID`, `AUTHORITY_UNREACHABLE`, `CSP_BLOCKED` | http authority, bad URL, network/DNS, CSP `connect-src` |
|
|
509
|
+
| `CLOCK_SKEW` | the device or server clock is off |
|
|
510
|
+
| `RAW_BODY_MISSING` | the body parser dropped the raw bytes |
|
|
511
|
+
| `MODE_INVALID` | `PSECURE_MODE` is not `auto`, `monitor`, `enforce` or `off` |
|
|
512
|
+
|
|
513
|
+
## `doctor` CLI
|
|
514
|
+
|
|
515
|
+
```bash
|
|
516
|
+
npx @pawmaskrelease/psecure doctor [--authority <url>] [--app-key psec_app_…] [--public-key <hex>]
|
|
517
|
+
[--origin https://app.example.com] [--api https://api.example.com/health] [--json]
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
Flags fall back to `PSECURE_AUTHORITY_URL`, `PSECURE_APP_KEY`, `PSECURE_PUBLIC_KEY` and `PSECURE_WEB_ORIGIN` (the
|
|
521
|
+
origin your web app runs on; not `PSECURE_ORIGINS`, which is your API's). The server key is read from `PSECURE_SERVER_KEY` only (a `--server-key` flag is refused so it does
|
|
522
|
+
not land in shell history) and is never printed: `doctor` proves it by sending a deliberately unsealed verify request,
|
|
523
|
+
which the authority must answer with a denial. With `PSECURE_APP_KEY` set, the same call proves that key is an app of
|
|
524
|
+
the token's organization; unset, `doctor` warns. Exit code 1 when a check fails.
|
|
525
|
+
|
|
526
|
+
## Entry points
|
|
527
|
+
|
|
528
|
+
| Import | What it provides |
|
|
529
|
+
| --- | --- |
|
|
530
|
+
| `@pawmaskrelease/psecure` | Client SDK barrel (protocol, crypto, vm, rasp, client, adapters); no server code |
|
|
531
|
+
| `@pawmaskrelease/psecure/fetch` | `createPSecureFetch` (sealing `fetch` with fallback; lazy-loads the client core) |
|
|
532
|
+
| `@pawmaskrelease/psecure/fastify` | **Backend.** `psecureFastify` plugin, `PSECURE_REQUEST_HEADERS`, `PSECURE_EXPOSED_HEADERS` |
|
|
533
|
+
| `@pawmaskrelease/psecure/express` | **Backend.** `psecure()` middleware, `psecureRawBody` |
|
|
534
|
+
| `@pawmaskrelease/psecure/edge` | **Backend / Edge.** `withPSecure`, `createPSecureEdge` (Next.js, Hono, Workers, Bun, Deno) |
|
|
535
|
+
| `@pawmaskrelease/psecure/client` | `PSecureClient` (`connect` / `sealRequest` / `sealRequestAsync` / `protectFetch` / `dispose`) |
|
|
536
|
+
| `@pawmaskrelease/psecure/server` | **Backend / Edge only.** Remote Authority client & verification middlewares: `PSecureRemoteAuthorityClient`, `createExpressMiddleware`, `createPSecureExpressMiddleware`, `createPSecureRemoteExpressMiddleware`, `createPSecureFetchEdgeMiddleware`, `createPSecureRemoteFetchEdgeMiddleware`, plus the 1.1 guard (`createPSecureGuard`) and the Express / Edge adapters. (The Split-Key Verification Engine is never shipped in the public npm package to prevent offline white-box oracle attacks; never import `@pawmaskrelease/psecure/server` from browser code.) |
|
|
537
|
+
| `@pawmaskrelease/psecure/protocol` | Wire format, codecs, lease verification, constants, types |
|
|
538
|
+
| `@pawmaskrelease/psecure/crypto` | Crypto primitives & ciphers |
|
|
539
|
+
| `@pawmaskrelease/psecure/vm` | `PSecureVm` bytecode VM + polymorphic bundler |
|
|
540
|
+
| `@pawmaskrelease/psecure/rasp` | RASP probes, device intelligence, hardware binding |
|
|
541
|
+
| `@pawmaskrelease/psecure/web-shield` | `PSecureWebShield` DOM/script integrity (browser) |
|
|
542
|
+
| `@pawmaskrelease/psecure/react-native` | React Native bridge adapter |
|
|
543
|
+
| `@pawmaskrelease/psecure/desktop` | Desktop bridge adapter |
|
|
544
|
+
| `@pawmaskrelease/psecure/mobile-bridge` | Native mobile bridge adapter |
|
|
545
|
+
| `@pawmaskrelease/psecure/web-kernel` | Standalone obfuscated web VM kernel (browser) |
|
|
546
|
+
|
|
547
|
+
## Native (Rust) core
|
|
548
|
+
|
|
549
|
+
A high-assurance Rust native kernel (`psecure_core` — `libpsecure_core.so`, `psecure_core.dll`,
|
|
550
|
+
`libpsecure_core.dylib` / `.a`) and post-build ephemeral binary packer (`psecure_packer`) implement
|
|
551
|
+
full Protocol `0.1.1` parity with the TypeScript SDK and Go Authority — including Native Client
|
|
552
|
+
Ed25519 Proof-of-Possession (`psec-pop-v0.1.1`: `clientPopPubKeyHex` in `psec_lease_req` +
|
|
553
|
+
`clientPopSigHex` in sealed envelopes), Kleptographic seals (`kleptoStreamSeed`, `kleptoMortonSeal`,
|
|
554
|
+
`wasmPulseSeal`), UTF-16 `computeDynamicPolyCanaryHex` parity, and pin-on-init `serverPublicKeyHex`
|
|
555
|
+
locking. They are **not required** by this JavaScript/TypeScript client SDK and are **not** shipped
|
|
556
|
+
in this npm package. Contact Paw Mask for native/enterprise integration.
|
|
557
|
+
|
|
558
|
+
## License
|
|
559
|
+
|
|
560
|
+
Proprietary. Copyright Paw Mask. See [LICENSE](./LICENSE).
|