@agnostack/verifyd 2.4.1-alpha.1 → 2.5.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -64
- package/dist/esm/lib/index.js +3 -0
- package/dist/esm/lib/types.js +1 -0
- package/dist/esm/lib/utils/index.js +1 -0
- package/dist/esm/lib/utils/rawbody.js +35 -0
- package/dist/esm/lib/verification.js +94 -0
- package/dist/esm/react/hooks/index.js +1 -0
- package/dist/esm/react/hooks/useVerification.js +47 -0
- package/dist/esm/react/index.js +2 -0
- package/dist/esm/react/types.js +0 -0
- package/dist/esm/shared/WebCrypto.js +350 -0
- package/dist/esm/shared/authorization.js +26 -0
- package/dist/esm/shared/display.js +429 -0
- package/dist/esm/shared/errors.js +37 -0
- package/dist/esm/shared/index.js +6 -0
- package/dist/esm/shared/request.js +60 -0
- package/dist/esm/shared/types.js +0 -0
- package/dist/esm/shared/verification.js +94 -0
- package/package.json +21 -7
package/README.md
CHANGED
|
@@ -1,95 +1,106 @@
|
|
|
1
1
|
# @agnostack/verifyd
|
|
2
|
+
|
|
2
3
|

|
|
4
|
+
|
|
5
|
+
Cryptographic verification and encryption library for agnoStack platform integrations. Provides ECDH key exchange, HMAC verification, AES-GCM encryption/decryption, and related utilities.
|
|
6
|
+
|
|
3
7
|
## Installation
|
|
4
8
|
|
|
5
9
|
```bash
|
|
6
10
|
yarn add @agnostack/verifyd
|
|
7
|
-
#
|
|
11
|
+
# or
|
|
12
|
+
npm install @agnostack/verifyd
|
|
8
13
|
```
|
|
9
14
|
|
|
10
|
-
##
|
|
15
|
+
## Export Paths
|
|
11
16
|
|
|
12
|
-
|
|
17
|
+
| Path | Format | Use Case |
|
|
18
|
+
|------|--------|----------|
|
|
19
|
+
| `@agnostack/verifyd` | CJS | Default — works everywhere (Node.js, bundlers) |
|
|
20
|
+
| `@agnostack/verifyd/esm` | ESM | Tree-shakeable — for modern bundlers (Vite, esbuild, Webpack 5+) |
|
|
21
|
+
| `@agnostack/verifyd/react` | CJS | React hooks (`useVerification`) |
|
|
22
|
+
| `@agnostack/verifyd/react/esm` | ESM | Tree-shakeable React hooks |
|
|
23
|
+
| `@agnostack/verifyd/external` | UMD | Script tag / CDN usage |
|
|
13
24
|
|
|
14
|
-
|
|
15
|
-
```js
|
|
16
|
-
const { withShopify } = require('@agnostack/verifyd')
|
|
25
|
+
### Tree-Shaking (ESM)
|
|
17
26
|
|
|
18
|
-
|
|
27
|
+
The `/esm` subpaths enable bundlers to eliminate unused code. For example, importing only `{ WebCrypto }` from `@agnostack/verifyd/esm` produces a ~62% smaller bundle compared to the CJS export (unused utilities like `display.js` are fully eliminated).
|
|
19
28
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
29
|
+
```js
|
|
30
|
+
// CJS (no tree-shaking)
|
|
31
|
+
import { WebCrypto } from '@agnostack/verifyd'
|
|
23
32
|
|
|
33
|
+
// ESM (tree-shakeable)
|
|
34
|
+
import { WebCrypto } from '@agnostack/verifyd/esm'
|
|
24
35
|
```
|
|
25
36
|
|
|
26
|
-
|
|
27
|
-
```js
|
|
28
|
-
import getConfig from 'next/config'
|
|
29
|
-
import { withShopify } from '@agnostack/verifyd'
|
|
37
|
+
## Web Crypto Resolution
|
|
30
38
|
|
|
31
|
-
|
|
39
|
+
`WebCrypto` uses a layered resolution strategy to find a Web Crypto API implementation:
|
|
32
40
|
|
|
33
|
-
|
|
34
|
-
|
|
41
|
+
1. **Constructor-injected** — if you pass `{ crypto }` to `new WebCrypto({ crypto })`, that's used directly
|
|
42
|
+
2. **`globalThis.crypto.subtle`** — native Web Crypto, available in all modern browsers and Node.js 18+
|
|
43
|
+
3. **`isomorphic-webcrypto`** polyfill — fallback for older Node.js environments (see below)
|
|
44
|
+
4. **Node.js `crypto` module** — last resort fallback
|
|
45
|
+
|
|
46
|
+
Each stage exits early on success — later fallbacks are only reached if all earlier checks fail.
|
|
47
|
+
|
|
48
|
+
### `isomorphic-webcrypto` (Optional Peer Dependency)
|
|
49
|
+
|
|
50
|
+
`isomorphic-webcrypto` is an **optional** peer dependency. Most consumers do not need to install it:
|
|
51
|
+
|
|
52
|
+
- **Browser**: `globalThis.crypto.subtle` is always available natively — the polyfill is never reached
|
|
53
|
+
- **Node.js 18+**: `globalThis.crypto.subtle` is available natively — the polyfill is never reached
|
|
54
|
+
- **Node.js <15**: `globalThis.crypto` is not available — install the polyfill if you are not passing `crypto` to the `WebCrypto` constructor:
|
|
35
55
|
|
|
56
|
+
```bash
|
|
57
|
+
yarn add isomorphic-webcrypto
|
|
58
|
+
# or
|
|
59
|
+
npm install isomorphic-webcrypto
|
|
60
|
+
```
|
|
36
61
|
|
|
37
|
-
|
|
62
|
+
> **Note**: `isomorphic-webcrypto` transitively pulls in `expo` and `react-native` as optional dependencies, which is why it is not included as a direct dependency of this package.
|
|
63
|
+
|
|
64
|
+
## Usage
|
|
65
|
+
|
|
66
|
+
### WebCrypto — Encryption / Decryption
|
|
38
67
|
|
|
39
|
-
Inside of `next.config.js`, add the following:
|
|
40
68
|
```js
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
interactive: true, // (defaults to false)
|
|
52
|
-
data: {
|
|
53
|
-
plan: 'silver',
|
|
54
|
-
app_id: 123,
|
|
55
|
-
installation_id: 12434234,
|
|
56
|
-
my_token: 'myValue',
|
|
57
|
-
parameters: {
|
|
58
|
-
someToken: 'fksjdhfb231435',
|
|
59
|
-
someSecret: 123,
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
routes: {
|
|
63
|
-
background: '/background'
|
|
64
|
-
user_sidebar: '/noTicket',
|
|
65
|
-
organization_sidebar: '/noTicket',
|
|
66
|
-
ticket_sidebar: '/ticket',
|
|
67
|
-
new_ticket_sidebar: '/ticket',
|
|
68
|
-
},
|
|
69
|
-
*/
|
|
70
|
-
},
|
|
71
|
-
}]
|
|
72
|
-
|
|
73
|
-
const nextConfig = withPlugins({
|
|
74
|
-
/* NOTE: standard nextConfig goes in here
|
|
75
|
-
reactStrictMode: true,
|
|
76
|
-
experimental: {
|
|
77
|
-
esmExternals: false,
|
|
78
|
-
},
|
|
79
|
-
*/
|
|
80
|
-
}, [nextPlugins])
|
|
69
|
+
import { WebCrypto } from '@agnostack/verifyd'
|
|
70
|
+
|
|
71
|
+
// Option 1: Let WebCrypto resolve crypto automatically
|
|
72
|
+
const webCrypto = new WebCrypto()
|
|
73
|
+
|
|
74
|
+
// Option 2: Pass native crypto explicitly (recommended for known environments)
|
|
75
|
+
const webCrypto = new WebCrypto({ crypto: globalThis.crypto })
|
|
76
|
+
|
|
77
|
+
// Encrypt
|
|
78
|
+
const encrypted = await webCrypto.encryptMessage('secret data', cryptoKey)
|
|
81
79
|
|
|
80
|
+
// Decrypt
|
|
81
|
+
const decrypted = await webCrypto.decryptMessage(encrypted, cryptoKey)
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
### Verification Helpers (Server-Side)
|
|
85
|
+
|
|
85
86
|
```js
|
|
86
|
-
import
|
|
87
|
-
|
|
87
|
+
import { getVerificationHelpers } from '@agnostack/verifyd'
|
|
88
|
+
|
|
89
|
+
const {
|
|
90
|
+
generateStorableKeyPairs,
|
|
91
|
+
prepareVerificationRequest,
|
|
92
|
+
processVerificationResponse,
|
|
93
|
+
} = getVerificationHelpers()
|
|
94
|
+
```
|
|
88
95
|
|
|
89
|
-
|
|
96
|
+
### React Hook
|
|
97
|
+
|
|
98
|
+
```js
|
|
99
|
+
import { useVerification } from '@agnostack/verifyd/react'
|
|
90
100
|
|
|
91
|
-
|
|
101
|
+
const { verify, isVerified, error } = useVerification(options)
|
|
92
102
|
```
|
|
93
103
|
|
|
104
|
+
---
|
|
94
105
|
|
|
95
106
|
_Contact [Adam Grohs](https://agnostack.com/founding-team/adam-grohs) @ [agnoStack](https://agnostack.com/) for any questions._
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './rawbody';
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { lowercase } from '../../shared/display';
|
|
11
|
+
const getChunkedRawBody = (req) => __awaiter(void 0, void 0, void 0, function* () {
|
|
12
|
+
if (req === null || req === void 0 ? void 0 : req.rawBody) {
|
|
13
|
+
return req.rawBody;
|
|
14
|
+
}
|
|
15
|
+
// TODO: move to req.text() after next 13.5: https://github.com/vercel/next.js/discussions/13405
|
|
16
|
+
try {
|
|
17
|
+
const _getRawBody = (yield import('raw-body')).default;
|
|
18
|
+
if (!req.method || (lowercase(req.method) === 'get')) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
return _getRawBody(req).then((_rawBody) => _rawBody === null || _rawBody === void 0 ? void 0 : _rawBody.toString());
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
console.error(`Failed to import 'raw-body', please ensure the dependency is installed`);
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
// TODO: explore returning mutated request object adding on req.rawBody??
|
|
29
|
+
export const ensureRawBody = (req) => __awaiter(void 0, void 0, void 0, function* () {
|
|
30
|
+
return (getChunkedRawBody(req)
|
|
31
|
+
.catch((error) => {
|
|
32
|
+
console.error(`Error getting raw body for '${req === null || req === void 0 ? void 0 : req.url}'`, error);
|
|
33
|
+
throw error;
|
|
34
|
+
}));
|
|
35
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { objectToSortedString, ensureString, safeParse, isTrue, } from '../shared/display';
|
|
11
|
+
import { normalizeURIParts, getRequestMethod, VERIFYD_HEADERS, } from '../shared/request';
|
|
12
|
+
import { VerificationError } from '../shared/errors';
|
|
13
|
+
import { WebCrypto } from '../shared/WebCrypto';
|
|
14
|
+
import { ensureRawBody } from './utils';
|
|
15
|
+
export const generateStorableKeyPairs = (...args_1) => __awaiter(void 0, [...args_1], void 0, function* ({ crypto: _crypto, util: _util } = {}) {
|
|
16
|
+
const webCrypto = new WebCrypto({ crypto: _crypto, util: _util });
|
|
17
|
+
const sharedKeyPair = yield webCrypto.generateKeyPair();
|
|
18
|
+
return webCrypto.getStorableKeyPair({
|
|
19
|
+
publicKey: sharedKeyPair.publicKey,
|
|
20
|
+
privateKey: sharedKeyPair.privateKey,
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
export const getVerificationHelpers = ({ keyPairs, util: _util, crypto: _crypto, DISABLE_RECRYPTION, } = { keyPairs: {} }) => {
|
|
24
|
+
const webCrypto = new WebCrypto({ crypto: _crypto, util: _util });
|
|
25
|
+
return (req, params) => __awaiter(void 0, void 0, void 0, function* () {
|
|
26
|
+
var _a;
|
|
27
|
+
const { [VERIFYD_HEADERS.PUBLIC_KEY]: _apiKey, [VERIFYD_HEADERS.PUBLIC_KEY.toLowerCase()]: apiKey = _apiKey, [VERIFYD_HEADERS.EPHEMERAL_KEY]: _ephemeralPublicKey, [VERIFYD_HEADERS.EPHEMERAL_KEY.toLowerCase()]: ephemeralPublicKey = _ephemeralPublicKey, [VERIFYD_HEADERS.AUTHORIZATION_TIMESTAMP]: _customAuthTimestamp, [VERIFYD_HEADERS.AUTHORIZATION_TIMESTAMP.toLowerCase()]: customAuthTimestamp = _customAuthTimestamp, [VERIFYD_HEADERS.AUTHORIZATION]: _customAuth, [VERIFYD_HEADERS.AUTHORIZATION.toLowerCase()]: customAuth = _customAuth, } = (_a = req.headers) !== null && _a !== void 0 ? _a : {};
|
|
28
|
+
const { uri: _uri, disableRecryption: _disableRecryption } = params !== null && params !== void 0 ? params : {};
|
|
29
|
+
const uri = _uri !== null && _uri !== void 0 ? _uri : req.url;
|
|
30
|
+
const disableRecryption = isTrue(DISABLE_RECRYPTION) || isTrue(_disableRecryption);
|
|
31
|
+
let isVerifiable = false;
|
|
32
|
+
try {
|
|
33
|
+
const [authProtocol, authSignature] = ensureString(customAuth).split(' ');
|
|
34
|
+
isVerifiable = isTrue(apiKey &&
|
|
35
|
+
ephemeralPublicKey &&
|
|
36
|
+
customAuthTimestamp &&
|
|
37
|
+
authSignature &&
|
|
38
|
+
(authProtocol === 'HMAC-SHA256') &&
|
|
39
|
+
(keyPairs === null || keyPairs === void 0 ? void 0 : keyPairs.shared));
|
|
40
|
+
let verificationKeys;
|
|
41
|
+
const rawBody = yield ensureRawBody(req);
|
|
42
|
+
// NOTE: requestBody should be wind up decrypted when isVerifiable (unless disableRecryption, then will pass through)
|
|
43
|
+
let requestBody = safeParse(rawBody);
|
|
44
|
+
// TEMP!!! remove isVerifiable check once web widget moved to react
|
|
45
|
+
if (isVerifiable) {
|
|
46
|
+
if (!apiKey ||
|
|
47
|
+
!ephemeralPublicKey ||
|
|
48
|
+
!customAuthTimestamp ||
|
|
49
|
+
!authSignature ||
|
|
50
|
+
(authProtocol !== 'HMAC-SHA256') ||
|
|
51
|
+
!(keyPairs === null || keyPairs === void 0 ? void 0 : keyPairs.shared) ||
|
|
52
|
+
(apiKey !== keyPairs.shared.publicKey)) {
|
|
53
|
+
throw new VerificationError('Invalid or missing authorization', { code: 401 });
|
|
54
|
+
}
|
|
55
|
+
verificationKeys = yield webCrypto.getVerificationKeys({
|
|
56
|
+
publicKey: ephemeralPublicKey,
|
|
57
|
+
privateKey: keyPairs.shared.privateKey,
|
|
58
|
+
});
|
|
59
|
+
if (!verificationKeys) {
|
|
60
|
+
throw new VerificationError('Invalid or missing verification', { code: 412 });
|
|
61
|
+
}
|
|
62
|
+
const verificationPayload = objectToSortedString(Object.assign({ method: getRequestMethod(rawBody, req.method), timestamp: customAuthTimestamp, body: requestBody }, normalizeURIParts(uri)));
|
|
63
|
+
const isValid = yield webCrypto.verifyHMAC(verificationPayload, verificationKeys.derivedHMACKey, authSignature);
|
|
64
|
+
if (!isValid) {
|
|
65
|
+
throw new VerificationError('Invalid or missing verification', { code: 403 });
|
|
66
|
+
}
|
|
67
|
+
if (!disableRecryption && requestBody) {
|
|
68
|
+
try {
|
|
69
|
+
const decryptedMessage = yield webCrypto.decryptMessage(requestBody, verificationKeys.derivedSecretKey);
|
|
70
|
+
requestBody = safeParse(decryptedMessage);
|
|
71
|
+
}
|
|
72
|
+
catch (_b) {
|
|
73
|
+
throw new VerificationError('Error decrypting request', { code: 400 });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const processResponse = (response) => __awaiter(void 0, void 0, void 0, function* () {
|
|
78
|
+
if (disableRecryption || !response || !isVerifiable || !(verificationKeys === null || verificationKeys === void 0 ? void 0 : verificationKeys.derivedSecretKey)) {
|
|
79
|
+
return response;
|
|
80
|
+
}
|
|
81
|
+
return webCrypto.encryptMessage(JSON.stringify(response), verificationKeys.derivedSecretKey);
|
|
82
|
+
});
|
|
83
|
+
return { rawBody, requestBody, processResponse };
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
console.error(`Error handling request verification for '${uri}'`, {
|
|
87
|
+
error,
|
|
88
|
+
isVerifiable,
|
|
89
|
+
disableRecryption,
|
|
90
|
+
});
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './useVerification';
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
11
|
+
var t = {};
|
|
12
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
13
|
+
t[p] = s[p];
|
|
14
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
15
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
16
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
17
|
+
t[p[i]] = s[p[i]];
|
|
18
|
+
}
|
|
19
|
+
return t;
|
|
20
|
+
};
|
|
21
|
+
import { useState } from 'react';
|
|
22
|
+
import { getVerificationKeysData, prepareVerificationRequest, processVerificationResponse, } from '../../shared/verification';
|
|
23
|
+
export const useVerification = (_a = {}) => {
|
|
24
|
+
var { publicKey, disableRecryption } = _a, params = __rest(_a, ["publicKey", "disableRecryption"]);
|
|
25
|
+
const [keysData, setKeysData] = useState();
|
|
26
|
+
const ensureKeysData = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
27
|
+
const _keysData = keysData !== null && keysData !== void 0 ? keysData : (yield getVerificationKeysData(publicKey, params));
|
|
28
|
+
if (_keysData && !keysData) {
|
|
29
|
+
setKeysData(_keysData);
|
|
30
|
+
}
|
|
31
|
+
return _keysData;
|
|
32
|
+
});
|
|
33
|
+
const prepareVerification = (requestPath, requestOptions) => __awaiter(void 0, void 0, void 0, function* () {
|
|
34
|
+
const _keysData = yield ensureKeysData();
|
|
35
|
+
const _prepareVerification = prepareVerificationRequest({ disableRecryption, keysData: _keysData });
|
|
36
|
+
return _prepareVerification(requestPath, requestOptions);
|
|
37
|
+
});
|
|
38
|
+
const processResponse = (encryptedResponse, derivedSecretKey) => __awaiter(void 0, void 0, void 0, function* () {
|
|
39
|
+
const _keysData = yield ensureKeysData();
|
|
40
|
+
const _processResponse = processVerificationResponse({ disableRecryption, keysData: _keysData });
|
|
41
|
+
return _processResponse(encryptedResponse, derivedSecretKey);
|
|
42
|
+
});
|
|
43
|
+
return {
|
|
44
|
+
prepareVerification,
|
|
45
|
+
processResponse,
|
|
46
|
+
};
|
|
47
|
+
};
|
|
File without changes
|