@bernierllc/email-headers 0.0.1 → 0.2.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/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ /*
2
+ Copyright (c) 2025 Bernier LLC
3
+
4
+ This file is licensed to the client under a limited-use license.
5
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
6
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
7
+ */
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Error codes for programmatic error handling in email header operations.
3
+ */
4
+ export declare const HeaderErrorCode: {
5
+ readonly INVALID_HEADER: "INVALID_HEADER";
6
+ readonly INVALID_MESSAGE_ID: "INVALID_MESSAGE_ID";
7
+ readonly INVALID_EMAIL: "INVALID_EMAIL";
8
+ readonly PARSE_ERROR: "PARSE_ERROR";
9
+ };
10
+ export type HeaderErrorCodeType = typeof HeaderErrorCode[keyof typeof HeaderErrorCode];
11
+ /**
12
+ * Options for creating a HeaderError.
13
+ */
14
+ export interface HeaderErrorOptions {
15
+ /** The underlying error that caused this error */
16
+ cause?: Error;
17
+ /** Machine-readable error code for programmatic handling */
18
+ code?: HeaderErrorCodeType;
19
+ /** Additional context about the error */
20
+ context?: Record<string, unknown>;
21
+ }
22
+ /**
23
+ * Error class for email header operations.
24
+ *
25
+ * Supports ES2022 Error.cause for error chaining.
26
+ *
27
+ * @example
28
+ * ```typescript
29
+ * throw new HeaderError('Invalid message ID format', {
30
+ * code: HeaderErrorCode.INVALID_MESSAGE_ID,
31
+ * context: { messageId: 'bad-id' }
32
+ * });
33
+ * ```
34
+ */
35
+ export declare class HeaderError extends Error {
36
+ readonly code: HeaderErrorCodeType;
37
+ readonly context: Record<string, unknown> | undefined;
38
+ constructor(message: string, options?: HeaderErrorOptions);
39
+ }
40
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAQA;;GAEG;AACH,eAAO,MAAM,eAAe;;;;;CAKlB,CAAC;AAEX,MAAM,MAAM,mBAAmB,GAAG,OAAO,eAAe,CAAC,MAAM,OAAO,eAAe,CAAC,CAAC;AAEvF;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,kDAAkD;IAClD,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,4DAA4D;IAC5D,IAAI,CAAC,EAAE,mBAAmB,CAAC;IAC3B,yCAAyC;IACzC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED;;;;;;;;;;;;GAYG;AACH,qBAAa,WAAY,SAAQ,KAAK;IACpC,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC;IACnC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;gBAE1C,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,kBAAkB;CAc1D"}
package/dist/errors.js ADDED
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ /*
3
+ Copyright (c) 2025 Bernier LLC
4
+
5
+ This file is licensed to the client under a limited-use license.
6
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
7
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.HeaderError = exports.HeaderErrorCode = void 0;
11
+ /**
12
+ * Error codes for programmatic error handling in email header operations.
13
+ */
14
+ exports.HeaderErrorCode = {
15
+ INVALID_HEADER: 'INVALID_HEADER',
16
+ INVALID_MESSAGE_ID: 'INVALID_MESSAGE_ID',
17
+ INVALID_EMAIL: 'INVALID_EMAIL',
18
+ PARSE_ERROR: 'PARSE_ERROR',
19
+ };
20
+ /**
21
+ * Error class for email header operations.
22
+ *
23
+ * Supports ES2022 Error.cause for error chaining.
24
+ *
25
+ * @example
26
+ * ```typescript
27
+ * throw new HeaderError('Invalid message ID format', {
28
+ * code: HeaderErrorCode.INVALID_MESSAGE_ID,
29
+ * context: { messageId: 'bad-id' }
30
+ * });
31
+ * ```
32
+ */
33
+ class HeaderError extends Error {
34
+ constructor(message, options) {
35
+ super(message);
36
+ this.name = 'HeaderError';
37
+ this.code = options?.code ?? exports.HeaderErrorCode.INVALID_HEADER;
38
+ this.context = options?.context;
39
+ // Set cause using ES2022 pattern (works at runtime in Node 16.9+)
40
+ if (options?.cause) {
41
+ this.cause = options.cause;
42
+ }
43
+ // Maintains proper stack trace in V8 engines
44
+ Error.captureStackTrace?.(this, this.constructor);
45
+ }
46
+ }
47
+ exports.HeaderError = HeaderError;
48
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":";AAAA;;;;;;EAME;;;AAEF;;GAEG;AACU,QAAA,eAAe,GAAG;IAC7B,cAAc,EAAE,gBAAgB;IAChC,kBAAkB,EAAE,oBAAoB;IACxC,aAAa,EAAE,eAAe;IAC9B,WAAW,EAAE,aAAa;CAClB,CAAC;AAgBX;;;;;;;;;;;;GAYG;AACH,MAAa,WAAY,SAAQ,KAAK;IAIpC,YAAY,OAAe,EAAE,OAA4B;QACvD,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,aAAa,CAAC;QAC1B,IAAI,CAAC,IAAI,GAAG,OAAO,EAAE,IAAI,IAAI,uBAAe,CAAC,cAAc,CAAC;QAC5D,IAAI,CAAC,OAAO,GAAG,OAAO,EAAE,OAAO,CAAC;QAEhC,kEAAkE;QAClE,IAAI,OAAO,EAAE,KAAK,EAAE,CAAC;YAClB,IAAoC,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;QAC9D,CAAC;QAED,6CAA6C;QAC7C,KAAK,CAAC,iBAAiB,EAAE,CAAC,IAAI,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;IACpD,CAAC;CACF;AAlBD,kCAkBC"}
@@ -0,0 +1,7 @@
1
+ export * from './types';
2
+ export * from './errors';
3
+ export * from './threading';
4
+ export * from './unsubscribe';
5
+ export * from './mdn';
6
+ export * from './merge';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAQA,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,OAAO,CAAC;AACtB,cAAc,SAAS,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ /*
3
+ Copyright (c) 2025 Bernier LLC
4
+
5
+ This file is licensed to the client under a limited-use license.
6
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
7
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
8
+ */
9
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ var desc = Object.getOwnPropertyDescriptor(m, k);
12
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
13
+ desc = { enumerable: true, get: function() { return m[k]; } };
14
+ }
15
+ Object.defineProperty(o, k2, desc);
16
+ }) : (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ o[k2] = m[k];
19
+ }));
20
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
21
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
22
+ };
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ __exportStar(require("./types"), exports);
25
+ __exportStar(require("./errors"), exports);
26
+ __exportStar(require("./threading"), exports);
27
+ __exportStar(require("./unsubscribe"), exports);
28
+ __exportStar(require("./mdn"), exports);
29
+ __exportStar(require("./merge"), exports);
30
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;EAME;;;;;;;;;;;;;;;;AAEF,0CAAwB;AACxB,2CAAyB;AACzB,8CAA4B;AAC5B,gDAA8B;AAC9B,wCAAsB;AACtB,0CAAwB"}
package/dist/mdn.d.ts ADDED
@@ -0,0 +1,41 @@
1
+ import type { MdnDisposition } from './types';
2
+ /**
3
+ * Create a Disposition-Notification-To header for requesting read receipts.
4
+ *
5
+ * @param notifyEmail - Email address to send the MDN notification to
6
+ * @returns Record with Disposition-Notification-To header
7
+ *
8
+ * @throws {HeaderError} If the email format is invalid
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * const headers = createMdnRequestHeader('sender@example.com');
13
+ * // { 'Disposition-Notification-To': 'sender@example.com' }
14
+ * ```
15
+ */
16
+ export declare function createMdnRequestHeader(notifyEmail: string): Record<string, string>;
17
+ /**
18
+ * Parse an MDN (Message Disposition Notification) response body per RFC 3798.
19
+ *
20
+ * MDN bodies contain fields like:
21
+ * - Original-Message-ID: <message-id>
22
+ * - Final-Recipient: rfc822; user@example.com
23
+ * - Disposition: automatic-action/MDN-sent-automatically; displayed
24
+ *
25
+ * @param mdnContent - The MDN body content to parse
26
+ * @returns Parsed MDN disposition information
27
+ *
28
+ * @throws {HeaderError} If required fields are missing or disposition is invalid
29
+ *
30
+ * @example
31
+ * ```typescript
32
+ * const mdn = parseMdnResponse(
33
+ * 'Original-Message-ID: <abc@example.com>\r\n' +
34
+ * 'Final-Recipient: rfc822; user@example.com\r\n' +
35
+ * 'Disposition: manual-action/MDN-sent-manually; displayed\r\n'
36
+ * );
37
+ * // { originalMessageId: '<abc@example.com>', recipient: 'user@example.com', disposition: 'displayed' }
38
+ * ```
39
+ */
40
+ export declare function parseMdnResponse(mdnContent: string): MdnDisposition;
41
+ //# sourceMappingURL=mdn.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mdn.d.ts","sourceRoot":"","sources":["../src/mdn.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAsB9C;;;;;;;;;;;;;GAaG;AACH,wBAAgB,sBAAsB,CACpC,WAAW,EAAE,MAAM,GAClB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAmBxB;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,cAAc,CAkFnE"}
package/dist/mdn.js ADDED
@@ -0,0 +1,160 @@
1
+ "use strict";
2
+ /*
3
+ Copyright (c) 2025 Bernier LLC
4
+
5
+ This file is licensed to the client under a limited-use license.
6
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
7
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.createMdnRequestHeader = createMdnRequestHeader;
11
+ exports.parseMdnResponse = parseMdnResponse;
12
+ const errors_1 = require("./errors");
13
+ /**
14
+ * Basic email format validation.
15
+ */
16
+ function isValidEmail(email) {
17
+ // Simple but reasonable email regex
18
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
19
+ }
20
+ /**
21
+ * Valid MDN disposition types per RFC 3798.
22
+ */
23
+ const VALID_DISPOSITIONS = new Set([
24
+ 'displayed',
25
+ 'deleted',
26
+ 'dispatched',
27
+ 'processed',
28
+ 'denied',
29
+ 'failed',
30
+ ]);
31
+ /**
32
+ * Create a Disposition-Notification-To header for requesting read receipts.
33
+ *
34
+ * @param notifyEmail - Email address to send the MDN notification to
35
+ * @returns Record with Disposition-Notification-To header
36
+ *
37
+ * @throws {HeaderError} If the email format is invalid
38
+ *
39
+ * @example
40
+ * ```typescript
41
+ * const headers = createMdnRequestHeader('sender@example.com');
42
+ * // { 'Disposition-Notification-To': 'sender@example.com' }
43
+ * ```
44
+ */
45
+ function createMdnRequestHeader(notifyEmail) {
46
+ if (!notifyEmail || notifyEmail.trim().length === 0) {
47
+ throw new errors_1.HeaderError('Notification email is required', {
48
+ code: errors_1.HeaderErrorCode.INVALID_EMAIL,
49
+ context: { notifyEmail },
50
+ });
51
+ }
52
+ const trimmed = notifyEmail.trim();
53
+ if (!isValidEmail(trimmed)) {
54
+ throw new errors_1.HeaderError('Invalid email format for MDN notification', {
55
+ code: errors_1.HeaderErrorCode.INVALID_EMAIL,
56
+ context: { notifyEmail: trimmed },
57
+ });
58
+ }
59
+ return {
60
+ 'Disposition-Notification-To': trimmed,
61
+ };
62
+ }
63
+ /**
64
+ * Parse an MDN (Message Disposition Notification) response body per RFC 3798.
65
+ *
66
+ * MDN bodies contain fields like:
67
+ * - Original-Message-ID: <message-id>
68
+ * - Final-Recipient: rfc822; user@example.com
69
+ * - Disposition: automatic-action/MDN-sent-automatically; displayed
70
+ *
71
+ * @param mdnContent - The MDN body content to parse
72
+ * @returns Parsed MDN disposition information
73
+ *
74
+ * @throws {HeaderError} If required fields are missing or disposition is invalid
75
+ *
76
+ * @example
77
+ * ```typescript
78
+ * const mdn = parseMdnResponse(
79
+ * 'Original-Message-ID: <abc@example.com>\r\n' +
80
+ * 'Final-Recipient: rfc822; user@example.com\r\n' +
81
+ * 'Disposition: manual-action/MDN-sent-manually; displayed\r\n'
82
+ * );
83
+ * // { originalMessageId: '<abc@example.com>', recipient: 'user@example.com', disposition: 'displayed' }
84
+ * ```
85
+ */
86
+ function parseMdnResponse(mdnContent) {
87
+ if (!mdnContent || mdnContent.trim().length === 0) {
88
+ throw new errors_1.HeaderError('MDN content is required', {
89
+ code: errors_1.HeaderErrorCode.PARSE_ERROR,
90
+ context: { mdnContent },
91
+ });
92
+ }
93
+ // Parse fields from MDN body (case-insensitive)
94
+ const lines = mdnContent.split(/\r?\n/);
95
+ const fields = {};
96
+ for (const line of lines) {
97
+ const colonIndex = line.indexOf(':');
98
+ if (colonIndex > 0) {
99
+ const key = line.substring(0, colonIndex).trim().toLowerCase();
100
+ const value = line.substring(colonIndex + 1).trim();
101
+ fields[key] = value;
102
+ }
103
+ }
104
+ // Extract Original-Message-ID
105
+ const originalMessageId = fields['original-message-id'];
106
+ if (originalMessageId === undefined || originalMessageId.length === 0) {
107
+ throw new errors_1.HeaderError('Missing Original-Message-ID in MDN response', {
108
+ code: errors_1.HeaderErrorCode.PARSE_ERROR,
109
+ context: { fields },
110
+ });
111
+ }
112
+ // Extract Final-Recipient (format: rfc822; user@example.com)
113
+ const finalRecipientRaw = fields['final-recipient'];
114
+ if (finalRecipientRaw === undefined || finalRecipientRaw.length === 0) {
115
+ throw new errors_1.HeaderError('Missing Final-Recipient in MDN response', {
116
+ code: errors_1.HeaderErrorCode.PARSE_ERROR,
117
+ context: { fields },
118
+ });
119
+ }
120
+ let recipient = finalRecipientRaw;
121
+ // Strip "rfc822;" or "rfc822; " prefix if present
122
+ const semicolonIndex = finalRecipientRaw.indexOf(';');
123
+ if (semicolonIndex >= 0) {
124
+ recipient = finalRecipientRaw.substring(semicolonIndex + 1).trim();
125
+ }
126
+ // Extract Disposition (format: action-mode/sending-mode; disposition-type)
127
+ const dispositionRaw = fields['disposition'];
128
+ if (dispositionRaw === undefined || dispositionRaw.length === 0) {
129
+ throw new errors_1.HeaderError('Missing Disposition in MDN response', {
130
+ code: errors_1.HeaderErrorCode.PARSE_ERROR,
131
+ context: { fields },
132
+ });
133
+ }
134
+ // Parse disposition type from the value after the last semicolon
135
+ const dispositionSemicolonIndex = dispositionRaw.lastIndexOf(';');
136
+ let dispositionType;
137
+ if (dispositionSemicolonIndex >= 0) {
138
+ dispositionType = dispositionRaw.substring(dispositionSemicolonIndex + 1).trim().toLowerCase();
139
+ }
140
+ else {
141
+ dispositionType = dispositionRaw.trim().toLowerCase();
142
+ }
143
+ // Handle composite disposition types like "displayed/error" - take the first part
144
+ const slashIndex = dispositionType.indexOf('/');
145
+ if (slashIndex >= 0) {
146
+ dispositionType = dispositionType.substring(0, slashIndex).trim();
147
+ }
148
+ if (!VALID_DISPOSITIONS.has(dispositionType)) {
149
+ throw new errors_1.HeaderError(`Invalid disposition type: ${dispositionType}`, {
150
+ code: errors_1.HeaderErrorCode.PARSE_ERROR,
151
+ context: { dispositionType, raw: dispositionRaw },
152
+ });
153
+ }
154
+ return {
155
+ originalMessageId,
156
+ recipient,
157
+ disposition: dispositionType,
158
+ };
159
+ }
160
+ //# sourceMappingURL=mdn.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mdn.js","sourceRoot":"","sources":["../src/mdn.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import type { EmailHeaders } from './types';
2
+ /**
3
+ * Merge multiple email header objects into one.
4
+ *
5
+ * Later values override earlier ones for the same key.
6
+ *
7
+ * @param base - The base headers to start with
8
+ * @param additional - Additional header sets to merge in order
9
+ * @returns Merged headers
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * const merged = mergeHeaders(
14
+ * { 'From': 'a@example.com', 'Subject': 'Hello' },
15
+ * { 'Subject': 'Updated', 'X-Custom': 'value' }
16
+ * );
17
+ * // { 'From': 'a@example.com', 'Subject': 'Updated', 'X-Custom': 'value' }
18
+ * ```
19
+ */
20
+ export declare function mergeHeaders(base: EmailHeaders, ...additional: EmailHeaders[]): EmailHeaders;
21
+ //# sourceMappingURL=merge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge.d.ts","sourceRoot":"","sources":["../src/merge.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE5C;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,YAAY,EAClB,GAAG,UAAU,EAAE,YAAY,EAAE,GAC5B,YAAY,CAad"}
package/dist/merge.js ADDED
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ /*
3
+ Copyright (c) 2025 Bernier LLC
4
+
5
+ This file is licensed to the client under a limited-use license.
6
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
7
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.mergeHeaders = mergeHeaders;
11
+ /**
12
+ * Merge multiple email header objects into one.
13
+ *
14
+ * Later values override earlier ones for the same key.
15
+ *
16
+ * @param base - The base headers to start with
17
+ * @param additional - Additional header sets to merge in order
18
+ * @returns Merged headers
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * const merged = mergeHeaders(
23
+ * { 'From': 'a@example.com', 'Subject': 'Hello' },
24
+ * { 'Subject': 'Updated', 'X-Custom': 'value' }
25
+ * );
26
+ * // { 'From': 'a@example.com', 'Subject': 'Updated', 'X-Custom': 'value' }
27
+ * ```
28
+ */
29
+ function mergeHeaders(base, ...additional) {
30
+ const result = { ...base };
31
+ for (const headers of additional) {
32
+ for (const key of Object.keys(headers)) {
33
+ const value = headers[key];
34
+ if (value !== undefined) {
35
+ result[key] = value;
36
+ }
37
+ }
38
+ }
39
+ return result;
40
+ }
41
+ //# sourceMappingURL=merge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge.js","sourceRoot":"","sources":["../src/merge.ts"],"names":[],"mappings":";AAAA;;;;;;EAME;;AAsBF,oCAgBC;AAlCD;;;;;;;;;;;;;;;;;GAiBG;AACH,SAAgB,YAAY,CAC1B,IAAkB,EAClB,GAAG,UAA0B;IAE7B,MAAM,MAAM,GAAiB,EAAE,GAAG,IAAI,EAAE,CAAC;IAEzC,KAAK,MAAM,OAAO,IAAI,UAAU,EAAE,CAAC;QACjC,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;YACvC,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;YAC3B,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;gBACxB,MAAM,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;YACtB,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -0,0 +1,34 @@
1
+ import type { ParsedThreadingInfo } from './types';
2
+ /**
3
+ * Create threading headers (In-Reply-To and References) for email replies.
4
+ *
5
+ * @param inReplyTo - The message ID being replied to
6
+ * @param references - Optional array of previous message IDs in the thread
7
+ * @returns Record with 'In-Reply-To' and 'References' headers
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * const headers = createThreadingHeaders('abc@example.com', ['root@example.com']);
12
+ * // { 'In-Reply-To': '<abc@example.com>', 'References': '<root@example.com> <abc@example.com>' }
13
+ * ```
14
+ */
15
+ export declare function createThreadingHeaders(inReplyTo: string, references?: string[]): Record<string, string>;
16
+ /**
17
+ * Parse threading headers from an email message.
18
+ *
19
+ * Performs case-insensitive header lookup to handle different email systems.
20
+ *
21
+ * @param headers - Raw email headers as key-value pairs
22
+ * @returns Parsed threading information
23
+ *
24
+ * @example
25
+ * ```typescript
26
+ * const info = parseThreadingHeaders({
27
+ * 'In-Reply-To': '<reply@example.com>',
28
+ * 'References': '<root@example.com> <reply@example.com>'
29
+ * });
30
+ * // { inReplyTo: '<reply@example.com>', references: ['<root@example.com>', '<reply@example.com>'], threadId: '<root@example.com>' }
31
+ * ```
32
+ */
33
+ export declare function parseThreadingHeaders(headers: Record<string, string>): ParsedThreadingInfo;
34
+ //# sourceMappingURL=threading.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"threading.d.ts","sourceRoot":"","sources":["../src/threading.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AA6BnD;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,MAAM,EACjB,UAAU,CAAC,EAAE,MAAM,EAAE,GACpB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA8BxB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC9B,mBAAmB,CAqCrB"}
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+ /*
3
+ Copyright (c) 2025 Bernier LLC
4
+
5
+ This file is licensed to the client under a limited-use license.
6
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
7
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.createThreadingHeaders = createThreadingHeaders;
11
+ exports.parseThreadingHeaders = parseThreadingHeaders;
12
+ const errors_1 = require("./errors");
13
+ /**
14
+ * Wrap a message ID in angle brackets if not already wrapped.
15
+ */
16
+ function ensureAngleBrackets(messageId) {
17
+ const trimmed = messageId.trim();
18
+ if (trimmed.startsWith('<') && trimmed.endsWith('>')) {
19
+ return trimmed;
20
+ }
21
+ return `<${trimmed}>`;
22
+ }
23
+ /**
24
+ * Extract message IDs from a header value (space or comma separated, angle-bracketed).
25
+ */
26
+ function extractMessageIds(headerValue) {
27
+ const matches = headerValue.match(/<[^>]+>/g);
28
+ if (!matches) {
29
+ // Try treating the whole value as a single message ID
30
+ const trimmed = headerValue.trim();
31
+ if (trimmed.length > 0) {
32
+ return [trimmed];
33
+ }
34
+ return [];
35
+ }
36
+ return matches;
37
+ }
38
+ /**
39
+ * Create threading headers (In-Reply-To and References) for email replies.
40
+ *
41
+ * @param inReplyTo - The message ID being replied to
42
+ * @param references - Optional array of previous message IDs in the thread
43
+ * @returns Record with 'In-Reply-To' and 'References' headers
44
+ *
45
+ * @example
46
+ * ```typescript
47
+ * const headers = createThreadingHeaders('abc@example.com', ['root@example.com']);
48
+ * // { 'In-Reply-To': '<abc@example.com>', 'References': '<root@example.com> <abc@example.com>' }
49
+ * ```
50
+ */
51
+ function createThreadingHeaders(inReplyTo, references) {
52
+ if (!inReplyTo || inReplyTo.trim().length === 0) {
53
+ throw new errors_1.HeaderError('inReplyTo message ID is required', {
54
+ code: errors_1.HeaderErrorCode.INVALID_MESSAGE_ID,
55
+ context: { inReplyTo },
56
+ });
57
+ }
58
+ const wrappedInReplyTo = ensureAngleBrackets(inReplyTo);
59
+ // Build references: existing refs + the inReplyTo at the end
60
+ const refIds = [];
61
+ if (references && references.length > 0) {
62
+ for (const ref of references) {
63
+ const wrapped = ensureAngleBrackets(ref);
64
+ if (!refIds.includes(wrapped)) {
65
+ refIds.push(wrapped);
66
+ }
67
+ }
68
+ }
69
+ // Add inReplyTo at the end if not already present
70
+ if (!refIds.includes(wrappedInReplyTo)) {
71
+ refIds.push(wrappedInReplyTo);
72
+ }
73
+ return {
74
+ 'In-Reply-To': wrappedInReplyTo,
75
+ 'References': refIds.join(' '),
76
+ };
77
+ }
78
+ /**
79
+ * Parse threading headers from an email message.
80
+ *
81
+ * Performs case-insensitive header lookup to handle different email systems.
82
+ *
83
+ * @param headers - Raw email headers as key-value pairs
84
+ * @returns Parsed threading information
85
+ *
86
+ * @example
87
+ * ```typescript
88
+ * const info = parseThreadingHeaders({
89
+ * 'In-Reply-To': '<reply@example.com>',
90
+ * 'References': '<root@example.com> <reply@example.com>'
91
+ * });
92
+ * // { inReplyTo: '<reply@example.com>', references: ['<root@example.com>', '<reply@example.com>'], threadId: '<root@example.com>' }
93
+ * ```
94
+ */
95
+ function parseThreadingHeaders(headers) {
96
+ // Case-insensitive header lookup
97
+ const normalizedHeaders = {};
98
+ for (const key of Object.keys(headers)) {
99
+ normalizedHeaders[key.toLowerCase()] = headers[key];
100
+ }
101
+ const inReplyToValue = normalizedHeaders['in-reply-to'];
102
+ const referencesValue = normalizedHeaders['references'];
103
+ // Parse In-Reply-To
104
+ let inReplyTo = null;
105
+ if (inReplyToValue !== undefined && inReplyToValue.trim().length > 0) {
106
+ const ids = extractMessageIds(inReplyToValue);
107
+ // In-Reply-To typically contains a single message ID
108
+ inReplyTo = ids[0] ?? null;
109
+ }
110
+ // Parse References
111
+ const references = [];
112
+ if (referencesValue !== undefined && referencesValue.trim().length > 0) {
113
+ const ids = extractMessageIds(referencesValue);
114
+ for (const id of ids) {
115
+ if (!references.includes(id)) {
116
+ references.push(id);
117
+ }
118
+ }
119
+ }
120
+ // Thread ID is the first message in the references chain
121
+ const threadId = references.length > 0 ? references[0] : null;
122
+ return {
123
+ inReplyTo,
124
+ references,
125
+ threadId,
126
+ };
127
+ }
128
+ //# sourceMappingURL=threading.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"threading.js","sourceRoot":"","sources":["../src/threading.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Generic email headers as key-value pairs.
3
+ * Values can be a single string or an array of strings for multi-value headers.
4
+ */
5
+ export type EmailHeaders = Record<string, string | string[]>;
6
+ /**
7
+ * Headers for email threading (In-Reply-To and References).
8
+ */
9
+ export interface ThreadingHeaders {
10
+ readonly inReplyTo: string;
11
+ readonly references?: readonly string[];
12
+ }
13
+ /**
14
+ * Headers for list unsubscribe functionality (RFC 8058).
15
+ */
16
+ export interface ListUnsubscribeHeaders {
17
+ readonly listUnsubscribe: string;
18
+ readonly listUnsubscribePost?: string;
19
+ }
20
+ /**
21
+ * Parsed MDN (Message Disposition Notification) disposition.
22
+ */
23
+ export interface MdnDisposition {
24
+ readonly originalMessageId: string;
25
+ readonly recipient: string;
26
+ readonly disposition: 'displayed' | 'deleted' | 'dispatched' | 'processed' | 'denied' | 'failed';
27
+ }
28
+ /**
29
+ * Options for requesting an MDN read receipt.
30
+ */
31
+ export interface MdnRequestOptions {
32
+ readonly notifyEmail: string;
33
+ }
34
+ /**
35
+ * Parsed threading information from email headers.
36
+ */
37
+ export interface ParsedThreadingInfo {
38
+ readonly inReplyTo: string | null;
39
+ readonly references: readonly string[];
40
+ readonly threadId: string | null;
41
+ }
42
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAQA;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;AAE7D;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACzC;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;CACvC;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,WAAW,GAAG,SAAS,GAAG,YAAY,GAAG,WAAW,GAAG,QAAQ,GAAG,QAAQ,CAAC;CAClG;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IACvC,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC"}
package/dist/types.js ADDED
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ /*
3
+ Copyright (c) 2025 Bernier LLC
4
+
5
+ This file is licensed to the client under a limited-use license.
6
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
7
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":";AAAA;;;;;;EAME"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Create List-Unsubscribe headers per RFC 2369 and optionally RFC 8058 (one-click).
3
+ *
4
+ * @param url - The HTTPS unsubscribe URL (must be HTTPS)
5
+ * @param mailto - Optional mailto address for unsubscribe
6
+ * @param oneClick - If true, adds List-Unsubscribe-Post header for RFC 8058 one-click unsubscribe
7
+ * @returns Record with List-Unsubscribe and optionally List-Unsubscribe-Post headers
8
+ *
9
+ * @throws {HeaderError} If the URL is not HTTPS
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * const headers = createListUnsubscribeHeaders(
14
+ * 'https://example.com/unsubscribe?token=abc',
15
+ * 'unsubscribe@example.com',
16
+ * true
17
+ * );
18
+ * // {
19
+ * // 'List-Unsubscribe': '<https://example.com/unsubscribe?token=abc>, <mailto:unsubscribe@example.com>',
20
+ * // 'List-Unsubscribe-Post': 'List-Unsubscribe=One-Click'
21
+ * // }
22
+ * ```
23
+ */
24
+ export declare function createListUnsubscribeHeaders(url: string, mailto?: string, oneClick?: boolean): Record<string, string>;
25
+ //# sourceMappingURL=unsubscribe.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unsubscribe.d.ts","sourceRoot":"","sources":["../src/unsubscribe.ts"],"names":[],"mappings":"AAUA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,4BAA4B,CAC1C,GAAG,EAAE,MAAM,EACX,MAAM,CAAC,EAAE,MAAM,EACf,QAAQ,CAAC,EAAE,OAAO,GACjB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAsCxB"}
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ /*
3
+ Copyright (c) 2025 Bernier LLC
4
+
5
+ This file is licensed to the client under a limited-use license.
6
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
7
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.createListUnsubscribeHeaders = createListUnsubscribeHeaders;
11
+ const errors_1 = require("./errors");
12
+ /**
13
+ * Create List-Unsubscribe headers per RFC 2369 and optionally RFC 8058 (one-click).
14
+ *
15
+ * @param url - The HTTPS unsubscribe URL (must be HTTPS)
16
+ * @param mailto - Optional mailto address for unsubscribe
17
+ * @param oneClick - If true, adds List-Unsubscribe-Post header for RFC 8058 one-click unsubscribe
18
+ * @returns Record with List-Unsubscribe and optionally List-Unsubscribe-Post headers
19
+ *
20
+ * @throws {HeaderError} If the URL is not HTTPS
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * const headers = createListUnsubscribeHeaders(
25
+ * 'https://example.com/unsubscribe?token=abc',
26
+ * 'unsubscribe@example.com',
27
+ * true
28
+ * );
29
+ * // {
30
+ * // 'List-Unsubscribe': '<https://example.com/unsubscribe?token=abc>, <mailto:unsubscribe@example.com>',
31
+ * // 'List-Unsubscribe-Post': 'List-Unsubscribe=One-Click'
32
+ * // }
33
+ * ```
34
+ */
35
+ function createListUnsubscribeHeaders(url, mailto, oneClick) {
36
+ if (!url || url.trim().length === 0) {
37
+ throw new errors_1.HeaderError('Unsubscribe URL is required', {
38
+ code: errors_1.HeaderErrorCode.INVALID_HEADER,
39
+ context: { url },
40
+ });
41
+ }
42
+ // Validate HTTPS
43
+ const trimmedUrl = url.trim();
44
+ if (!trimmedUrl.toLowerCase().startsWith('https://')) {
45
+ throw new errors_1.HeaderError('Unsubscribe URL must use HTTPS', {
46
+ code: errors_1.HeaderErrorCode.INVALID_HEADER,
47
+ context: { url: trimmedUrl },
48
+ });
49
+ }
50
+ // Build List-Unsubscribe value
51
+ const parts = [`<${trimmedUrl}>`];
52
+ if (mailto !== undefined && mailto.trim().length > 0) {
53
+ const trimmedMailto = mailto.trim();
54
+ // Add mailto: prefix if not already present
55
+ const mailtoUri = trimmedMailto.startsWith('mailto:')
56
+ ? trimmedMailto
57
+ : `mailto:${trimmedMailto}`;
58
+ parts.push(`<${mailtoUri}>`);
59
+ }
60
+ const result = {
61
+ 'List-Unsubscribe': parts.join(', '),
62
+ };
63
+ // RFC 8058 one-click unsubscribe
64
+ if (oneClick === true) {
65
+ result['List-Unsubscribe-Post'] = 'List-Unsubscribe=One-Click';
66
+ }
67
+ return result;
68
+ }
69
+ //# sourceMappingURL=unsubscribe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unsubscribe.js","sourceRoot":"","sources":["../src/unsubscribe.ts"],"names":[],"mappings":";AAAA;;;;;;EAME;;AA2BF,oEA0CC;AAnED,qCAAwD;AAExD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,SAAgB,4BAA4B,CAC1C,GAAW,EACX,MAAe,EACf,QAAkB;IAElB,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACpC,MAAM,IAAI,oBAAW,CAAC,6BAA6B,EAAE;YACnD,IAAI,EAAE,wBAAe,CAAC,cAAc;YACpC,OAAO,EAAE,EAAE,GAAG,EAAE;SACjB,CAAC,CAAC;IACL,CAAC;IAED,iBAAiB;IACjB,MAAM,UAAU,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAC9B,IAAI,CAAC,UAAU,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QACrD,MAAM,IAAI,oBAAW,CAAC,gCAAgC,EAAE;YACtD,IAAI,EAAE,wBAAe,CAAC,cAAc;YACpC,OAAO,EAAE,EAAE,GAAG,EAAE,UAAU,EAAE;SAC7B,CAAC,CAAC;IACL,CAAC;IAED,+BAA+B;IAC/B,MAAM,KAAK,GAAa,CAAC,IAAI,UAAU,GAAG,CAAC,CAAC;IAC5C,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrD,MAAM,aAAa,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC;QACpC,4CAA4C;QAC5C,MAAM,SAAS,GAAG,aAAa,CAAC,UAAU,CAAC,SAAS,CAAC;YACnD,CAAC,CAAC,aAAa;YACf,CAAC,CAAC,UAAU,aAAa,EAAE,CAAC;QAC9B,KAAK,CAAC,IAAI,CAAC,IAAI,SAAS,GAAG,CAAC,CAAC;IAC/B,CAAC;IAED,MAAM,MAAM,GAA2B;QACrC,kBAAkB,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC;KACrC,CAAC;IAEF,iCAAiC;IACjC,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;QACtB,MAAM,CAAC,uBAAuB,CAAC,GAAG,4BAA4B,CAAC;IACjE,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
package/package.json CHANGED
@@ -1,10 +1,53 @@
1
1
  {
2
2
  "name": "@bernierllc/email-headers",
3
- "version": "0.0.1",
4
- "description": "OIDC trusted publishing setup package for @bernierllc/email-headers",
3
+ "version": "0.2.0",
4
+ "description": "Email header construction and parsing utilities for threading, unsubscribe, and read receipts with zero external dependencies",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist",
9
+ "README.md",
10
+ "LICENSE"
11
+ ],
5
12
  "keywords": [
6
- "oidc",
7
- "trusted-publishing",
8
- "setup"
9
- ]
10
- }
13
+ "email",
14
+ "headers",
15
+ "threading",
16
+ "unsubscribe",
17
+ "mdn",
18
+ "read-receipt",
19
+ "rfc-8058",
20
+ "rfc-3798",
21
+ "typescript"
22
+ ],
23
+ "author": "Bernier LLC",
24
+ "license": "PROPRIETARY",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "https://github.com/bernier-llc/tools"
28
+ },
29
+ "devDependencies": {
30
+ "@types/jest": "^29.5.0",
31
+ "@types/node": "^20.0.0",
32
+ "jest": "^29.5.0",
33
+ "ts-jest": "^29.1.0",
34
+ "typescript": "^5.0.0"
35
+ },
36
+ "engines": {
37
+ "node": ">=18.0.0"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public",
41
+ "registry": "https://registry.npmjs.org/"
42
+ },
43
+ "scripts": {
44
+ "build": "tsc",
45
+ "test": "jest",
46
+ "test:run": "jest --watchAll=false --forceExit",
47
+ "test:coverage": "jest --coverage",
48
+ "lint": "eslint src --ext .ts",
49
+ "lint:fix": "eslint src --ext .ts --fix",
50
+ "clean": "rm -rf dist",
51
+ "prebuild": "npm run clean"
52
+ }
53
+ }
package/README.md DELETED
@@ -1,45 +0,0 @@
1
- # @bernierllc/email-headers
2
-
3
- ## ⚠️ IMPORTANT NOTICE ⚠️
4
-
5
- **This package is created solely for the purpose of setting up OIDC (OpenID Connect) trusted publishing with npm.**
6
-
7
- This is **NOT** a functional package and contains **NO** code or functionality beyond the OIDC setup configuration.
8
-
9
- ## Purpose
10
-
11
- This package exists to:
12
- 1. Configure OIDC trusted publishing for the package name `@bernierllc/email-headers`
13
- 2. Enable secure, token-less publishing from CI/CD workflows
14
- 3. Establish provenance for packages published under this name
15
-
16
- ## What is OIDC Trusted Publishing?
17
-
18
- OIDC trusted publishing allows package maintainers to publish packages directly from their CI/CD workflows without needing to manage npm access tokens. Instead, it uses OpenID Connect to establish trust between the CI/CD provider (like GitHub Actions) and npm.
19
-
20
- ## Setup Instructions
21
-
22
- To properly configure OIDC trusted publishing for this package:
23
-
24
- 1. Go to [npmjs.com](https://www.npmjs.com/) and navigate to your package settings
25
- 2. Configure the trusted publisher (e.g., GitHub Actions)
26
- 3. Specify the repository and workflow that should be allowed to publish
27
- 4. Use the configured workflow to publish your actual package
28
-
29
- ## DO NOT USE THIS PACKAGE
30
-
31
- This package is a placeholder for OIDC configuration only. It:
32
- - Contains no executable code
33
- - Provides no functionality
34
- - Should not be installed as a dependency
35
- - Exists only for administrative purposes
36
-
37
- ## More Information
38
-
39
- For more details about npm's trusted publishing feature, see:
40
- - [npm Trusted Publishing Documentation](https://docs.npmjs.com/generating-provenance-statements)
41
- - [GitHub Actions OIDC Documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
42
-
43
- ---
44
-
45
- **Maintained for OIDC setup purposes only**