@yougrowai/node 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +264 -55
- package/dist/cjs/index.d.ts +189 -0
- package/dist/cjs/index.js +246 -0
- package/dist/cjs/jwt.d.ts +54 -0
- package/dist/cjs/jwt.js +80 -0
- package/dist/cjs/origin.d.ts +11 -0
- package/dist/cjs/origin.js +20 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/server.d.ts +121 -0
- package/dist/cjs/server.js +140 -0
- package/dist/index.d.ts +164 -64
- package/dist/index.js +205 -121
- package/dist/jwt.d.ts +3 -2
- package/dist/jwt.js +2 -1
- package/dist/origin.d.ts +11 -0
- package/dist/origin.js +15 -0
- package/dist/server.d.ts +17 -4
- package/dist/server.js +19 -5
- package/package.json +18 -9
- package/test/vectors.json +1 -24
- package/dist/signing.d.ts +0 -19
- package/dist/signing.js +0 -10
package/dist/origin.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YouGrow's origin, shared by the client (its requests go to `${origin}/api/v2/…`)
|
|
3
|
+
* and the verifier (tokens carry `iss: origin`; keys are at
|
|
4
|
+
* `${origin}/.well-known/jwks.json`). Pass the same value to both, e.g. from
|
|
5
|
+
* YOUGROW_ORIGIN, when you're connected to another YouGrow instance.
|
|
6
|
+
*/
|
|
7
|
+
export declare const DEFAULT_ORIGIN = "https://yougrow.ai";
|
|
8
|
+
/** Without trailing slashes. Unset or empty means DEFAULT_ORIGIN. */
|
|
9
|
+
export declare function originOf(value: string | undefined): string;
|
|
10
|
+
/** https, or plain http on localhost for local development. */
|
|
11
|
+
export declare function isSecureOrigin(origin: string): boolean;
|
package/dist/origin.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YouGrow's origin, shared by the client (its requests go to `${origin}/api/v2/…`)
|
|
3
|
+
* and the verifier (tokens carry `iss: origin`; keys are at
|
|
4
|
+
* `${origin}/.well-known/jwks.json`). Pass the same value to both, e.g. from
|
|
5
|
+
* YOUGROW_ORIGIN, when you're connected to another YouGrow instance.
|
|
6
|
+
*/
|
|
7
|
+
export const DEFAULT_ORIGIN = "https://yougrow.ai";
|
|
8
|
+
/** Without trailing slashes. Unset or empty means DEFAULT_ORIGIN. */
|
|
9
|
+
export function originOf(value) {
|
|
10
|
+
return (value || DEFAULT_ORIGIN).replace(/\/+$/, "");
|
|
11
|
+
}
|
|
12
|
+
/** https, or plain http on localhost for local development. */
|
|
13
|
+
export function isSecureOrigin(origin) {
|
|
14
|
+
return /^https:\/\//.test(origin) || /^http:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/.test(origin);
|
|
15
|
+
}
|
package/dist/server.d.ts
CHANGED
|
@@ -10,13 +10,15 @@ import { type Jwk, type JwtFailure, type RequestDirection, type YouGrowClaims }
|
|
|
10
10
|
* Every such request carries `Authorization: Bearer <JWT>` signed with
|
|
11
11
|
* YouGrow's private key. Your secret is NOT involved — you verify against
|
|
12
12
|
* YouGrow's public keys, so nothing you store can be used to forge YouGrow.
|
|
13
|
-
* Always verify against the RAW body,
|
|
13
|
+
* Always verify against the RAW body (the exact bytes received, as a Buffer or
|
|
14
|
+
* string), before parsing it.
|
|
14
15
|
*
|
|
15
|
-
* const verifier = createVerifier({ keyId: process.env.YOUGROW_KEY_ID
|
|
16
|
+
* const verifier = createVerifier({ keyId: process.env.YOUGROW_KEY_ID!, origin: process.env.YOUGROW_ORIGIN });
|
|
16
17
|
* const v = await verifier.verify({ headers: req.headers, rawBody, direction: "context" });
|
|
17
18
|
* if (!v.ok) return res.status(401).end();
|
|
18
19
|
*/
|
|
19
20
|
export type { Jwk, RequestDirection, YouGrowClaims } from "./jwt.js";
|
|
21
|
+
/** YouGrow's default origin: the `iss` of its tokens. */
|
|
20
22
|
export declare const DEFAULT_ISSUER = "https://yougrow.ai";
|
|
21
23
|
type HeaderBag = Headers | Record<string, string | string[] | undefined>;
|
|
22
24
|
export type VerifyResult = {
|
|
@@ -29,7 +31,13 @@ export type VerifyResult = {
|
|
|
29
31
|
export interface VerifierOptions {
|
|
30
32
|
/** Your connection's key id — the token's audience. */
|
|
31
33
|
keyId: string;
|
|
32
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* YouGrow's origin: the same value as the client's `origin`, e.g. from
|
|
36
|
+
* YOUGROW_ORIGIN. Defaults to https://yougrow.ai. Tokens must carry it as
|
|
37
|
+
* `iss`, and the keys are fetched from `${origin}/.well-known/jwks.json`.
|
|
38
|
+
*/
|
|
39
|
+
origin?: string;
|
|
40
|
+
/** Alias of `origin` (its 0.1 name). */
|
|
33
41
|
issuer?: string;
|
|
34
42
|
/** Pin the key set instead of fetching it (tests, air-gapped setups). */
|
|
35
43
|
jwks?: {
|
|
@@ -38,9 +46,14 @@ export interface VerifierOptions {
|
|
|
38
46
|
fetch?: typeof fetch;
|
|
39
47
|
}
|
|
40
48
|
export interface Verifier {
|
|
49
|
+
/**
|
|
50
|
+
* Check one request. `rawBody` is the exact body received — a string, or the
|
|
51
|
+
* bytes (e.g. a Buffer) — never re-serialised JSON. Plain header objects
|
|
52
|
+
* match in any case; Fetch `Headers` already do.
|
|
53
|
+
*/
|
|
41
54
|
verify(input: {
|
|
42
55
|
headers: HeaderBag;
|
|
43
|
-
rawBody: string;
|
|
56
|
+
rawBody: string | Uint8Array;
|
|
44
57
|
direction: RequestDirection;
|
|
45
58
|
nowMs?: number;
|
|
46
59
|
}): Promise<VerifyResult>;
|
package/dist/server.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { tokenFromAuthorization, tokenKid, verifyJwt } from "./jwt.js";
|
|
2
|
-
|
|
2
|
+
import { DEFAULT_ORIGIN, isSecureOrigin, originOf } from "./origin.js";
|
|
3
|
+
/** YouGrow's default origin: the `iss` of its tokens. */
|
|
4
|
+
export const DEFAULT_ISSUER = DEFAULT_ORIGIN;
|
|
3
5
|
const JWKS_PATH = "/.well-known/jwks.json";
|
|
4
6
|
const MIN_CACHE_MS = 60_000;
|
|
5
7
|
const MAX_CACHE_MS = 24 * 3600_000;
|
|
@@ -7,10 +9,17 @@ const DEFAULT_CACHE_MS = 3600_000;
|
|
|
7
9
|
/** An unknown kid refetches the keys at most this often. */
|
|
8
10
|
const REFETCH_COOLDOWN_MS = 60_000;
|
|
9
11
|
function header(h, name) {
|
|
12
|
+
if (!h)
|
|
13
|
+
return null;
|
|
10
14
|
if (typeof h.get === "function")
|
|
11
15
|
return h.get(name);
|
|
12
16
|
const bag = h;
|
|
13
|
-
|
|
17
|
+
let v = bag[name] ?? bag[name.toLowerCase()];
|
|
18
|
+
if (v === undefined) {
|
|
19
|
+
// Plain objects can keep the sender's casing (e.g. API Gateway REST events): match any case.
|
|
20
|
+
const lower = name.toLowerCase();
|
|
21
|
+
v = Object.entries(bag).find(([k, value]) => value !== undefined && k.toLowerCase() === lower)?.[1];
|
|
22
|
+
}
|
|
14
23
|
return Array.isArray(v) ? (v[0] ?? null) : (v ?? null);
|
|
15
24
|
}
|
|
16
25
|
function cacheMs(cacheControl) {
|
|
@@ -25,10 +34,12 @@ function cacheMs(cacheControl) {
|
|
|
25
34
|
* change on your side. If a refresh fails it keeps using the keys it has.
|
|
26
35
|
*/
|
|
27
36
|
export function createVerifier(opts) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
throw new Error("createVerifier: issuer must be https");
|
|
37
|
+
if (opts.origin && opts.issuer && originOf(opts.origin) !== originOf(opts.issuer)) {
|
|
38
|
+
throw new Error("createVerifier: origin and issuer differ (issuer is an alias of origin); pass origin only");
|
|
31
39
|
}
|
|
40
|
+
const issuer = originOf(opts.origin || opts.issuer);
|
|
41
|
+
if (!opts.jwks && !isSecureOrigin(issuer))
|
|
42
|
+
throw new Error("createVerifier: origin must be https");
|
|
32
43
|
if (!opts.keyId)
|
|
33
44
|
throw new Error("createVerifier: keyId is required");
|
|
34
45
|
const doFetch = opts.fetch ?? globalThis.fetch;
|
|
@@ -62,6 +73,9 @@ export function createVerifier(opts) {
|
|
|
62
73
|
}
|
|
63
74
|
return {
|
|
64
75
|
async verify(input) {
|
|
76
|
+
if (typeof input.rawBody !== "string" && !ArrayBuffer.isView(input.rawBody)) {
|
|
77
|
+
throw new TypeError("verify: rawBody must be the raw request body (a string or Buffer), not parsed JSON");
|
|
78
|
+
}
|
|
65
79
|
const token = tokenFromAuthorization(header(input.headers, "authorization"));
|
|
66
80
|
const now = Date.now();
|
|
67
81
|
const sinceFetch = now - lastFetchAt;
|
package/package.json
CHANGED
|
@@ -1,29 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yougrowai/node",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Keep your users' state in YouGrow lifecycle journeys, and verify the requests YouGrow sends you.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "YouGrow.AI Limited",
|
|
7
|
-
"homepage": "https://
|
|
7
|
+
"homepage": "https://yougrow.ai/developers",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
10
|
"url": "git+https://github.com/ygai-jezl/vizzyblmrkt.git",
|
|
11
11
|
"directory": "sdk/node"
|
|
12
12
|
},
|
|
13
|
-
"keywords": ["yougrow", "lifecycle-email", "onboarding", "events", "webhooks", "jwks"],
|
|
13
|
+
"keywords": ["yougrow", "lifecycle-email", "onboarding", "user-state", "events", "webhooks", "jwks"],
|
|
14
14
|
"type": "module",
|
|
15
|
-
"main": "./dist/index.js",
|
|
16
|
-
"types": "./dist/index.d.ts",
|
|
15
|
+
"main": "./dist/cjs/index.js",
|
|
16
|
+
"types": "./dist/cjs/index.d.ts",
|
|
17
17
|
"exports": {
|
|
18
|
-
".": {
|
|
19
|
-
|
|
18
|
+
".": {
|
|
19
|
+
"import": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
|
|
20
|
+
"require": { "types": "./dist/cjs/index.d.ts", "default": "./dist/cjs/index.js" }
|
|
21
|
+
},
|
|
22
|
+
"./server": {
|
|
23
|
+
"import": { "types": "./dist/server.d.ts", "default": "./dist/server.js" },
|
|
24
|
+
"require": { "types": "./dist/cjs/server.d.ts", "default": "./dist/cjs/server.js" }
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"typesVersions": {
|
|
28
|
+
"*": { "server": ["./dist/cjs/server.d.ts"] }
|
|
20
29
|
},
|
|
21
30
|
"files": ["dist", "test/vectors.json", "README.md", "LICENSE"],
|
|
22
31
|
"engines": { "node": ">=18" },
|
|
23
32
|
"sideEffects": false,
|
|
24
33
|
"publishConfig": { "access": "public" },
|
|
25
34
|
"scripts": {
|
|
26
|
-
"build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json",
|
|
35
|
+
"build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json && tsc -p tsconfig.cjs.json && node -e \"require('fs').writeFileSync('dist/cjs/package.json',JSON.stringify({type:'commonjs'})+'\\n')\"",
|
|
27
36
|
"prepack": "npm run build"
|
|
28
37
|
}
|
|
29
38
|
}
|
package/test/vectors.json
CHANGED
|
@@ -1,28 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"description": "Signing vectors for the YouGrow product-connection protocol. '
|
|
3
|
-
"vectors": [
|
|
4
|
-
{
|
|
5
|
-
"secret": "ygs_test_secret_one",
|
|
6
|
-
"direction": "events",
|
|
7
|
-
"timestamp": 1758455000,
|
|
8
|
-
"body": "{\"batch\":[{\"type\":\"identify\",\"messageId\":\"m1\",\"userId\":\"u1\",\"timestamp\":\"2026-09-21T10:00:00Z\",\"traits\":{\"email\":\"alex@acme.test\"}}]}",
|
|
9
|
-
"signature": "v1=d4bdf726960d10a135b3493f4074b3d3d3773a23ec49955d6efb720bcd2ef914"
|
|
10
|
-
},
|
|
11
|
-
{
|
|
12
|
-
"secret": "ygs_test_secret_two",
|
|
13
|
-
"direction": "events",
|
|
14
|
-
"timestamp": 1758455123,
|
|
15
|
-
"body": "{\"batch\":[{\"type\":\"track\",\"messageId\":\"m2\",\"userId\":\"ü-用户\",\"timestamp\":\"2026-09-21T10:00:00+01:00\",\"event\":\"user.signed_up\"}]}",
|
|
16
|
-
"signature": "v1=fadcda4cd874e1ad19b1f7059cca902b54afdfe9032c10dcfdffa75cc450b317"
|
|
17
|
-
},
|
|
18
|
-
{
|
|
19
|
-
"secret": "ygs_test_secret_one",
|
|
20
|
-
"direction": "events",
|
|
21
|
-
"timestamp": 1758455000,
|
|
22
|
-
"body": "",
|
|
23
|
-
"signature": "v1=aae9033d246c64b2e62a630910f59e35531cfb71d5b1a4da90a1002323979eae"
|
|
24
|
-
}
|
|
25
|
-
],
|
|
2
|
+
"description": "Signing vectors for the YouGrow product-connection protocol. 'outbound': platform -> product ES256 JWTs (context pulls, webhooks), signed once with a throwaway P-256 key whose private half was discarded; only the public JWK is kept. Both the server and the SDK test suites must accept every outbound token at nowMs and reject the listed tamperings.",
|
|
26
3
|
"outbound": {
|
|
27
4
|
"issuer": "https://yougrow.ai",
|
|
28
5
|
"audience": "ygk_vectorsvectorsvectors0",
|
package/dist/signing.d.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Request signing for events your product sends to YouGrow:
|
|
3
|
-
*
|
|
4
|
-
* X-YouGrow-Signature: v1=<hex HMAC-SHA256(secret, `events:${timestamp}.${rawBody}`)>
|
|
5
|
-
*
|
|
6
|
-
* Timestamps are unix seconds; requests more than five minutes off are refused.
|
|
7
|
-
* Pinned by test/vectors.json.
|
|
8
|
-
*
|
|
9
|
-
* Requests YouGrow sends YOU (context pulls, webhooks) are not signed with your
|
|
10
|
-
* secret: they carry a JWT signed with YouGrow's own key. Verify those with
|
|
11
|
-
* `createVerifier` from "@yougrowai/node/server".
|
|
12
|
-
*/
|
|
13
|
-
export type Direction = "events";
|
|
14
|
-
export declare const HEADERS: {
|
|
15
|
-
readonly keyId: "x-yougrow-key-id";
|
|
16
|
-
readonly timestamp: "x-yougrow-timestamp";
|
|
17
|
-
readonly signature: "x-yougrow-signature";
|
|
18
|
-
};
|
|
19
|
-
export declare function sign(secret: string, direction: Direction, timestampSec: number, rawBody: string): string;
|
package/dist/signing.js
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { createHmac } from "node:crypto";
|
|
2
|
-
export const HEADERS = {
|
|
3
|
-
keyId: "x-yougrow-key-id",
|
|
4
|
-
timestamp: "x-yougrow-timestamp",
|
|
5
|
-
signature: "x-yougrow-signature",
|
|
6
|
-
};
|
|
7
|
-
export function sign(secret, direction, timestampSec, rawBody) {
|
|
8
|
-
const mac = createHmac("sha256", secret).update(`${direction}:${timestampSec}.${rawBody}`).digest("hex");
|
|
9
|
-
return `v1=${mac}`;
|
|
10
|
-
}
|