@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.
@@ -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, before parsing it.
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
- /** YouGrow's origin. Defaults to https://yougrow.ai; use the dev origin for staging. */
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
- export const DEFAULT_ISSUER = "https://yougrow.ai";
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
- const v = bag[name] ?? bag[name.toLowerCase()];
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
- const issuer = (opts.issuer ?? DEFAULT_ISSUER).replace(/\/+$/, "");
29
- if (!opts.jwks && !/^https:\/\//.test(issuer) && !/^http:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/.test(issuer)) {
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.1.0",
4
- "description": "Send your product's user events to YouGrow lifecycle journeys, and answer its signed context and webhook requests.",
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://github.com/ygai-jezl/vizzyblmrkt/tree/main/sdk/node#readme",
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
- ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
19
- "./server": { "types": "./dist/server.d.ts", "default": "./dist/server.js" }
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. 'vectors': product -> platform events, signature = 'v1=' + hex(HMAC-SHA256(secret, 'events:' + timestamp + '.' + body)), generated with openssl. '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.",
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
- }