@rudderhq/identity-core 0.6.6-canary.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.
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Canonicalizes an email only after an upstream provider has proved ownership.
3
+ *
4
+ * Rudder deliberately does not implement provider-specific dot or plus aliases:
5
+ * those rules are not universal and would merge distinct mailboxes.
6
+ */
7
+ export declare function normalizeVerifiedEmail(value: string): string;
8
+ export declare function maskEmail(value: string): string;
9
+ //# sourceMappingURL=email.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email.d.ts","sourceRoot":"","sources":["../src/email.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAgB5D;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAK/C"}
package/dist/email.js ADDED
@@ -0,0 +1,27 @@
1
+ const MAX_EMAIL_LENGTH = 254;
2
+ /**
3
+ * Canonicalizes an email only after an upstream provider has proved ownership.
4
+ *
5
+ * Rudder deliberately does not implement provider-specific dot or plus aliases:
6
+ * those rules are not universal and would merge distinct mailboxes.
7
+ */
8
+ export function normalizeVerifiedEmail(value) {
9
+ const normalized = value.normalize("NFKC").trim().toLowerCase();
10
+ if (normalized.length === 0 ||
11
+ normalized.length > MAX_EMAIL_LENGTH ||
12
+ /[\u0000-\u001f\u007f]/u.test(normalized)) {
13
+ throw new Error("Invalid verified email");
14
+ }
15
+ const at = normalized.lastIndexOf("@");
16
+ if (at <= 0 || at === normalized.length - 1 || normalized.indexOf("@") !== at) {
17
+ throw new Error("Invalid verified email");
18
+ }
19
+ return normalized;
20
+ }
21
+ export function maskEmail(value) {
22
+ const normalized = normalizeVerifiedEmail(value);
23
+ const [local, domain] = normalized.split("@");
24
+ const visible = local.slice(0, Math.min(2, local.length));
25
+ return `${visible}${local.length > 2 ? "•••" : "•"}@${domain}`;
26
+ }
27
+ //# sourceMappingURL=email.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email.js","sourceRoot":"","sources":["../src/email.ts"],"names":[],"mappings":"AAAA,MAAM,gBAAgB,GAAG,GAAG,CAAC;AAE7B;;;;;GAKG;AACH,MAAM,UAAU,sBAAsB,CAAC,KAAa;IAClD,MAAM,UAAU,GAAG,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAChE,IACE,UAAU,CAAC,MAAM,KAAK,CAAC;QACvB,UAAU,CAAC,MAAM,GAAG,gBAAgB;QACpC,wBAAwB,CAAC,IAAI,CAAC,UAAU,CAAC,EACzC,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,wBAAwB,CAAC,CAAC;IAC5C,CAAC;IAED,MAAM,EAAE,GAAG,UAAU,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IACvC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,UAAU,CAAC,MAAM,GAAG,CAAC,IAAI,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,CAAC;QAC9E,MAAM,IAAI,KAAK,CAAC,wBAAwB,CAAC,CAAC;IAC5C,CAAC;IAED,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,KAAa;IACrC,MAAM,UAAU,GAAG,sBAAsB,CAAC,KAAK,CAAC,CAAC;IACjD,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,GAAG,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC9C,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;IAC1D,OAAO,GAAG,OAAO,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,IAAI,MAAM,EAAE,CAAC;AACjE,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=email.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email.test.d.ts","sourceRoot":"","sources":["../src/email.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,21 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { maskEmail, normalizeVerifiedEmail } from "./email.js";
3
+ describe("normalizeVerifiedEmail", () => {
4
+ it("normalizes case, whitespace, and Unicode width", () => {
5
+ expect(normalizeVerifiedEmail(" Alice@Example.COM ")).toBe("alice@example.com");
6
+ });
7
+ it("does not apply provider-specific alias rules", () => {
8
+ expect(normalizeVerifiedEmail("alice+work@gmail.com")).toBe("alice+work@gmail.com");
9
+ });
10
+ it("rejects malformed addresses and controls", () => {
11
+ expect(() => normalizeVerifiedEmail("alice")).toThrow("Invalid verified email");
12
+ expect(() => normalizeVerifiedEmail("a@example.com\n")).not.toThrow();
13
+ expect(() => normalizeVerifiedEmail("a\nb@example.com")).toThrow("Invalid verified email");
14
+ });
15
+ });
16
+ describe("maskEmail", () => {
17
+ it("preserves enough context without exposing the full local part", () => {
18
+ expect(maskEmail("alice@example.com")).toBe("al•••@example.com");
19
+ });
20
+ });
21
+ //# sourceMappingURL=email.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email.test.js","sourceRoot":"","sources":["../src/email.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAE/D,QAAQ,CAAC,wBAAwB,EAAE,GAAG,EAAE;IACtC,EAAE,CAAC,gDAAgD,EAAE,GAAG,EAAE;QACxD,MAAM,CAAC,sBAAsB,CAAC,sBAAsB,CAAC,CAAC,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC;IACnF,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,8CAA8C,EAAE,GAAG,EAAE;QACtD,MAAM,CAAC,sBAAsB,CAAC,sBAAsB,CAAC,CAAC,CAAC,IAAI,CAAC,sBAAsB,CAAC,CAAC;IACtF,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0CAA0C,EAAE,GAAG,EAAE;QAClD,MAAM,CAAC,GAAG,EAAE,CAAC,sBAAsB,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,wBAAwB,CAAC,CAAC;QAChF,MAAM,CAAC,GAAG,EAAE,CAAC,sBAAsB,CAAC,iBAAiB,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;QACtE,MAAM,CAAC,GAAG,EAAE,CAAC,sBAAsB,CAAC,kBAAkB,CAAC,CAAC,CAAC,OAAO,CAAC,wBAAwB,CAAC,CAAC;IAC7F,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,WAAW,EAAE,GAAG,EAAE;IACzB,EAAE,CAAC,+DAA+D,EAAE,GAAG,EAAE;QACvE,MAAM,CAAC,SAAS,CAAC,mBAAmB,CAAC,CAAC,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC;IACnE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,5 @@
1
+ export * from "./email.js";
2
+ export * from "./offline-grant.js";
3
+ export * from "./release-policy.js";
4
+ export * from "./secrets.js";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,cAAc,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export * from "./email.js";
2
+ export * from "./offline-grant.js";
3
+ export * from "./release-policy.js";
4
+ export * from "./secrets.js";
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,cAAc,CAAC"}
@@ -0,0 +1,92 @@
1
+ import { KeyObject } from "node:crypto";
2
+ export declare const MAX_OFFLINE_GRANT_LIFETIME_MS: number;
3
+ export type OfflineGrantClaims = {
4
+ version: 1;
5
+ issuer: string;
6
+ accountId: string;
7
+ deviceId: string;
8
+ installationId: string;
9
+ audience: "rudder-local-board";
10
+ publicKeyThumbprint: string;
11
+ issuedAtMs: number;
12
+ expiresAtMs: number;
13
+ trustedTimeMs: number;
14
+ signOutEpoch: number;
15
+ jti: string;
16
+ };
17
+ export type OfflineGrantProof = {
18
+ payload: {
19
+ version: 1;
20
+ grantHash: string;
21
+ method: string;
22
+ path: string;
23
+ bodyHash: string;
24
+ nonce: string;
25
+ issuedAtMs: number;
26
+ };
27
+ signature: string;
28
+ };
29
+ export declare function generateOfflineDeviceKeyPair(): {
30
+ privateKey: KeyObject;
31
+ publicKey: KeyObject;
32
+ thumbprint: string;
33
+ };
34
+ export declare function offlineDevicePublicKeyThumbprint(publicKey: KeyObject | string | Buffer): string;
35
+ export declare function issueOfflineGrant(input: {
36
+ signingPrivateKey: KeyObject | string | Buffer;
37
+ keyId: string;
38
+ issuer: string;
39
+ accountId: string;
40
+ deviceId: string;
41
+ installationId: string;
42
+ publicKeyThumbprint: string;
43
+ nowMs: number;
44
+ expiresAtMs?: number;
45
+ trustedTimeMs: number;
46
+ signOutEpoch: number;
47
+ jti: string;
48
+ }): string;
49
+ export declare function verifyOfflineGrant(input: {
50
+ grant: string;
51
+ identityPublicKey: KeyObject | string | Buffer;
52
+ expectedKeyId: string;
53
+ expectedIssuer: string;
54
+ expectedInstallationId: string;
55
+ expectedDeviceId?: string;
56
+ expectedAccountId?: string;
57
+ nowMs: number;
58
+ lastTrustedTimeMs: number;
59
+ localSignOutEpoch: number;
60
+ }): OfflineGrantClaims;
61
+ export declare function createOfflineGrantProof(input: {
62
+ grant: string;
63
+ devicePrivateKey: KeyObject | string | Buffer;
64
+ method: string;
65
+ path: string;
66
+ bodyHash: string;
67
+ nonce: string;
68
+ issuedAtMs: number;
69
+ }): OfflineGrantProof;
70
+ export declare function verifyOfflineGrantAndProof(input: {
71
+ grant: string;
72
+ proof: OfflineGrantProof;
73
+ identityPublicKey: KeyObject | string | Buffer;
74
+ expectedKeyId: string;
75
+ devicePublicKey: KeyObject | string | Buffer;
76
+ expectedIssuer: string;
77
+ expectedInstallationId: string;
78
+ expectedDeviceId?: string;
79
+ expectedAccountId?: string;
80
+ expectedMethod: string;
81
+ expectedPath: string;
82
+ expectedBodyHash: string;
83
+ nowMs: number;
84
+ lastTrustedTimeMs: number;
85
+ localSignOutEpoch: number;
86
+ consumeNonce(nonce: string): boolean;
87
+ }): {
88
+ claims: OfflineGrantClaims;
89
+ nextTrustedTimeMs: number;
90
+ };
91
+ export declare function offlineRequestBodyHash(body: Buffer | string): string;
92
+ //# sourceMappingURL=offline-grant.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"offline-grant.d.ts","sourceRoot":"","sources":["../src/offline-grant.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,217 @@
1
+ import { createHash, createPrivateKey, createPublicKey, generateKeyPairSync, KeyObject, sign, verify, } from "node:crypto";
2
+ export const MAX_OFFLINE_GRANT_LIFETIME_MS = 30 * 24 * 60 * 60 * 1000;
3
+ const CLOCK_ROLLBACK_TOLERANCE_MS = 5 * 60 * 1000;
4
+ const PROOF_CLOCK_SKEW_MS = 5 * 60 * 1000;
5
+ const MAX_TOKEN_BYTES = 32 * 1024;
6
+ function base64url(value) {
7
+ return Buffer.from(value).toString("base64url");
8
+ }
9
+ function decodeBase64url(value) {
10
+ if (!/^[A-Za-z0-9_-]+$/.test(value))
11
+ throw new Error("invalid_encoding");
12
+ return Buffer.from(value, "base64url");
13
+ }
14
+ function stableJson(value) {
15
+ if (value === null || typeof value !== "object")
16
+ return JSON.stringify(value);
17
+ if (Array.isArray(value))
18
+ return `[${value.map(stableJson).join(",")}]`;
19
+ const record = value;
20
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`).join(",")}}`;
21
+ }
22
+ function requirePrivateKey(value) {
23
+ return value instanceof KeyObject ? value : createPrivateKey(value);
24
+ }
25
+ function requirePublicKey(value) {
26
+ return value instanceof KeyObject ? value : createPublicKey(value);
27
+ }
28
+ function sha256(value) {
29
+ return createHash("sha256").update(value).digest("base64url");
30
+ }
31
+ function assertSafeString(value, label, max = 512) {
32
+ if (typeof value !== "string" || value.length === 0 || value.length > max) {
33
+ throw new Error(`invalid_${label}`);
34
+ }
35
+ }
36
+ function assertNonce(value) {
37
+ if (typeof value !== "string"
38
+ || value.length < 16
39
+ || value.length > 256
40
+ || !/^[A-Za-z0-9_-]+$/.test(value))
41
+ throw new Error("invalid_nonce");
42
+ }
43
+ function parseClaims(value) {
44
+ if (!value || typeof value !== "object")
45
+ throw new Error("invalid_offline_grant");
46
+ const claims = value;
47
+ if (claims.version !== 1
48
+ || claims.audience !== "rudder-local-board"
49
+ || !Number.isSafeInteger(claims.issuedAtMs)
50
+ || !Number.isSafeInteger(claims.expiresAtMs)
51
+ || !Number.isSafeInteger(claims.trustedTimeMs)
52
+ || !Number.isSafeInteger(claims.signOutEpoch)
53
+ || Number(claims.signOutEpoch) < 0)
54
+ throw new Error("invalid_offline_grant");
55
+ assertSafeString(claims.issuer, "issuer");
56
+ assertSafeString(claims.accountId, "account_id");
57
+ assertSafeString(claims.deviceId, "device_id");
58
+ assertSafeString(claims.installationId, "installation_id");
59
+ assertSafeString(claims.publicKeyThumbprint, "public_key_thumbprint");
60
+ assertSafeString(claims.jti, "jti");
61
+ return claims;
62
+ }
63
+ export function generateOfflineDeviceKeyPair() {
64
+ const { privateKey, publicKey } = generateKeyPairSync("ed25519");
65
+ return { privateKey, publicKey, thumbprint: offlineDevicePublicKeyThumbprint(publicKey) };
66
+ }
67
+ export function offlineDevicePublicKeyThumbprint(publicKey) {
68
+ const jwk = requirePublicKey(publicKey).export({ format: "jwk" });
69
+ if (jwk.kty !== "OKP" || jwk.crv !== "Ed25519" || typeof jwk.x !== "string") {
70
+ throw new Error("Offline device keys must use Ed25519");
71
+ }
72
+ return sha256(stableJson({ crv: jwk.crv, kty: jwk.kty, x: jwk.x }));
73
+ }
74
+ export function issueOfflineGrant(input) {
75
+ const expiresAtMs = input.expiresAtMs ?? input.nowMs + MAX_OFFLINE_GRANT_LIFETIME_MS;
76
+ if (!Number.isSafeInteger(input.nowMs)
77
+ || !Number.isSafeInteger(expiresAtMs)
78
+ || expiresAtMs <= input.nowMs
79
+ || expiresAtMs - input.nowMs > MAX_OFFLINE_GRANT_LIFETIME_MS
80
+ || !Number.isSafeInteger(input.trustedTimeMs)
81
+ || input.trustedTimeMs > input.nowMs
82
+ || !Number.isSafeInteger(input.signOutEpoch)
83
+ || input.signOutEpoch < 0)
84
+ throw new Error("invalid_offline_grant_lifetime");
85
+ for (const [label, value] of [
86
+ ["key_id", input.keyId],
87
+ ["issuer", input.issuer],
88
+ ["account_id", input.accountId],
89
+ ["device_id", input.deviceId],
90
+ ["installation_id", input.installationId],
91
+ ["public_key_thumbprint", input.publicKeyThumbprint],
92
+ ["jti", input.jti],
93
+ ])
94
+ assertSafeString(value, label);
95
+ const header = {
96
+ alg: "EdDSA",
97
+ typ: "rudder-offline-grant+jwt",
98
+ kid: input.keyId,
99
+ };
100
+ const claims = {
101
+ version: 1,
102
+ issuer: input.issuer,
103
+ accountId: input.accountId,
104
+ deviceId: input.deviceId,
105
+ installationId: input.installationId,
106
+ audience: "rudder-local-board",
107
+ publicKeyThumbprint: input.publicKeyThumbprint,
108
+ issuedAtMs: input.nowMs,
109
+ expiresAtMs,
110
+ trustedTimeMs: input.trustedTimeMs,
111
+ signOutEpoch: input.signOutEpoch,
112
+ jti: input.jti,
113
+ };
114
+ const signingInput = `${base64url(stableJson(header))}.${base64url(stableJson(claims))}`;
115
+ const signature = sign(null, Buffer.from(signingInput), requirePrivateKey(input.signingPrivateKey));
116
+ return `${signingInput}.${base64url(signature)}`;
117
+ }
118
+ export function verifyOfflineGrant(input) {
119
+ if (!Number.isSafeInteger(input.nowMs)
120
+ || !Number.isSafeInteger(input.lastTrustedTimeMs)
121
+ || !Number.isSafeInteger(input.localSignOutEpoch)
122
+ || input.localSignOutEpoch < 0)
123
+ throw new Error("invalid_offline_verification_state");
124
+ if (Buffer.byteLength(input.grant, "utf8") > MAX_TOKEN_BYTES)
125
+ throw new Error("invalid_offline_grant");
126
+ const parts = input.grant.split(".");
127
+ if (parts.length !== 3)
128
+ throw new Error("invalid_offline_grant");
129
+ let headerValue;
130
+ let claimsValue;
131
+ try {
132
+ headerValue = JSON.parse(decodeBase64url(parts[0]).toString("utf8"));
133
+ claimsValue = JSON.parse(decodeBase64url(parts[1]).toString("utf8"));
134
+ }
135
+ catch {
136
+ throw new Error("invalid_offline_grant");
137
+ }
138
+ const header = headerValue;
139
+ if (header.alg !== "EdDSA"
140
+ || header.typ !== "rudder-offline-grant+jwt"
141
+ || header.kid !== input.expectedKeyId)
142
+ throw new Error("invalid_offline_grant_header");
143
+ const signatureValid = verify(null, Buffer.from(`${parts[0]}.${parts[1]}`), requirePublicKey(input.identityPublicKey), decodeBase64url(parts[2]));
144
+ if (!signatureValid)
145
+ throw new Error("invalid_offline_grant_signature");
146
+ const claims = parseClaims(claimsValue);
147
+ if (claims.issuer !== input.expectedIssuer
148
+ || claims.installationId !== input.expectedInstallationId
149
+ || (input.expectedDeviceId !== undefined && claims.deviceId !== input.expectedDeviceId)
150
+ || (input.expectedAccountId !== undefined && claims.accountId !== input.expectedAccountId))
151
+ throw new Error("offline_grant_binding_mismatch");
152
+ if (claims.expiresAtMs <= claims.issuedAtMs
153
+ || claims.expiresAtMs - claims.issuedAtMs > MAX_OFFLINE_GRANT_LIFETIME_MS)
154
+ throw new Error("offline_grant_expired");
155
+ if (input.nowMs + CLOCK_ROLLBACK_TOLERANCE_MS
156
+ < Math.max(claims.trustedTimeMs, input.lastTrustedTimeMs))
157
+ throw new Error("offline_clock_rollback");
158
+ if (input.nowMs < claims.issuedAtMs - PROOF_CLOCK_SKEW_MS
159
+ || input.nowMs >= claims.expiresAtMs)
160
+ throw new Error("offline_grant_expired");
161
+ if (input.localSignOutEpoch > claims.signOutEpoch)
162
+ throw new Error("offline_grant_signed_out");
163
+ return claims;
164
+ }
165
+ export function createOfflineGrantProof(input) {
166
+ assertSafeString(input.method, "method", 16);
167
+ assertSafeString(input.path, "path", 2048);
168
+ assertSafeString(input.bodyHash, "body_hash", 128);
169
+ assertNonce(input.nonce);
170
+ const payload = {
171
+ version: 1,
172
+ grantHash: sha256(input.grant),
173
+ method: input.method.toUpperCase(),
174
+ path: input.path,
175
+ bodyHash: input.bodyHash,
176
+ nonce: input.nonce,
177
+ issuedAtMs: input.issuedAtMs,
178
+ };
179
+ const signature = sign(null, Buffer.from(stableJson(payload)), requirePrivateKey(input.devicePrivateKey));
180
+ return { payload, signature: base64url(signature) };
181
+ }
182
+ export function verifyOfflineGrantAndProof(input) {
183
+ const claims = verifyOfflineGrant(input);
184
+ if (offlineDevicePublicKeyThumbprint(input.devicePublicKey) !== claims.publicKeyThumbprint) {
185
+ throw new Error("offline_device_key_mismatch");
186
+ }
187
+ const proof = input.proof;
188
+ if (!proof
189
+ || proof.payload?.version !== 1
190
+ || proof.payload.grantHash !== sha256(input.grant)
191
+ || proof.payload.method !== input.expectedMethod.toUpperCase()
192
+ || proof.payload.path !== input.expectedPath
193
+ || proof.payload.bodyHash !== input.expectedBodyHash
194
+ || !Number.isSafeInteger(proof.payload.issuedAtMs)
195
+ || Math.abs(input.nowMs - proof.payload.issuedAtMs) > PROOF_CLOCK_SKEW_MS)
196
+ throw new Error("invalid_offline_proof");
197
+ assertNonce(proof.payload.nonce);
198
+ let signature;
199
+ try {
200
+ signature = decodeBase64url(proof.signature);
201
+ }
202
+ catch {
203
+ throw new Error("invalid_offline_proof");
204
+ }
205
+ if (!verify(null, Buffer.from(stableJson(proof.payload)), requirePublicKey(input.devicePublicKey), signature))
206
+ throw new Error("invalid_offline_proof_signature");
207
+ if (!input.consumeNonce(proof.payload.nonce))
208
+ throw new Error("offline_proof_replay");
209
+ return {
210
+ claims,
211
+ nextTrustedTimeMs: Math.max(input.nowMs, input.lastTrustedTimeMs, claims.trustedTimeMs),
212
+ };
213
+ }
214
+ export function offlineRequestBodyHash(body) {
215
+ return sha256(body);
216
+ }
217
+ //# sourceMappingURL=offline-grant.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"offline-grant.js","sourceRoot":"","sources":["../src/offline-grant.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=offline-grant.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"offline-grant.test.d.ts","sourceRoot":"","sources":["../src/offline-grant.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,146 @@
1
+ import { generateKeyPairSync } from "node:crypto";
2
+ import { describe, expect, it } from "vitest";
3
+ import { createOfflineGrantProof, generateOfflineDeviceKeyPair, issueOfflineGrant, MAX_OFFLINE_GRANT_LIFETIME_MS, offlineRequestBodyHash, verifyOfflineGrantAndProof, } from "./offline-grant.js";
4
+ const nowMs = Date.parse("2026-07-29T00:00:00.000Z");
5
+ function fixture() {
6
+ const identityKeys = generateKeyPairSync("ed25519");
7
+ const deviceKeys = generateOfflineDeviceKeyPair();
8
+ const grant = issueOfflineGrant({
9
+ signingPrivateKey: identityKeys.privateKey,
10
+ keyId: "identity-key-1",
11
+ issuer: "https://accounts.rudderhq.dev",
12
+ accountId: "account-1",
13
+ deviceId: "device-1",
14
+ installationId: "default",
15
+ publicKeyThumbprint: deviceKeys.thumbprint,
16
+ nowMs,
17
+ trustedTimeMs: nowMs,
18
+ signOutEpoch: 3,
19
+ jti: "grant-1",
20
+ });
21
+ const bodyHash = offlineRequestBodyHash('{"intent":"open-local-board"}');
22
+ const proof = createOfflineGrantProof({
23
+ grant,
24
+ devicePrivateKey: deviceKeys.privateKey,
25
+ method: "POST",
26
+ path: "/api/auth/offline",
27
+ bodyHash,
28
+ nonce: "server-challenge-1",
29
+ issuedAtMs: nowMs + 1_000,
30
+ });
31
+ return { identityKeys, deviceKeys, grant, proof, bodyHash };
32
+ }
33
+ function verifyFixture(value, overrides = {}) {
34
+ const usedNonces = new Set();
35
+ return verifyOfflineGrantAndProof({
36
+ grant: value.grant,
37
+ proof: value.proof,
38
+ identityPublicKey: value.identityKeys.publicKey,
39
+ expectedKeyId: "identity-key-1",
40
+ devicePublicKey: value.deviceKeys.publicKey,
41
+ expectedIssuer: "https://accounts.rudderhq.dev",
42
+ expectedInstallationId: "default",
43
+ expectedDeviceId: "device-1",
44
+ expectedAccountId: "account-1",
45
+ expectedMethod: "POST",
46
+ expectedPath: "/api/auth/offline",
47
+ expectedBodyHash: value.bodyHash,
48
+ nowMs: nowMs + 1_000,
49
+ lastTrustedTimeMs: nowMs,
50
+ localSignOutEpoch: 3,
51
+ consumeNonce: (nonce) => {
52
+ if (usedNonces.has(nonce))
53
+ return false;
54
+ usedNonces.add(nonce);
55
+ return true;
56
+ },
57
+ ...overrides,
58
+ });
59
+ }
60
+ describe("Local offline grant proof of possession", () => {
61
+ it("verifies an installation-bound grant and advances trusted time", () => {
62
+ const value = fixture();
63
+ const result = verifyFixture(value);
64
+ expect(result.claims).toMatchObject({
65
+ accountId: "account-1",
66
+ deviceId: "device-1",
67
+ installationId: "default",
68
+ audience: "rudder-local-board",
69
+ signOutEpoch: 3,
70
+ });
71
+ expect(result.nextTrustedTimeMs).toBe(nowMs + 1_000);
72
+ });
73
+ it("rejects a copied grant without the bound device private key", () => {
74
+ const value = fixture();
75
+ const attacker = generateOfflineDeviceKeyPair();
76
+ const attackerProof = createOfflineGrantProof({
77
+ grant: value.grant,
78
+ devicePrivateKey: attacker.privateKey,
79
+ method: "POST",
80
+ path: "/api/auth/offline",
81
+ bodyHash: value.bodyHash,
82
+ nonce: "attacker-challenge",
83
+ issuedAtMs: nowMs + 1_000,
84
+ });
85
+ expect(() => verifyFixture(value, {
86
+ proof: attackerProof,
87
+ devicePublicKey: attacker.publicKey,
88
+ })).toThrow("device_key_mismatch");
89
+ });
90
+ it("rejects nonce replay after one valid proof", () => {
91
+ const value = fixture();
92
+ let consumed = false;
93
+ const options = {
94
+ consumeNonce: () => {
95
+ if (consumed)
96
+ return false;
97
+ consumed = true;
98
+ return true;
99
+ },
100
+ };
101
+ expect(() => verifyFixture(value, options)).not.toThrow();
102
+ expect(() => verifyFixture(value, options)).toThrow("proof_replay");
103
+ });
104
+ it("rejects clock rollback, expiry, installation mismatch, and local sign-out", () => {
105
+ const value = fixture();
106
+ expect(() => verifyFixture(value, {
107
+ nowMs: nowMs - 6 * 60 * 1000,
108
+ lastTrustedTimeMs: nowMs,
109
+ })).toThrow("clock_rollback");
110
+ expect(() => verifyFixture(value, {
111
+ nowMs: nowMs + MAX_OFFLINE_GRANT_LIFETIME_MS,
112
+ proof: createOfflineGrantProof({
113
+ grant: value.grant,
114
+ devicePrivateKey: value.deviceKeys.privateKey,
115
+ method: "POST",
116
+ path: "/api/auth/offline",
117
+ bodyHash: value.bodyHash,
118
+ nonce: "expiry-challenge",
119
+ issuedAtMs: nowMs + MAX_OFFLINE_GRANT_LIFETIME_MS,
120
+ }),
121
+ })).toThrow("grant_expired");
122
+ expect(() => verifyFixture(value, { expectedInstallationId: "restored-copy" }))
123
+ .toThrow("binding_mismatch");
124
+ expect(() => verifyFixture(value, { localSignOutEpoch: 4 }))
125
+ .toThrow("signed_out");
126
+ });
127
+ it("will not issue a grant longer than 30 days", () => {
128
+ const identityKeys = generateKeyPairSync("ed25519");
129
+ const deviceKeys = generateOfflineDeviceKeyPair();
130
+ expect(() => issueOfflineGrant({
131
+ signingPrivateKey: identityKeys.privateKey,
132
+ keyId: "identity-key-1",
133
+ issuer: "https://accounts.rudderhq.dev",
134
+ accountId: "account-1",
135
+ deviceId: "device-1",
136
+ installationId: "default",
137
+ publicKeyThumbprint: deviceKeys.thumbprint,
138
+ nowMs,
139
+ expiresAtMs: nowMs + MAX_OFFLINE_GRANT_LIFETIME_MS + 1,
140
+ trustedTimeMs: nowMs,
141
+ signOutEpoch: 0,
142
+ jti: "grant-too-long",
143
+ })).toThrow("lifetime");
144
+ });
145
+ });
146
+ //# sourceMappingURL=offline-grant.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"offline-grant.test.js","sourceRoot":"","sources":["../src/offline-grant.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ export type IdentityReleaseChannel = "development" | "test" | "preview" | "production";
2
+ export type IdentityReleasePolicy = {
3
+ channel: IdentityReleaseChannel;
4
+ issuer: string;
5
+ allowCapturedMail: boolean;
6
+ allowTestClients: boolean;
7
+ };
8
+ export declare function assertIdentityReleasePolicy(policy: IdentityReleasePolicy): void;
9
+ //# sourceMappingURL=release-policy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"release-policy.d.ts","sourceRoot":"","sources":["../src/release-policy.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,sBAAsB,GAAG,aAAa,GAAG,MAAM,GAAG,SAAS,GAAG,YAAY,CAAC;AAEvF,MAAM,MAAM,qBAAqB,GAAG;IAClC,OAAO,EAAE,sBAAsB,CAAC;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,iBAAiB,EAAE,OAAO,CAAC;IAC3B,gBAAgB,EAAE,OAAO,CAAC;CAC3B,CAAC;AAEF,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,qBAAqB,GAAG,IAAI,CA8B/E"}
@@ -0,0 +1,29 @@
1
+ export function assertIdentityReleasePolicy(policy) {
2
+ const issuer = new URL(policy.issuer);
3
+ if (policy.channel === "production" || policy.channel === "preview") {
4
+ const releaseName = policy.channel === "production" ? "Production" : "Preview";
5
+ if (issuer.protocol !== "https:")
6
+ throw new Error(`${releaseName} Identity issuer must use HTTPS`);
7
+ if (policy.allowCapturedMail)
8
+ throw new Error(`${releaseName} Identity cannot use captured mail`);
9
+ if (policy.allowTestClients)
10
+ throw new Error(`${releaseName} Identity cannot accept test clients`);
11
+ if (issuer.username ||
12
+ issuer.password ||
13
+ issuer.pathname !== "/" ||
14
+ issuer.search ||
15
+ issuer.hash) {
16
+ throw new Error(`${releaseName} Identity issuer must be an origin`);
17
+ }
18
+ }
19
+ if (policy.channel === "production" &&
20
+ issuer.origin !== "https://accounts.rudderhq.dev") {
21
+ throw new Error("Production Identity issuer must be https://accounts.rudderhq.dev");
22
+ }
23
+ if ((policy.channel === "development" || policy.channel === "test") &&
24
+ issuer.protocol !== "https:" &&
25
+ !["127.0.0.1", "::1", "localhost"].includes(issuer.hostname)) {
26
+ throw new Error("Non-HTTPS Identity issuer must be loopback");
27
+ }
28
+ }
29
+ //# sourceMappingURL=release-policy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"release-policy.js","sourceRoot":"","sources":["../src/release-policy.ts"],"names":[],"mappings":"AASA,MAAM,UAAU,2BAA2B,CAAC,MAA6B;IACvE,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IACtC,IAAI,MAAM,CAAC,OAAO,KAAK,YAAY,IAAI,MAAM,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;QACpE,MAAM,WAAW,GAAG,MAAM,CAAC,OAAO,KAAK,YAAY,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,SAAS,CAAC;QAC/E,IAAI,MAAM,CAAC,QAAQ,KAAK,QAAQ;YAAE,MAAM,IAAI,KAAK,CAAC,GAAG,WAAW,iCAAiC,CAAC,CAAC;QACnG,IAAI,MAAM,CAAC,iBAAiB;YAAE,MAAM,IAAI,KAAK,CAAC,GAAG,WAAW,oCAAoC,CAAC,CAAC;QAClG,IAAI,MAAM,CAAC,gBAAgB;YAAE,MAAM,IAAI,KAAK,CAAC,GAAG,WAAW,sCAAsC,CAAC,CAAC;QACnG,IACE,MAAM,CAAC,QAAQ;YACf,MAAM,CAAC,QAAQ;YACf,MAAM,CAAC,QAAQ,KAAK,GAAG;YACvB,MAAM,CAAC,MAAM;YACb,MAAM,CAAC,IAAI,EACX,CAAC;YACD,MAAM,IAAI,KAAK,CAAC,GAAG,WAAW,oCAAoC,CAAC,CAAC;QACtE,CAAC;IACH,CAAC;IACD,IACE,MAAM,CAAC,OAAO,KAAK,YAAY;QAC/B,MAAM,CAAC,MAAM,KAAK,+BAA+B,EACjD,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,kEAAkE,CAAC,CAAC;IACtF,CAAC;IACD,IACE,CAAC,MAAM,CAAC,OAAO,KAAK,aAAa,IAAI,MAAM,CAAC,OAAO,KAAK,MAAM,CAAC;QAC/D,MAAM,CAAC,QAAQ,KAAK,QAAQ;QAC5B,CAAC,CAAC,WAAW,EAAE,KAAK,EAAE,WAAW,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,EAC5D,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,4CAA4C,CAAC,CAAC;IAChE,CAAC;AACH,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=release-policy.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"release-policy.test.d.ts","sourceRoot":"","sources":["../src/release-policy.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,62 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { assertIdentityReleasePolicy } from "./release-policy.js";
3
+ describe("assertIdentityReleasePolicy", () => {
4
+ it("rejects production test capabilities", () => {
5
+ expect(() => assertIdentityReleasePolicy({
6
+ channel: "production",
7
+ issuer: "https://accounts.rudderhq.dev",
8
+ allowCapturedMail: true,
9
+ allowTestClients: false,
10
+ })).toThrow("captured mail");
11
+ });
12
+ it("allows isolated local development", () => {
13
+ expect(() => assertIdentityReleasePolicy({
14
+ channel: "development",
15
+ issuer: "http://127.0.0.1:3200",
16
+ allowCapturedMail: true,
17
+ allowTestClients: true,
18
+ })).not.toThrow();
19
+ });
20
+ it("fails preview closed for capture mail and test clients", () => {
21
+ for (const policy of [
22
+ { allowCapturedMail: true, allowTestClients: false },
23
+ { allowCapturedMail: false, allowTestClients: true },
24
+ ]) {
25
+ expect(() => assertIdentityReleasePolicy({
26
+ channel: "preview",
27
+ issuer: "https://preview-identity.rudderhq.dev",
28
+ ...policy,
29
+ })).toThrow();
30
+ }
31
+ });
32
+ it("rejects development capture on a non-loopback HTTP issuer", () => {
33
+ expect(() => assertIdentityReleasePolicy({
34
+ channel: "development",
35
+ issuer: "http://identity.internal",
36
+ allowCapturedMail: true,
37
+ allowTestClients: true,
38
+ })).toThrow("loopback");
39
+ });
40
+ it("requires an origin-only canonical production issuer", () => {
41
+ for (const issuer of [
42
+ "https://accounts.rudderhq.dev/account",
43
+ "https://user@accounts.rudderhq.dev",
44
+ "https://accounts.rudderhq.dev?preview=true",
45
+ "https://identity.rudderhq.dev",
46
+ ]) {
47
+ expect(() => assertIdentityReleasePolicy({
48
+ channel: "production",
49
+ issuer,
50
+ allowCapturedMail: false,
51
+ allowTestClients: false,
52
+ })).toThrow();
53
+ }
54
+ expect(() => assertIdentityReleasePolicy({
55
+ channel: "production",
56
+ issuer: "https://accounts.rudderhq.dev",
57
+ allowCapturedMail: false,
58
+ allowTestClients: false,
59
+ })).not.toThrow();
60
+ });
61
+ });
62
+ //# sourceMappingURL=release-policy.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"release-policy.test.js","sourceRoot":"","sources":["../src/release-policy.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,2BAA2B,EAAE,MAAM,qBAAqB,CAAC;AAElE,QAAQ,CAAC,6BAA6B,EAAE,GAAG,EAAE;IAC3C,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC9C,MAAM,CAAC,GAAG,EAAE,CACV,2BAA2B,CAAC;YAC1B,OAAO,EAAE,YAAY;YACrB,MAAM,EAAE,+BAA+B;YACvC,iBAAiB,EAAE,IAAI;YACvB,gBAAgB,EAAE,KAAK;SACxB,CAAC,CACH,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;IAC7B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mCAAmC,EAAE,GAAG,EAAE;QAC3C,MAAM,CAAC,GAAG,EAAE,CACV,2BAA2B,CAAC;YAC1B,OAAO,EAAE,aAAa;YACtB,MAAM,EAAE,uBAAuB;YAC/B,iBAAiB,EAAE,IAAI;YACvB,gBAAgB,EAAE,IAAI;SACvB,CAAC,CACH,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;IAClB,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wDAAwD,EAAE,GAAG,EAAE;QAChE,KAAK,MAAM,MAAM,IAAI;YACnB,EAAE,iBAAiB,EAAE,IAAI,EAAE,gBAAgB,EAAE,KAAK,EAAE;YACpD,EAAE,iBAAiB,EAAE,KAAK,EAAE,gBAAgB,EAAE,IAAI,EAAE;SACrD,EAAE,CAAC;YACF,MAAM,CAAC,GAAG,EAAE,CACV,2BAA2B,CAAC;gBAC1B,OAAO,EAAE,SAAS;gBAClB,MAAM,EAAE,uCAAuC;gBAC/C,GAAG,MAAM;aACV,CAAC,CACH,CAAC,OAAO,EAAE,CAAC;QACd,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2DAA2D,EAAE,GAAG,EAAE;QACnE,MAAM,CAAC,GAAG,EAAE,CACV,2BAA2B,CAAC;YAC1B,OAAO,EAAE,aAAa;YACtB,MAAM,EAAE,0BAA0B;YAClC,iBAAiB,EAAE,IAAI;YACvB,gBAAgB,EAAE,IAAI;SACvB,CAAC,CACH,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IACxB,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qDAAqD,EAAE,GAAG,EAAE;QAC7D,KAAK,MAAM,MAAM,IAAI;YACnB,uCAAuC;YACvC,oCAAoC;YACpC,4CAA4C;YAC5C,+BAA+B;SAChC,EAAE,CAAC;YACF,MAAM,CAAC,GAAG,EAAE,CACV,2BAA2B,CAAC;gBAC1B,OAAO,EAAE,YAAY;gBACrB,MAAM;gBACN,iBAAiB,EAAE,KAAK;gBACxB,gBAAgB,EAAE,KAAK;aACxB,CAAC,CACH,CAAC,OAAO,EAAE,CAAC;QACd,CAAC;QACD,MAAM,CAAC,GAAG,EAAE,CACV,2BAA2B,CAAC;YAC1B,OAAO,EAAE,YAAY;YACrB,MAAM,EAAE,+BAA+B;YACvC,iBAAiB,EAAE,KAAK;YACxB,gBAAgB,EAAE,KAAK;SACxB,CAAC,CACH,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;IAClB,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,4 @@
1
+ export declare function hashOpaqueSecret(value: string): string;
2
+ export declare function opaqueSecretMatches(value: string, expectedHash: string): boolean;
3
+ export declare function redactSensitiveText(value: string): string;
4
+ //# sourceMappingURL=secrets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secrets.d.ts","sourceRoot":"","sources":["../src/secrets.ts"],"names":[],"mappings":"AAEA,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAIhF;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAKzD"}
@@ -0,0 +1,16 @@
1
+ import { createHash, timingSafeEqual } from "node:crypto";
2
+ export function hashOpaqueSecret(value) {
3
+ return createHash("sha256").update(value, "utf8").digest("base64url");
4
+ }
5
+ export function opaqueSecretMatches(value, expectedHash) {
6
+ const actual = Buffer.from(hashOpaqueSecret(value));
7
+ const expected = Buffer.from(expectedHash);
8
+ return actual.length === expected.length && timingSafeEqual(actual, expected);
9
+ }
10
+ export function redactSensitiveText(value) {
11
+ return value
12
+ .replace(/\b\d{6}\b/gu, "[REDACTED_OTP]")
13
+ .replace(/(bearer\s+)[^\s]+/giu, "$1[REDACTED]")
14
+ .replace(/((?:^|[?&\s])(?:token|code|secret)=)[^&\s]+/giu, "$1[REDACTED]");
15
+ }
16
+ //# sourceMappingURL=secrets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secrets.js","sourceRoot":"","sources":["../src/secrets.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE1D,MAAM,UAAU,gBAAgB,CAAC,KAAa;IAC5C,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;AACxE,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,KAAa,EAAE,YAAoB;IACrE,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC,CAAC;IACpD,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;IAC3C,OAAO,MAAM,CAAC,MAAM,KAAK,QAAQ,CAAC,MAAM,IAAI,eAAe,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;AAChF,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,KAAa;IAC/C,OAAO,KAAK;SACT,OAAO,CAAC,aAAa,EAAE,gBAAgB,CAAC;SACxC,OAAO,CAAC,sBAAsB,EAAE,cAAc,CAAC;SAC/C,OAAO,CAAC,gDAAgD,EAAE,cAAc,CAAC,CAAC;AAC/E,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=secrets.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secrets.test.d.ts","sourceRoot":"","sources":["../src/secrets.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,15 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { hashOpaqueSecret, opaqueSecretMatches, redactSensitiveText } from "./secrets.js";
3
+ describe("opaque secret helpers", () => {
4
+ it("hashes deterministically and compares safely", () => {
5
+ const hash = hashOpaqueSecret("refresh-secret");
6
+ expect(hash).not.toContain("refresh-secret");
7
+ expect(opaqueSecretMatches("refresh-secret", hash)).toBe(true);
8
+ expect(opaqueSecretMatches("wrong", hash)).toBe(false);
9
+ });
10
+ it("redacts OTPs and URL or bearer credentials", () => {
11
+ expect(redactSensitiveText("OTP 123456 bearer abc? token=secret")).toBe("OTP [REDACTED_OTP] bearer [REDACTED] token=[REDACTED]");
12
+ expect(redactSensitiveText("/callback?code=secret&state=ok")).toBe("/callback?code=[REDACTED]&state=ok");
13
+ });
14
+ });
15
+ //# sourceMappingURL=secrets.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secrets.test.js","sourceRoot":"","sources":["../src/secrets.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAE1F,QAAQ,CAAC,uBAAuB,EAAE,GAAG,EAAE;IACrC,EAAE,CAAC,8CAA8C,EAAE,GAAG,EAAE;QACtD,MAAM,IAAI,GAAG,gBAAgB,CAAC,gBAAgB,CAAC,CAAC;QAChD,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,gBAAgB,CAAC,CAAC;QAC7C,MAAM,CAAC,mBAAmB,CAAC,gBAAgB,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC/D,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACzD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4CAA4C,EAAE,GAAG,EAAE;QACpD,MAAM,CAAC,mBAAmB,CAAC,qCAAqC,CAAC,CAAC,CAAC,IAAI,CACrE,uDAAuD,CACxD,CAAC;QACF,MAAM,CAAC,mBAAmB,CAAC,gCAAgC,CAAC,CAAC,CAAC,IAAI,CAChE,oCAAoC,CACrC,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@rudderhq/identity-core",
3
+ "version": "0.6.6-canary.2",
4
+ "license": "SEE LICENSE IN LICENSE",
5
+ "homepage": "https://github.com/Undertone0809/rudder",
6
+ "bugs": {
7
+ "url": "https://github.com/Undertone0809/rudder/issues"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/Undertone0809/rudder",
12
+ "directory": "packages/identity-core"
13
+ },
14
+ "type": "module",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "import": "./dist/index.js",
19
+ "default": "./dist/index.js"
20
+ }
21
+ },
22
+ "publishConfig": {
23
+ "access": "public",
24
+ "exports": {
25
+ ".": {
26
+ "types": "./dist/index.d.ts",
27
+ "import": "./dist/index.js"
28
+ }
29
+ },
30
+ "main": "./dist/index.js",
31
+ "types": "./dist/index.d.ts"
32
+ },
33
+ "files": [
34
+ "dist"
35
+ ],
36
+ "scripts": {
37
+ "build": "tsc",
38
+ "clean": "rm -rf dist",
39
+ "test": "vitest run --config vitest.config.ts",
40
+ "typecheck": "tsc --noEmit"
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "^24.12.0",
44
+ "typescript": "^5.9.3",
45
+ "vitest": "^3.2.7"
46
+ },
47
+ "main": "./dist/index.js",
48
+ "types": "./dist/index.d.ts"
49
+ }