@memberjunction/lists 0.0.1 → 5.36.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,123 @@
1
+ /**
2
+ * Server-signed token attached to every `ListDelta`. The token is the only
3
+ * thing `ApplyDelta` will accept as proof that:
4
+ * - the preview was produced server-side (not forged by a client),
5
+ * - the preview describes a known target + source pairing,
6
+ * - the preview is fresh (within `TOKEN_TTL_MS`).
7
+ *
8
+ * Token format is `<base64url(payload-json)>.<base64url(hmac-sha256)>`. We
9
+ * deliberately avoid a JWT dependency — the payload shape is fixed, the
10
+ * algorithm is fixed, and we own both ends.
11
+ *
12
+ * Uses Node's built-in `node:crypto`. This module is server-only — clients
13
+ * never sign or verify tokens (they receive them inside a `ListDelta` and
14
+ * hand them back unchanged to `ApplyDelta`).
15
+ *
16
+ * Public functions remain async for backwards compatibility with callers
17
+ * that already `await` them; the underlying node:crypto APIs are synchronous,
18
+ * which is fine — HMAC on a tiny payload is sub-millisecond.
19
+ */
20
+ import { createHash, createHmac, timingSafeEqual } from 'node:crypto';
21
+ import { TOKEN_TTL_MS, } from '@memberjunction/lists-base';
22
+ export class DeltaTokenVerificationError extends Error {
23
+ constructor(code, message) {
24
+ super(message);
25
+ this.Code = code;
26
+ }
27
+ }
28
+ /**
29
+ * Produce a deterministic fingerprint of a record-ID set. Used as the `ssig`
30
+ * field on the token payload — server-side `ApplyDelta` recomputes the
31
+ * fingerprint of the freshly resolved source and compares against the token
32
+ * to detect `STALE_DELTA`.
33
+ */
34
+ export async function ComputeSourceSignature(recordIds) {
35
+ const sorted = [...recordIds].sort();
36
+ return createHash('sha256').update(sorted.join('\n'), 'utf8').digest('hex');
37
+ }
38
+ /**
39
+ * Resolve the HMAC secret. Server bootstrap is expected to set
40
+ * `MJ_LIST_DELTA_SECRET` (or call `SetDeltaTokenSecret` directly for tests).
41
+ * We refuse to fall back to a default — silent fallback to a known secret
42
+ * would let a misconfigured server hand out forgeable tokens.
43
+ */
44
+ let injectedSecret;
45
+ export function SetDeltaTokenSecret(secret) {
46
+ injectedSecret = secret && secret.length > 0 ? secret : undefined;
47
+ }
48
+ function getSecret() {
49
+ const envSecret = process?.env?.MJ_LIST_DELTA_SECRET;
50
+ const secret = injectedSecret ?? envSecret;
51
+ if (!secret || secret.length === 0) {
52
+ throw new Error("MJ_LIST_DELTA_SECRET is not configured. Set the env var or call SetDeltaTokenSecret() at server boot.");
53
+ }
54
+ return secret;
55
+ }
56
+ // ---------------------------------------------------------------------------
57
+ // Base64URL helpers
58
+ // ---------------------------------------------------------------------------
59
+ function bufferToBase64Url(buf) {
60
+ return buf.toString('base64').replace(/=+$/, '').replace(/\+/g, '-').replace(/\//g, '_');
61
+ }
62
+ function base64UrlToBuffer(s) {
63
+ const padded = s + '='.repeat((4 - (s.length % 4)) % 4);
64
+ const std = padded.replace(/-/g, '+').replace(/_/g, '/');
65
+ return Buffer.from(std, 'base64');
66
+ }
67
+ function hmacOf(payloadEncoded) {
68
+ return createHmac('sha256', getSecret()).update(payloadEncoded, 'utf8').digest();
69
+ }
70
+ /**
71
+ * Sign a payload. Callers should pass `iat: Date.now()` and an `ssig`
72
+ * computed via `ComputeSourceSignature`. Returns the wire form.
73
+ */
74
+ export async function SignDeltaToken(payload) {
75
+ const json = JSON.stringify(payload);
76
+ const payloadEncoded = bufferToBase64Url(Buffer.from(json, 'utf8'));
77
+ const sig = bufferToBase64Url(hmacOf(payloadEncoded));
78
+ return `${payloadEncoded}.${sig}`;
79
+ }
80
+ /**
81
+ * Verify a token: checks structural shape, HMAC, and TTL. Returns the parsed
82
+ * payload on success; throws `DeltaTokenVerificationError` otherwise.
83
+ *
84
+ * `timingSafeEqual` compares in time independent of the mismatch position,
85
+ * so a token-fuzzing attacker can't learn the secret from comparison-time
86
+ * side channels.
87
+ */
88
+ export async function VerifyDeltaToken(token, now = Date.now()) {
89
+ if (typeof token !== 'string' || !token.includes('.')) {
90
+ throw new DeltaTokenVerificationError('INVALID_TOKEN', 'Malformed delta token');
91
+ }
92
+ const [payloadEncoded, sigEncoded] = token.split('.', 2);
93
+ if (!payloadEncoded || !sigEncoded) {
94
+ throw new DeltaTokenVerificationError('INVALID_TOKEN', 'Malformed delta token');
95
+ }
96
+ const providedSig = base64UrlToBuffer(sigEncoded);
97
+ const expectedSig = hmacOf(payloadEncoded);
98
+ // Length mismatch would throw from timingSafeEqual — guard explicitly so
99
+ // the error path stays uniform.
100
+ // `Uint8Array.from(...)` strips the Buffer→ArrayBufferLike narrowing that
101
+ // TS 5.9's node:crypto typings reject (they require a concrete ArrayBuffer,
102
+ // not the SharedArrayBuffer-tolerant union Buffer carries).
103
+ if (providedSig.length !== expectedSig.length ||
104
+ !timingSafeEqual(Uint8Array.from(providedSig), Uint8Array.from(expectedSig))) {
105
+ throw new DeltaTokenVerificationError('INVALID_TOKEN', 'Delta token signature mismatch');
106
+ }
107
+ let payload;
108
+ try {
109
+ const json = base64UrlToBuffer(payloadEncoded).toString('utf8');
110
+ payload = JSON.parse(json);
111
+ }
112
+ catch {
113
+ throw new DeltaTokenVerificationError('INVALID_TOKEN', 'Delta token payload not parseable');
114
+ }
115
+ if (payload.v !== 1 || typeof payload.iat !== 'number' || typeof payload.ssig !== 'string') {
116
+ throw new DeltaTokenVerificationError('INVALID_TOKEN', 'Delta token payload shape invalid');
117
+ }
118
+ if (now - payload.iat > TOKEN_TTL_MS) {
119
+ throw new DeltaTokenVerificationError('EXPIRED_TOKEN', 'Delta token expired');
120
+ }
121
+ return payload;
122
+ }
123
+ //# sourceMappingURL=deltaToken.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deltaToken.js","sourceRoot":"","sources":["../src/deltaToken.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ export { ListOperations, type DeltaTarget } from './ListOperations.js';
2
+ export { ListSharing, DEFAULT_INVITATION_TTL_MS } from './ListSharing.js';
3
+ export { AudienceResolver } from './AudienceResolver.js';
4
+ export { ComputeSourceSignature, DeltaTokenVerificationError, SetDeltaTokenSecret, SignDeltaToken, VerifyDeltaToken, } from './deltaToken.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,cAAc,EAAE,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpE,OAAO,EAAE,WAAW,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAC;AACvE,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EACL,sBAAsB,EACtB,2BAA2B,EAC3B,mBAAmB,EACnB,cAAc,EACd,gBAAgB,GACjB,MAAM,cAAc,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ // @memberjunction/lists — server-side runtime. Pure types/interfaces are
2
+ // in @memberjunction/lists-base; per MJ's "no re-exports between packages"
3
+ // rule, this barrel only exports things defined within this package. Consumers
4
+ // that want shared types should import directly from @memberjunction/lists-base.
5
+ export { ListOperations } from './ListOperations.js';
6
+ export { ListSharing, DEFAULT_INVITATION_TTL_MS } from './ListSharing.js';
7
+ export { AudienceResolver } from './AudienceResolver.js';
8
+ export { ComputeSourceSignature, DeltaTokenVerificationError, SetDeltaTokenSecret, SignDeltaToken, VerifyDeltaToken, } from './deltaToken.js';
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,2EAA2E;AAC3E,+EAA+E;AAC/E,iFAAiF;AAEjF,OAAO,EAAE,cAAc,EAAoB,MAAM,kBAAkB,CAAC;AACpE,OAAO,EAAE,WAAW,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAC;AACvE,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EACL,sBAAsB,EACtB,2BAA2B,EAC3B,mBAAmB,EACnB,cAAc,EACd,gBAAgB,GACjB,MAAM,cAAc,CAAC"}
package/package.json CHANGED
@@ -1,10 +1,35 @@
1
1
  {
2
2
  "name": "@memberjunction/lists",
3
- "version": "0.0.1",
4
- "description": "OIDC trusted publishing setup package for @memberjunction/lists",
5
- "keywords": [
6
- "oidc",
7
- "trusted-publishing",
8
- "setup"
9
- ]
3
+ "type": "module",
4
+ "version": "5.36.0",
5
+ "description": "MemberJunction: Lists — server-side TypeScript core for list materialization, set algebra, delta computation, lineage tracking, refresh modes, and sharing/invitations. Writes to MJ entities (List Detail, Resource Permission, List Invitation, Audit Log) — not safe to import from browser bundles. UI consumers should depend on @memberjunction/lists-base for type/interface sharing and call the server via GraphQL.",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "/dist"
10
+ ],
11
+ "scripts": {
12
+ "start": "ts-node-dev src/index.ts",
13
+ "build": "tsc && tsc-alias -f",
14
+ "test": "vitest run",
15
+ "test:watch": "vitest",
16
+ "test:coverage": "vitest run --coverage"
17
+ },
18
+ "author": "MemberJunction.com",
19
+ "license": "ISC",
20
+ "devDependencies": {
21
+ "ts-node-dev": "^2.0.0",
22
+ "typescript": "^5.9.3",
23
+ "vitest": "^4.0.18"
24
+ },
25
+ "dependencies": {
26
+ "@memberjunction/core": "5.36.0",
27
+ "@memberjunction/core-entities": "5.36.0",
28
+ "@memberjunction/global": "5.36.0",
29
+ "@memberjunction/lists-base": "5.36.0"
30
+ },
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/MemberJunction/MJ"
34
+ }
10
35
  }