@pawmaskrelease/psecure 1.0.0 → 1.1.1
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 +100 -0
- package/README.md +413 -41
- package/bin/psecure.mjs +13 -0
- package/dist/adapters/desktop.js +1 -1
- package/dist/adapters/index.js +1 -1
- package/dist/adapters/mobile-bridge.js +1 -1
- package/dist/adapters/react-native.js +1 -1
- package/dist/adapters/web-shield.js +1 -1
- package/dist/adapters/web-vault.js +1 -1
- 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.js +83 -82
- package/dist/crypto/block-ciphers.js +1 -1
- package/dist/crypto/chacha-cfb.js +1 -1
- 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 +1 -1
- package/dist/crypto/wasm-coprocessor.js +1 -1
- package/dist/crypto/whitebox-aes.js +1 -1
- package/dist/dist-manifest.json +101 -56
- 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 +94 -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.js +1 -1
- package/dist/protocol/codecs.js +1 -1
- package/dist/protocol/constants.js +1 -1
- package/dist/protocol/dvp-probes.js +1 -1
- package/dist/protocol/ed25519.js +1 -1
- package/dist/protocol/index.js +1 -1
- package/dist/protocol/types.d.ts +3 -1
- package/dist/protocol/types.js +1 -1
- package/dist/protocol/wire.js +1 -1
- package/dist/rasp/auto-collector.js +1 -1
- package/dist/rasp/behavior-stream.js +1 -1
- package/dist/rasp/dvp-consistency.js +1 -1
- package/dist/rasp/hardware-binding.js +1 -1
- package/dist/rasp/index.js +1 -1
- package/dist/rasp/klepto-seal.js +1 -1
- package/dist/rasp/mobile-native-probes.js +1 -1
- package/dist/rasp/probes.js +1 -1
- package/dist/rasp/web-integrity-probes.js +43 -44
- package/dist/server/authority-types.d.ts +2 -0
- package/dist/server/authority-types.js +1 -1
- package/dist/server/edge.d.ts +20 -0
- package/dist/server/edge.js +54 -0
- package/dist/server/express.d.ts +41 -0
- package/dist/server/express.js +65 -0
- package/dist/server/fastify.d.ts +44 -0
- package/dist/server/fastify.js +44 -0
- package/dist/server/guard.d.ts +132 -0
- package/dist/server/guard.js +127 -0
- package/dist/server/index.d.ts +4 -0
- package/dist/server/index.js +5 -1
- package/dist/server/middleware.js +1 -1
- package/dist/server/remote-authority-client.js +27 -27
- package/dist/server/verdicts.js +6 -6
- package/dist/version.d.ts +1 -0
- package/dist/version.js +3 -0
- package/dist/vm/dynamic-ob.js +1 -1
- package/dist/vm/env-key.js +1 -1
- package/dist/vm/iabc-isa.js +1 -1
- package/dist/vm/index.js +1 -1
- package/dist/vm/psecure-vm.js +1 -1
- package/dist/web/psecure-polymorphic-loader.obf.js +1 -1
- package/dist/web/psecure-vm-kernel.js +1 -1
- package/dist/web/psecure-web-shield.bundle.js +1 -1
- package/package.json +33 -2
- package/types/fastify-augment.d.ts +14 -0
- package/types/fastify-augment.js +2 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Changelog: `@pawmaskrelease/psecure`
|
|
2
|
+
|
|
3
|
+
## 1.1.1
|
|
4
|
+
|
|
5
|
+
Fixes from a first production integration of 1.1.0. Backward compatible with 1.1.0.
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **Every real browser was reported as tampered (critical, all web clients ≤ 1.1.0).** The `env_stub_dom_traps`
|
|
10
|
+
RASP probe treated a configurable `document.all[0]` descriptor as a polyfill, but WebIDL makes it configurable in
|
|
11
|
+
every genuine browser (verified on Edge/Chrome 157). Each sealed request carried a RASP bit, the authority counted a
|
|
12
|
+
strike, and the third request banned the account and device for 24 h. The check is removed. The authority (1.1.1)
|
|
13
|
+
also no longer counts strikes for apps in Monitor, so a false positive can never ban users there.
|
|
14
|
+
- **`clientIp` option (Fastify, Express)** to pass the end-user IP when `request.ip` is a proxy hop (e.g. Cloudflare
|
|
15
|
+
Tunnel → Traefik). Use a resolver that only trusts headers your edge sets.
|
|
16
|
+
- **Fastify typing.** `app.register(psecureFastify, { onDecision, blockedResponse })` left the callback parameters
|
|
17
|
+
implicitly `any` under `strict` (the plugin's optional options parameter made Fastify infer `Record<string, any>`).
|
|
18
|
+
`psecureFastify` is now typed `PSecureFastifyPlugin` with a required options parameter; Fastify always passes one,
|
|
19
|
+
and `app.register(psecureFastify)` without options still works.
|
|
20
|
+
- **`createPSecureFetch().ready()` / `status()`** reported `ready` after a background lease refresh failed
|
|
21
|
+
(`lease acquisition failed`), and `ready()` called right after a request resolved before the queued refresh began.
|
|
22
|
+
A failed acquisition now sets `failed` (the previous client keeps sealing until its lease expires), `ready()` waits
|
|
23
|
+
for a queued refresh, and the next successful lease returns to `ready` and clears `lastError()`.
|
|
24
|
+
- README: the backend verification section said the console Monitor/Block switch does not change verdicts. It now
|
|
25
|
+
describes 1.1: the adapters follow it in `auto`, verifier callers read `verdict.appMode`, pre-1.1 middlewares ignore
|
|
26
|
+
it.
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- `@pawmaskrelease/psecure/fastify/augment` (types only): types `request.psecure` (`PSecureDecision | null`) and route
|
|
31
|
+
config `{ psecure?: boolean; psecureAppKey?: string }`. Also exported: `PSecureFastifyRequestDecoration`,
|
|
32
|
+
`PSecureFastifyRouteConfig`, `PSecureFastifyPlugin`. `request.psecure` is now decorated in every mode (`null` in
|
|
33
|
+
`off`).
|
|
34
|
+
- `createPSecureAuditThrottle` and its types (`PSecureGuardAuditEvent`, `PSecureAuditSink`,
|
|
35
|
+
`PSecureAuditThrottleOptions`, `PSecureAuditThrottle`) from `/fastify`, `/express` and `/edge`, not only `/server`.
|
|
36
|
+
`flush()` and `close()` now return a promise that settles once the sink finished (a failing sink never rejects);
|
|
37
|
+
`close()` flushes, is idempotent and ignores later decisions.
|
|
38
|
+
- `modeFallback` / `PSECURE_MODE_FALLBACK` (`monitor` | `enforce`, default `monitor`): the mode `auto` applies while
|
|
39
|
+
the console mode is unknown (restart during an authority outage, cached mode expired). A verdict carrying the
|
|
40
|
+
console mode always wins.
|
|
41
|
+
- Docs: app pinning and `appMode` trust, testing with a stubbed authority, `blockedResponse` status for reissued
|
|
42
|
+
leases, per-route opt-in (`protect: []` + route config), audit events, dynamic import of `/fetch`, web setup
|
|
43
|
+
(`setAccount`, `enabled`, lazy loading, CSP `connect-src`), the cold-start caveat of `auto`.
|
|
44
|
+
- `SERVER_KEY_IS_APP_KEY` (an app key `psec_app_…` in `PSECURE_SERVER_KEY`) now names both key kinds in its message.
|
|
45
|
+
|
|
46
|
+
### Same release, outside the npm package
|
|
47
|
+
|
|
48
|
+
- Console: new applications no longer get made-up figures (SDK version, p95 latency, device count); stored
|
|
49
|
+
placeholders are cleared on load and the Console shows "No data yet". "Apps in use" is counted from the
|
|
50
|
+
organization's applications (console, admin, app-limit check) instead of a stored counter that drifted.
|
|
51
|
+
- Docs: a "Go to production" checklist (secrets on hosting, real client IP behind proxies, browser checks, Monitor to
|
|
52
|
+
Block), troubleshooting for denials right after a deploy and a leaked server key runbook.
|
|
53
|
+
|
|
54
|
+
## 1.1.0
|
|
55
|
+
|
|
56
|
+
Integrations in under an hour: framework adapters, a sealing `fetch`, actionable errors and a `doctor` CLI.
|
|
57
|
+
Backward compatible with 1.0.0: nothing was removed or changed in meaning, and every 1.0.0 import keeps working.
|
|
58
|
+
|
|
59
|
+
### Added
|
|
60
|
+
|
|
61
|
+
- **Server adapters** on one guard core (`createPSecureGuard`), configured from `PSECURE_*` environment variables:
|
|
62
|
+
- `@pawmaskrelease/psecure/fastify`: `psecureFastify` plugin. It captures the raw body itself, sets `request.psecure`,
|
|
63
|
+
and supports route config `{ psecure: true | false }`.
|
|
64
|
+
- `@pawmaskrelease/psecure/express`: `psecure()` middleware and `psecureRawBody` (for `express.json({ verify })`);
|
|
65
|
+
sets `req.psecure`.
|
|
66
|
+
- `@pawmaskrelease/psecure/edge`: `withPSecure(handler)` and `createPSecureEdge()` for Next.js, Hono, Cloudflare
|
|
67
|
+
Workers, Bun and Deno.
|
|
68
|
+
- `PSECURE_REQUEST_HEADERS` / `PSECURE_EXPOSED_HEADERS` for CORS, `getPSecureDecision(request)`, `onDecision` hook,
|
|
69
|
+
`createPSecureAuditThrottle`, pino-compatible logging that never contains the key, envelope, lease or body.
|
|
70
|
+
- Decision table: Monitor never blocks. Block refuses denied and malformed requests everywhere, and fails closed
|
|
71
|
+
(missing envelope, authority down) only on `protect` routes (default: all guarded routes). A lease reissue is
|
|
72
|
+
answered `401` + `x-psecure-reissued-lease`; any other refusal is `403 { error: 'PSECURE_REJECTED' }`.
|
|
73
|
+
- **The console Monitor/Block switch drives enforcement.** The default mode `auto` follows the app's mode, which the
|
|
74
|
+
authority (>= 1.1) reports on every verdict as `appMode`. The last mode seen is cached for 10 minutes when a request
|
|
75
|
+
carries no verdict; a cold start behaves as Monitor. `PSECURE_MODE=monitor | enforce | off` overrides the switch.
|
|
76
|
+
`PSecureVerificationVerdict.appMode` is typed.
|
|
77
|
+
- **App pinning** (`appKey` / `PSECURE_APP_KEY`, recommended): the guard sends it as `expectedAppKey`. The
|
|
78
|
+
authority then denies a lease of any other application, same organization included, with the new reason
|
|
79
|
+
`app_mismatch`, and the guard only takes a console mode reported for that app (`verdict.appKey`). This closes the
|
|
80
|
+
downgrade where a lease of a Monitor app is replayed against a Block app's backend. In `auto` without it, the guard
|
|
81
|
+
logs a one-time `APP_KEY_NOT_PINNED` warning, and `doctor` warns.
|
|
82
|
+
`PSecureRequestVerificationContext.expectedAppKey`, `PSecureHttpVerifyInput.expectedAppKey`, `verdict.appKey` and
|
|
83
|
+
the `app_mismatch` reason are typed and pass through `PSecureRemoteAuthorityClient`.
|
|
84
|
+
- **`createPSecureFetch`** (`@pawmaskrelease/psecure/fetch`, also from the root): a drop-in sealing `fetch`. It falls
|
|
85
|
+
back gracefully to plain `fetch`, filters origins, never sends a POST twice, provides `setAccount()`, refreshes the
|
|
86
|
+
lease in the background, defaults to `telemetry: 'minimal'` and lazy-loads the client core.
|
|
87
|
+
- **`PSecureConfigError`** with a stable `code`, an English `hint` and a `docsUrl`, for common misconfigurations
|
|
88
|
+
(public key missing, invalid or mismatched; keys swapped; non-https authority; CSP or unreachable authority; origins;
|
|
89
|
+
clock skew; raw body; server key missing or rejected).
|
|
90
|
+
- **`npx @pawmaskrelease/psecure doctor`**: checks that the authority is reachable, the pinned key, the clock, CORS,
|
|
91
|
+
a test lease with the app key and the server key (from `PSECURE_SERVER_KEY` only, never printed). Optionally checks
|
|
92
|
+
your API's CORS.
|
|
93
|
+
- `PSECURE_SDK_VERSION`.
|
|
94
|
+
|
|
95
|
+
### Changed
|
|
96
|
+
|
|
97
|
+
- The `PSecureClient.connect` lease failure error now carries `status` and the authority's SDK `code`
|
|
98
|
+
(`INVALID_APP_KEY`, `CROSS_TENANT_APP_KEY`, …), and names the code in its message.
|
|
99
|
+
- The `@pawmaskrelease/psecure/server` barrel also exports the guard and the Express and Edge adapters (the Fastify
|
|
100
|
+
plugin uses `node:stream` and has its own subpath, so `/server` stays edge-safe).
|
package/README.md
CHANGED
|
@@ -15,13 +15,97 @@ before it reaches your business logic.
|
|
|
15
15
|
> shipped in the public npm client package. `@pawmaskrelease/psecure/server` is backend-only, is
|
|
16
16
|
> not exported from the root barrel, and must never be bundled into browser code.
|
|
17
17
|
|
|
18
|
-
- Full docs: **https://pawmask.com/docs
|
|
18
|
+
- Full docs: **https://psecure.pawmask.com/docs**
|
|
19
19
|
- Proprietary software — use requires a valid Paw Mask PSecure subscription or sandbox
|
|
20
20
|
agreement. See [LICENSE](./LICENSE).
|
|
21
21
|
|
|
22
22
|
> 🇻🇳 **Lưu ý:** Đây là SDK phía client. Việc xác minh (Verify) chạy trên dịch vụ
|
|
23
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://pawmask.com/docs
|
|
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
|
|
25
109
|
|
|
26
110
|
## Install
|
|
27
111
|
|
|
@@ -40,7 +124,7 @@ Requirements: Node.js >= 20.16.0 or a modern browser (ES2023). Zero runtime depe
|
|
|
40
124
|
3. Your **backend** forwards the envelope to the PSecure **Verify API** with request
|
|
41
125
|
context; Verify returns allow / deny (and policy signals) which your backend enforces.
|
|
42
126
|
|
|
43
|
-
##
|
|
127
|
+
## `PSecureClient` in detail (web / browser)
|
|
44
128
|
|
|
45
129
|
### 1. One-call connect (`PSecureClient.connect`)
|
|
46
130
|
|
|
@@ -58,7 +142,7 @@ const client = await PSecureClient.connect({
|
|
|
58
142
|
authorityUrl: 'https://psecure-api.pawmask.com',
|
|
59
143
|
serverPublicKey: PSECURE_SERVER_PUBLIC_KEY, // REQUIRED (hex, PEM, KeyObject, or an array during key rotation)
|
|
60
144
|
sealOrigins: ['https://api.example.com'], // origins protectFetch may seal for (default: authority origin)
|
|
61
|
-
appKey: '
|
|
145
|
+
appKey: 'psec_app_…', // app key from the console (not a secret)
|
|
62
146
|
accountId,
|
|
63
147
|
// telemetry: 'minimal', onBeforeCollect: (ctx) => userHasConsented, // see "Privacy & consent"
|
|
64
148
|
});
|
|
@@ -202,69 +286,357 @@ What the SDK does:
|
|
|
202
286
|
|
|
203
287
|
### 3. Verify on your backend
|
|
204
288
|
|
|
205
|
-
|
|
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).
|
|
206
296
|
|
|
207
297
|
```ts
|
|
208
298
|
import {
|
|
209
299
|
PSecureRemoteAuthorityClient,
|
|
210
|
-
|
|
300
|
+
createPSecureExpressMiddleware,
|
|
211
301
|
createPSecureRemoteFetchEdgeMiddleware,
|
|
212
302
|
} from '@pawmaskrelease/psecure/server';
|
|
213
303
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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'],
|
|
218
323
|
});
|
|
219
324
|
|
|
220
|
-
|
|
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
|
|
221
337
|
app.use(
|
|
222
338
|
'/api',
|
|
223
|
-
|
|
224
|
-
authorityBaseUrl: 'https://psecure-api.pawmask.com',
|
|
225
|
-
authorityApiKey: process.env.PSECURE_SERVER_KEY!,
|
|
339
|
+
createPSecureExpressMiddleware(authority, {
|
|
226
340
|
origins: ['https://api.example.com'],
|
|
341
|
+
excludePaths: ['/api/health'],
|
|
227
342
|
}),
|
|
228
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, let denied requests
|
|
358
|
+
through), **Block** once the reports look clean. The console Monitor/Block switch of the app drives this:
|
|
359
|
+
|
|
360
|
+
- **1.1 adapters** (`psecureFastify`, `psecure()`, `withPSecure`, `createPSecureGuard`) in the default mode `auto`
|
|
361
|
+
follow the switch on the next request, no redeploy (see [Monitor / Block](#monitor--block-mode-auto)).
|
|
362
|
+
- **This lower-level verifier** returns the verdict only; the decision to refuse is your code. The authority (>= 1.1)
|
|
363
|
+
reports the switch on every verdict as `verdict.appMode` (`'monitor' | 'block'`, with `verdict.appKey`): branch on
|
|
364
|
+
it, e.g. refuse when `!verdict.allowed && verdict.appMode === 'block'`.
|
|
365
|
+
- **Pre-1.1 middlewares** (`createPSecureExpressMiddleware`, `createPSecureRemoteFetchEdgeMiddleware`) ignore
|
|
366
|
+
`appMode`: they refuse every denied request whatever the switch says. Upgrade to the 1.1 adapters to follow it.
|
|
367
|
+
|
|
368
|
+
The verdict itself (`allowed`, `action`, `reason`) does not depend on the switch. `verdict.shadowWouldHaveAction` is
|
|
369
|
+
set only when the authority deployment itself runs in shadow mode. See
|
|
370
|
+
https://psecure.pawmask.com/docs/backend/rollout.
|
|
371
|
+
|
|
372
|
+
The HTTP contract of the authority (including `POST /v1/request/verify`) is published as OpenAPI 3.1 in
|
|
373
|
+
`server/api/openapi.json` of the repository.
|
|
229
374
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
375
|
+
## Server adapters (1.1)
|
|
376
|
+
|
|
377
|
+
The Fastify plugin, the Express middleware and the Fetch/Edge wrapper share one guard (`createPSecureGuard`, also
|
|
378
|
+
exported for custom integrations). Types are structural: the SDK does not depend on any framework.
|
|
379
|
+
|
|
380
|
+
### Options
|
|
381
|
+
|
|
382
|
+
Every option falls back to an environment variable, so `psecureFastify` / `psecure()` / `withPSecure` work with no
|
|
383
|
+
arguments.
|
|
384
|
+
|
|
385
|
+
| Option | Env | Default | |
|
|
386
|
+
| --- | --- | --- | --- |
|
|
387
|
+
| `serverKey` | `PSECURE_SERVER_KEY` | (required unless `mode: 'off'`) | API token `psec_pat_…`. Never logged. |
|
|
388
|
+
| `origins` | `PSECURE_ORIGINS` (comma separated) | (required) | Public origin(s) your browsers call this API on. The client seals for them. |
|
|
389
|
+
| `mode` | `PSECURE_MODE` | `'auto'` | `auto` follows the console switch; `monitor` / `enforce` / `off` override it. |
|
|
390
|
+
| `authorityUrl` | `PSECURE_AUTHORITY_URL` | `https://psecure-api.pawmask.com` | https only (http for loopback). |
|
|
391
|
+
| `timeoutMs` | `PSECURE_TIMEOUT_MS` | `1500` | Authority call timeout. |
|
|
392
|
+
| `modeFallback` | `PSECURE_MODE_FALLBACK` | `'monitor'` | Mode `auto` applies while the console mode is unknown (cold start, cached mode expired). `'enforce'` for apps in Block: see [Monitor / Block](#monitor--block-mode-auto). |
|
|
393
|
+
| `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`). |
|
|
394
|
+
| `protect` | | `'all'` | Routes that fail closed (see below). Default: **every** guarded route; for per-route opt-in pass `protect: []` and mark routes (Fastify route config `{ psecure: true }`, or matchers). Matchers: `'/pay'`, `'/api/*'`, `'/orders/:id'`, `'POST /pay'`, `RegExp`, `(info) => boolean`. |
|
|
395
|
+
| `skip` | | `[]` | Routes never checked (health checks, webhooks). `OPTIONS` is always skipped. |
|
|
396
|
+
| `onDecision` | | | `(decision, { method, path, route, ip }) => void` for metrics / audit. `createPSecureAuditThrottle({ sink })` turns it into deduplicated audit events (see [Audit events](#audit-events)). |
|
|
397
|
+
| `logger` | | console | pino-compatible `{ info, warn, debug? }` (Fastify uses `app.log`); `false` disables. |
|
|
398
|
+
| `blockedResponse` | | `403 { error: 'PSECURE_REJECTED' }` | `(decision) => ({ status?, body })`: your API's error shape for refused requests. Without `status` the SDK's status is kept: **401** for a reissued lease (the client needs it to adopt the lease and retry), else 403. Keep `error: 'PSECURE_REJECTED'` in the body if you use `createPSecureFetch`: it refreshes the lease on that answer. |
|
|
399
|
+
|
|
400
|
+
Fastify extras: route config `{ psecure: true }` protects a route, `{ psecure: false }` skips it, `{ psecureAppKey }`
|
|
401
|
+
pins the route to another app. Express extras:
|
|
402
|
+
`maxBodyBytes` (default 1 MiB). Edge extras: `maxBodyBytes`, `clientIp(request)`; `options` may be a function of the
|
|
403
|
+
handler's extra arguments (Cloudflare Workers `env`).
|
|
404
|
+
|
|
405
|
+
**Per-route opt-in.** By default every guarded route is fail-closed in Block. To protect only some routes:
|
|
406
|
+
|
|
407
|
+
```ts
|
|
408
|
+
await app.register(psecureFastify, { protect: [] }); // nothing fails closed by default…
|
|
409
|
+
app.post('/v1/pay', { config: { psecure: true } }, handler); // …except the routes you mark
|
|
410
|
+
app.post('/v1/webhook', { config: { psecure: false } }, handler); // never checked
|
|
411
|
+
// Express / Edge: psecure({ protect: ['POST /v1/pay', '/v1/orders/*'] })
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
Denied and malformed envelopes are still refused on every guarded route in Block; `protect` only decides where a
|
|
415
|
+
**missing** envelope or an unreachable authority is refused.
|
|
416
|
+
|
|
417
|
+
**Fastify with TypeScript.** `app.register(psecureFastify, { onDecision, blockedResponse })` types the callbacks
|
|
418
|
+
(1.1.1; in 1.1.0 they were implicitly `any` under `strict`). Add the augmentation once to type `request.psecure`
|
|
419
|
+
(`PSecureDecision | null`) and the route config:
|
|
420
|
+
|
|
421
|
+
```ts
|
|
422
|
+
import '@pawmaskrelease/psecure/fastify/augment'; // types only (needs fastify 4 or 5 in your project)
|
|
423
|
+
// or, equivalently:
|
|
424
|
+
// declare module 'fastify' {
|
|
425
|
+
// interface FastifyRequest extends PSecureFastifyRequestDecoration {}
|
|
426
|
+
// interface FastifyContextConfig extends PSecureFastifyRouteConfig {}
|
|
427
|
+
// }
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
### Decision table
|
|
431
|
+
|
|
432
|
+
| Outcome | Monitor | Block (enforce), `protect` route | Block, other route |
|
|
433
|
+
| --- | --- | --- | --- |
|
|
434
|
+
| verified | served | served | served |
|
|
435
|
+
| denied (tampered, replayed, forged, banned, lease of another app) or malformed envelope | served, logged | **403** | **403** |
|
|
436
|
+
| lease must be renewed, with a reissued lease | served + `x-psecure-reissued-lease` | **401** + `x-psecure-reissued-lease` | **401** + header |
|
|
437
|
+
| no envelope | served | **403** | served |
|
|
438
|
+
| authority down / timeout / key rejected | served | **403** (fail-closed) | served |
|
|
439
|
+
|
|
440
|
+
`createPSecureFetch` adopts a reissued lease on the 401 and retries once. Each decision is available as
|
|
441
|
+
`request.psecure` (Fastify), `req.psecure` / `res.locals.psecure` (Express), or `getPSecureDecision(request)` (all
|
|
442
|
+
adapters, typed): `{ verified, result, action, reason, effectiveMode, modeSource, blocked, wouldBlock,
|
|
443
|
+
envelopeAccountId, ... }`. `envelopeAccountId` is what the client claims: compare it with your session user before
|
|
444
|
+
trusting it.
|
|
445
|
+
|
|
446
|
+
### Monitor / Block (`mode: 'auto'`)
|
|
447
|
+
|
|
448
|
+
The authority reports the app's console mode on every verdict (`appMode`). In `auto` the guard blocks only while the
|
|
449
|
+
app is in **Block**. When a request carries no verdict (no envelope, authority unreachable), the guard uses the last
|
|
450
|
+
mode it saw for the app, for up to 10 minutes; with nothing known yet (cold start) it behaves as Monitor. Trade-off:
|
|
451
|
+
right after a restart during an authority outage, unsealed requests are served until the first verdict arrives (a
|
|
452
|
+
cold start is Monitor), and the same happens when the cached mode expired during a long outage. Where that matters
|
|
453
|
+
more than availability:
|
|
454
|
+
|
|
455
|
+
- `PSECURE_MODE=enforce` pins Block whatever the console says (the switch is then ignored), or
|
|
456
|
+
- `PSECURE_MODE_FALLBACK=enforce` (`modeFallback: 'enforce'`) keeps following the switch but applies Block while the
|
|
457
|
+
console mode is unknown. A verdict that carries the console mode always wins. Default `monitor`.
|
|
458
|
+
|
|
459
|
+
Set `PSECURE_APP_KEY`. Without it, the guard follows the mode of whichever app a lease belongs to, so a lease of one
|
|
460
|
+
of your Monitor apps replayed against a Block app's backend would only be logged; the guard warns once at startup
|
|
461
|
+
(`APP_KEY_NOT_PINNED`). With it, that lease is denied (`reason: 'app_mismatch'`) and refused in Block, and the guard
|
|
462
|
+
trusts a verdict's `appMode` only when the verdict's `appKey` equals the pinned key; any other verdict is treated as
|
|
463
|
+
carrying no mode (cache, then `modeFallback`). Each decision says which mode applied (`effectiveMode`) and why
|
|
464
|
+
(`modeSource`: `console`, `cache`, `default` or `config`).
|
|
465
|
+
|
|
466
|
+
### Audit events
|
|
467
|
+
|
|
468
|
+
```ts
|
|
469
|
+
import { createPSecureAuditThrottle } from '@pawmaskrelease/psecure/fastify'; // also /express, /edge, /server
|
|
470
|
+
const audit = createPSecureAuditThrottle({ sink: (event) => auditLog.write(event) });
|
|
471
|
+
await app.register(psecureFastify, { onDecision: audit.onDecision });
|
|
472
|
+
app.addHook('onClose', () => audit.close());
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
The sink (`PSecureAuditSink`, `(event) => void | Promise<void>`) receives a `PSecureGuardAuditEvent`; it is never
|
|
476
|
+
awaited by the request path and its errors are swallowed. Events never contain the key, envelope, lease or body:
|
|
477
|
+
|
|
478
|
+
- `{ type: 'psecure.reject', result, action, reason, route, method, ip? }`: a refused request, emitted at once.
|
|
479
|
+
Identical refusals (result, reason, route, ip) are emitted once per `rejectCooldownMs` (default 60 s).
|
|
480
|
+
- `{ type: 'psecure.monitor_summary', windowStart, windowEnd, wouldBlock, distinctIps, distinctIpsCapped, buckets }`:
|
|
481
|
+
what Monitor would have refused in a window (`summaryWindowMs`, default 10 min); `buckets` is
|
|
482
|
+
`[{ result, reason, route, count }]`, top 50 by count.
|
|
483
|
+
|
|
484
|
+
`flush()` emits the pending summary now (nothing if the window is empty) and starts a new window; refusals are never
|
|
485
|
+
buffered. `close()` stops the timer and **flushes**, then ignores later decisions; it is idempotent. Both return a
|
|
486
|
+
promise that settles when the sink finished (`await audit.close()` before exiting).
|
|
487
|
+
|
|
488
|
+
### Testing with a stubbed authority
|
|
489
|
+
|
|
490
|
+
Point `authorityUrl` at a local stub (http is accepted on loopback) or pass `fetch`. The guard calls
|
|
491
|
+
`POST {authorityUrl}/psecure.authority.v1.PSecureAuthorityService/VerifySealedRequest` and needs only:
|
|
492
|
+
|
|
493
|
+
```json
|
|
494
|
+
{ "allowed": true, "action": "ALLOW", "reason": "ok" }
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
A denial is `{ "allowed": false, "action": "STEP_UP_CHALLENGE", "reason": "verification_failed" }`; a reissue is
|
|
498
|
+
`{ "allowed": false, "action": "REISSUE_LEASE", "reason": "reissue_required", "reissuedLease": { "compactToken": "…" } }`.
|
|
499
|
+
To exercise `auto`, add `"appMode": "block"` (or `"monitor"`) and, when the guard is pinned,
|
|
500
|
+
`"appKey": "<the pinned PSECURE_APP_KEY>"`: without a matching `appKey` the mode is ignored. Requests still need a
|
|
501
|
+
well-formed `x-psecure-envelope` (from a real client, or `sealWirePayloadB64Url` from
|
|
502
|
+
`@pawmaskrelease/psecure/protocol`); without one the guard does not call the authority (`result: 'missing'`). For
|
|
503
|
+
tests that need no verification at all, `PSECURE_MODE=off`.
|
|
504
|
+
|
|
505
|
+
### Raw body
|
|
506
|
+
|
|
507
|
+
PSecure hashes the exact bytes the client sealed, so the adapters need the raw body:
|
|
508
|
+
|
|
509
|
+
- **Fastify** captures it itself (a `preParsing` tap: works with every content-type parser, never parses twice). An
|
|
510
|
+
existing `request.rawBody` is used as is.
|
|
511
|
+
- **Express**: `express.json({ verify: psecureRawBody })` (same for `express.text` / `raw` / `urlencoded`), or mount
|
|
512
|
+
`psecure()` before the body parser: it then reads and parses the body once and the parser skips it. A body consumed
|
|
513
|
+
without its bytes is reported as `raw_body_unavailable` (and refused in Block).
|
|
514
|
+
- **Edge**: read from a clone; your handler gets an unread `request`.
|
|
515
|
+
|
|
516
|
+
### CORS
|
|
517
|
+
|
|
518
|
+
```ts
|
|
519
|
+
import { PSECURE_REQUEST_HEADERS, PSECURE_EXPOSED_HEADERS } from '@pawmaskrelease/psecure/fastify';
|
|
520
|
+
await app.register(cors, {
|
|
521
|
+
origin: ['https://app.example.com'],
|
|
522
|
+
allowedHeaders: ['content-type', 'authorization', ...PSECURE_REQUEST_HEADERS],
|
|
523
|
+
exposedHeaders: [...PSECURE_EXPOSED_HEADERS],
|
|
234
524
|
});
|
|
525
|
+
```
|
|
235
526
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
},
|
|
254
|
-
clientIp: req.ip,
|
|
255
|
-
userAgent: req.headers['user-agent'],
|
|
256
|
-
}),
|
|
527
|
+
### Recipes
|
|
528
|
+
|
|
529
|
+
**NestJS** (no Nest dependency): on Express, `const app = await NestFactory.create(AppModule, { rawBody: true });
|
|
530
|
+
app.use(psecure());` (Nest's `rawBody: true` fills `req.rawBody`). On Fastify, `NestFactory.create(AppModule, new
|
|
531
|
+
FastifyAdapter())` then `await app.register(psecureFastify)`. A Nest project compiled to CommonJS needs Node.js
|
|
532
|
+
>= 20.19 or >= 22.12 (`require()` of an ES module).
|
|
533
|
+
|
|
534
|
+
**Hono**:
|
|
535
|
+
|
|
536
|
+
```ts
|
|
537
|
+
import { createPSecureEdge } from '@pawmaskrelease/psecure/edge';
|
|
538
|
+
const ps = createPSecureEdge();
|
|
539
|
+
app.use('/api/*', async (c, next) => {
|
|
540
|
+
const { decision, response } = await ps.check(c.req.raw);
|
|
541
|
+
if (response) return response;
|
|
542
|
+
await next();
|
|
543
|
+
c.res = ps.finish(c.res, decision);
|
|
257
544
|
});
|
|
258
|
-
const result = await verify.json(); // { allowed, action, reason, ... }
|
|
259
545
|
```
|
|
260
546
|
|
|
547
|
+
**Cloudflare Workers**: `export default { fetch: withPSecure(handler, (env) => ({ serverKey: env.PSECURE_SERVER_KEY,
|
|
548
|
+
origins: env.PSECURE_ORIGINS, clientIp: (r) => r.headers.get('cf-connecting-ip') ?? undefined })) };`
|
|
549
|
+
|
|
550
|
+
## Client helper: `createPSecureFetch` (1.1)
|
|
551
|
+
|
|
552
|
+
`@pawmaskrelease/psecure/fetch` wraps `PSecureClient` for apps that just want a sealing `fetch`:
|
|
553
|
+
|
|
554
|
+
| Option | Default | |
|
|
555
|
+
| --- | --- | --- |
|
|
556
|
+
| `appKey`, `serverPublicKey`, `apiOrigins` | (required) | Public values; `serverPublicKey` may be an array during a key rotation. Only `apiOrigins` are sealed. |
|
|
557
|
+
| `authorityUrl` | `https://psecure-api.pawmask.com` | |
|
|
558
|
+
| `enabled` | `true` | Rollout switch: `false` = plain fetch, SDK never loaded. |
|
|
559
|
+
| `accountId` | `null` | Or later `setAccount(id \| null)`. |
|
|
560
|
+
| `telemetry`, `onBeforeCollect` | `'minimal'` | See [Privacy & consent](#privacy--consent). |
|
|
561
|
+
| `waitForConnectMs` | `2000` | How long the first requests wait for the lease before going out unsealed. |
|
|
562
|
+
| `onError`, `strict` | | Called once per problem; `strict: true` throws configuration errors instead. |
|
|
563
|
+
|
|
564
|
+
Returns `{ fetch, setAccount, status, lastError, ready, start, dispose }`. `status()` is `disabled`,
|
|
565
|
+
`misconfigured`, `idle`, `connecting`, `ready` or `failed`. Behaviour:
|
|
566
|
+
|
|
567
|
+
- Graceful fallback: requests go out unsealed while PSecure is disabled, misconfigured, connecting or failing
|
|
568
|
+
(retried with backoff from 30 s to 5 min). A request that already left and failed on the network is rethrown,
|
|
569
|
+
never re-sent unsealed (no duplicated POST).
|
|
570
|
+
- The lease is refreshed in the background when it expires or the API answers `PSECURE_REJECTED`.
|
|
571
|
+
- The SDK core is loaded lazily on the first sealed request (a dynamic import your bundler puts in its own chunk).
|
|
572
|
+
The eager part of `@pawmaskrelease/psecure/fetch` is about 16 KiB gzip; the lazy core about 112 KiB gzip (measured
|
|
573
|
+
on 1.1.0, before your bundler's minification).
|
|
574
|
+
- `ready()` resolves with the outcome of the connect or lease refresh in flight: `ready`, or `failed` when the latest
|
|
575
|
+
lease acquisition failed (including a failed background refresh, 1.1.1); the next successful lease returns to
|
|
576
|
+
`ready`.
|
|
577
|
+
- Browser CSP: add the authority origin to `connect-src` (e.g. `connect-src 'self' https://api.example.com
|
|
578
|
+
https://psecure-api.pawmask.com`). A blocked request is reported as `CSP_BLOCKED`.
|
|
579
|
+
|
|
580
|
+
**Web apps.** Call `setAccount(user.id)` after sign-in and `setAccount(null)` after sign-out: the lease is bound to the
|
|
581
|
+
account and re-acquired (calling it before the first request only records the account). Use `enabled` as the rollout
|
|
582
|
+
switch (`enabled: false` = plain `fetch`, the core is never loaded). The core is loaded on the first sealed request or
|
|
583
|
+
`start()`; call `start()` early (e.g. after the first render) to hide the lease latency.
|
|
584
|
+
|
|
585
|
+
To keep even the eager ~16 KiB out of a build where PSecure is disabled, import the helper dynamically behind your
|
|
586
|
+
flag; your bundler then splits it out and never loads it:
|
|
587
|
+
|
|
588
|
+
```ts
|
|
589
|
+
let apiFetch: typeof fetch = fetch;
|
|
590
|
+
if (import.meta.env.VITE_PSECURE_ENABLED === 'true') {
|
|
591
|
+
const { createPSecureFetch } = await import('@pawmaskrelease/psecure/fetch');
|
|
592
|
+
apiFetch = createPSecureFetch({ appKey, serverPublicKey, apiOrigins: ['https://api.example.com'] }).fetch;
|
|
593
|
+
}
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
## Errors
|
|
597
|
+
|
|
598
|
+
`PSecureConfigError` (`code`, `hint`, `docsUrl`) is thrown at startup by the server adapters and reported by
|
|
599
|
+
`createPSecureFetch` (`lastError()`, `onError`). Runtime problems the guard can only see per request are logged once
|
|
600
|
+
as `psecure config:` lines with the same codes.
|
|
601
|
+
|
|
602
|
+
| Code | Typical cause |
|
|
603
|
+
| --- | --- |
|
|
604
|
+
| `SERVER_KEY_MISSING` | `PSECURE_SERVER_KEY` not set (set `PSECURE_MODE=off` to run without PSecure) |
|
|
605
|
+
| `SERVER_KEY_IS_APP_KEY`, `SERVER_KEY_IN_CLIENT` | app key and API token swapped |
|
|
606
|
+
| `SERVER_KEY_REJECTED` | token revoked/expired, or of another organization (authority answered 401/403) |
|
|
607
|
+
| `PUBLIC_KEY_MISSING`, `PUBLIC_KEY_INVALID`, `PUBLIC_KEY_MISMATCH` | pinned key absent, malformed, or not the authority's key |
|
|
608
|
+
| `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 |
|
|
609
|
+
| `ORIGINS_MISSING`, `ORIGIN_INVALID`, `ORIGIN_MISMATCH` | `PSECURE_ORIGINS` / `apiOrigins` absent, malformed, or not the same on both sides |
|
|
610
|
+
| `AUTHORITY_INSECURE`, `AUTHORITY_INVALID`, `AUTHORITY_UNREACHABLE`, `CSP_BLOCKED` | http authority, bad URL, network/DNS, CSP `connect-src` |
|
|
611
|
+
| `CLOCK_SKEW` | the device or server clock is off |
|
|
612
|
+
| `RAW_BODY_MISSING` | the body parser dropped the raw bytes |
|
|
613
|
+
| `MODE_INVALID` | `PSECURE_MODE` is not `auto`, `monitor`, `enforce` or `off` (or `PSECURE_MODE_FALLBACK` not `monitor` / `enforce`) |
|
|
614
|
+
|
|
615
|
+
## `doctor` CLI
|
|
616
|
+
|
|
617
|
+
```bash
|
|
618
|
+
npx @pawmaskrelease/psecure doctor [--authority <url>] [--app-key psec_app_…] [--public-key <hex>]
|
|
619
|
+
[--origin https://app.example.com] [--api https://api.example.com/health] [--json]
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
Flags fall back to `PSECURE_AUTHORITY_URL`, `PSECURE_APP_KEY`, `PSECURE_PUBLIC_KEY` and `PSECURE_WEB_ORIGIN` (the
|
|
623
|
+
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
|
|
624
|
+
not land in shell history) and is never printed: `doctor` proves it by sending a deliberately unsealed verify request,
|
|
625
|
+
which the authority must answer with a denial. With `PSECURE_APP_KEY` set, the same call proves that key is an app of
|
|
626
|
+
the token's organization; unset, `doctor` warns. Exit code 1 when a check fails.
|
|
627
|
+
|
|
261
628
|
## Entry points
|
|
262
629
|
|
|
263
630
|
| Import | What it provides |
|
|
264
631
|
| --- | --- |
|
|
265
632
|
| `@pawmaskrelease/psecure` | Client SDK barrel (protocol, crypto, vm, rasp, client, adapters); no server code |
|
|
633
|
+
| `@pawmaskrelease/psecure/fetch` | `createPSecureFetch` (sealing `fetch` with fallback; lazy-loads the client core) |
|
|
634
|
+
| `@pawmaskrelease/psecure/fastify` | **Backend.** `psecureFastify` plugin, `PSECURE_REQUEST_HEADERS`, `PSECURE_EXPOSED_HEADERS`, `createPSecureAuditThrottle` |
|
|
635
|
+
| `@pawmaskrelease/psecure/fastify/augment` | Types only: `request.psecure` and route config `{ psecure, psecureAppKey }` on Fastify's types |
|
|
636
|
+
| `@pawmaskrelease/psecure/express` | **Backend.** `psecure()` middleware, `psecureRawBody`, `createPSecureAuditThrottle` |
|
|
637
|
+
| `@pawmaskrelease/psecure/edge` | **Backend / Edge.** `withPSecure`, `createPSecureEdge` (Next.js, Hono, Workers, Bun, Deno) |
|
|
266
638
|
| `@pawmaskrelease/psecure/client` | `PSecureClient` (`connect` / `sealRequest` / `sealRequestAsync` / `protectFetch` / `dispose`) |
|
|
267
|
-
| `@pawmaskrelease/psecure/server` | **Backend / Edge only.** Remote Authority client & verification middlewares: `PSecureRemoteAuthorityClient`, `createExpressMiddleware`, `createPSecureExpressMiddleware`, `createPSecureRemoteExpressMiddleware`, `createPSecureFetchEdgeMiddleware`, `createPSecureRemoteFetchEdgeMiddleware
|
|
639
|
+
| `@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.) |
|
|
268
640
|
| `@pawmaskrelease/psecure/protocol` | Wire format, codecs, lease verification, constants, types |
|
|
269
641
|
| `@pawmaskrelease/psecure/crypto` | Crypto primitives & ciphers |
|
|
270
642
|
| `@pawmaskrelease/psecure/vm` | `PSecureVm` bytecode VM + polymorphic bundler |
|
package/bin/psecure.mjs
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `npx @pawmaskrelease/psecure doctor` — see `psecure --help`.
|
|
3
|
+
const [major] = process.versions.node.split('.').map(Number);
|
|
4
|
+
if (major < 20) {
|
|
5
|
+
console.error(`@pawmaskrelease/psecure needs Node.js >= 20 (this is ${process.versions.node}).`);
|
|
6
|
+
process.exit(1);
|
|
7
|
+
}
|
|
8
|
+
const { runPSecureCli } = await import('../dist/cli/doctor.js');
|
|
9
|
+
process.exitCode = await runPSecureCli(process.argv.slice(2), {
|
|
10
|
+
stdout: (t) => console.log(t),
|
|
11
|
+
stderr: (t) => console.error(t),
|
|
12
|
+
env: process.env,
|
|
13
|
+
});
|
package/dist/adapters/desktop.js
CHANGED
|
@@ -55,4 +55,4 @@ dispose(){if(this.#_0x1dd12b)
|
|
|
55
55
|
return;this.#_0x1dd12b=true;this.#_0x557f55.fill(0);this.#_0xd63721.clear();this.#_0x32df7f.dispose();}}
|
|
56
56
|
export async function sealDesktopRequestAsync(_0x53637e,_0x02eef0){if(_0x53637e instanceof PSecureDesktopBridge){return _0x53637e.sealDesktopRequestAsync(_0x02eef0);}
|
|
57
57
|
return _0x53637e.sealRequestAsync({[_0xc9bd50_S(26)]:_0x02eef0.method,[_0xc9bd50_S(27)]:_0x02eef0.path,[_0xc9bd50_S(28)]:_0x02eef0.origin,[_0xc9bd50_S(29)]:_0x02eef0.body,[_0xc9bd50_S(30)]:_0x02eef0.nowMs,});}
|
|
58
|
-
/* @psec-self-seal sha256=
|
|
58
|
+
/* @psec-self-seal sha256=2a8c0aa9c667aa603e5f3b3a4cf29077637dc0fb7a64c87c5edf46e6766b9daf fnv1a=dd197b03 */
|
package/dist/adapters/index.js
CHANGED
|
@@ -3,4 +3,4 @@ export * from "./mobile-bridge.js";
|
|
|
3
3
|
export * from "./react-native.js";
|
|
4
4
|
export * from "./desktop.js";
|
|
5
5
|
|
|
6
|
-
/* @psec-self-seal sha256=
|
|
6
|
+
/* @psec-self-seal sha256=6711c3a559952f6bcf55039df4e99df9f78f11aa2db94ff9d53f19950930e50c fnv1a=dd197b03 */
|