@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 CHANGED
@@ -1,95 +1,106 @@
1
1
  # @agnostack/verifyd
2
+
2
3
  ![Test](https://github.com/agnostack/verifyd/actions/workflows/test.yml/badge.svg)
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
- # npm install @agnostack/verifyd
11
+ # or
12
+ npm install @agnostack/verifyd
8
13
  ```
9
14
 
10
- ## Quickstart - TODO
15
+ ## Export Paths
11
16
 
12
- TODO!!!!!!! This readme needs to be updated to show how to use verifyd!
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
- Inside of `next.config.js`, add the following:
15
- ```js
16
- const { withShopify } = require('@agnostack/verifyd')
25
+ ### Tree-Shaking (ESM)
17
26
 
18
- const manifestTemplate = require('./manifestTemplate.json')
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
- const nextConfig = withShopify({
21
- zendesk: { manifestTemplate },
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
- Also, create an api route (`pages/api/apps.js`) containing the following:
27
- ```js
28
- import getConfig from 'next/config'
29
- import { withShopify } from '@agnostack/verifyd'
37
+ ## Web Crypto Resolution
30
38
 
31
- const { serverRuntimeConfig } = getConfig() ?? {}
39
+ `WebCrypto` uses a layered resolution strategy to find a Web Crypto API implementation:
32
40
 
33
- export default withShopify(serverRuntimeConfig)
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
- ## Alternate Option #1
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
- const { withPlugins } = require('@agnostack/next-plugins')
42
- const { withShopify } = require('@agnostack/verifyd')
43
-
44
- const manifest = require('./manifest.json')
45
-
46
- const nextPlugins = [{
47
- [withShopify]: {
48
- manifestTemplate: manifest,
49
- /* NOTE: add optionale below
50
- apiRoute: '/api/my-custom-api-json-route', // (defaults to /api/apps)
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
- Also, create an api route (`pages/api/apps.js`) containing the following:
84
+ ### Verification Helpers (Server-Side)
85
+
85
86
  ```js
86
- import getConfig from 'next/config'
87
- import { withShopify } from '@agnostack/verifyd'
87
+ import { getVerificationHelpers } from '@agnostack/verifyd'
88
+
89
+ const {
90
+ generateStorableKeyPairs,
91
+ prepareVerificationRequest,
92
+ processVerificationResponse,
93
+ } = getVerificationHelpers()
94
+ ```
88
95
 
89
- const { publicRuntimeConfig } = getConfig() ?? {}
96
+ ### React Hook
97
+
98
+ ```js
99
+ import { useVerification } from '@agnostack/verifyd/react'
90
100
 
91
- export default withShopify(publicRuntimeConfig)
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,3 @@
1
+ export * from '../shared';
2
+ export * from './utils';
3
+ export * from './verification';
@@ -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
+ };
@@ -0,0 +1,2 @@
1
+ export * from '../shared/request'; // HMMMM: why cant we export WebCrypto class??
2
+ export * from './hooks';
File without changes