@kyyinfinite/lumina 1.0.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.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +629 -0
  3. package/examples/ai-rich.js +84 -0
  4. package/examples/button.js +57 -0
  5. package/examples/carousel.js +51 -0
  6. package/examples/interactive.js +102 -0
  7. package/examples/media.js +66 -0
  8. package/examples/simple-bot.js +56 -0
  9. package/package.json +86 -0
  10. package/src/builders/ai-rich.js +644 -0
  11. package/src/builders/base.js +109 -0
  12. package/src/builders/button-v2.js +159 -0
  13. package/src/builders/button.js +398 -0
  14. package/src/builders/card.js +168 -0
  15. package/src/builders/carousel.js +122 -0
  16. package/src/builders/index.d.ts +1 -0
  17. package/src/builders/index.js +13 -0
  18. package/src/client/bot.js +192 -0
  19. package/src/client/connection.js +180 -0
  20. package/src/errors.js +88 -0
  21. package/src/index.d.ts +458 -0
  22. package/src/index.js +152 -0
  23. package/src/media/fetch.js +67 -0
  24. package/src/media/image.js +86 -0
  25. package/src/media/index.d.ts +1 -0
  26. package/src/media/index.js +12 -0
  27. package/src/media/resolver.js +115 -0
  28. package/src/media/uploader.js +65 -0
  29. package/src/media/video.js +195 -0
  30. package/src/parsers/code-tokenizer-keywords.js +128 -0
  31. package/src/parsers/code-tokenizer.js +191 -0
  32. package/src/parsers/index.d.ts +1 -0
  33. package/src/parsers/index.js +11 -0
  34. package/src/parsers/inline-entity.js +231 -0
  35. package/src/parsers/table-metadata.js +69 -0
  36. package/src/proto/enums.js +170 -0
  37. package/src/proto/index.d.ts +1 -0
  38. package/src/proto/index.js +13 -0
  39. package/src/proto/layouts.js +89 -0
  40. package/src/proto/primitives.js +181 -0
  41. package/src/proto/relay-nodes.js +55 -0
  42. package/src/proto/rich-response.js +144 -0
  43. package/src/proto/updater.js +318 -0
  44. package/src/services/index.d.ts +1 -0
  45. package/src/services/index.js +10 -0
  46. package/src/services/media-service.js +184 -0
  47. package/src/services/message-service.js +288 -0
  48. package/src/services/proto-service.js +90 -0
  49. package/src/utils/id.js +42 -0
  50. package/src/utils/logger.js +65 -0
  51. package/src/utils/mime.js +104 -0
  52. package/src/utils/promise.js +52 -0
  53. package/src/utils/validator.js +129 -0
@@ -0,0 +1,191 @@
1
+ /**
2
+ * @file code-tokenizer.js
3
+ * @module lumina/parsers/code-tokenizer
4
+ *
5
+ * Lightweight hand-rolled lexer for code-block syntax highlighting inside
6
+ * AI Rich Responses. Returns two parallel representations:
7
+ *
8
+ * - `codeBlock` — array of `{ codeContent, highlightType }` where
9
+ * `highlightType` is the numeric 0-5 enum used by WAProto.
10
+ * - `unifiedBlocks` — array of `{ content, type }` with the string label
11
+ * (`'DEFAULT' | 'KEYWORD' | 'METHOD' | 'STR' | 'NUMBER' | 'COMMENT'`).
12
+ *
13
+ * The legacy `AIRich.tokenizer` rebuilt its keyword Set on every call and
14
+ * inlined all 660 lines of language maps. Lumina splits the catalog into
15
+ * `code-tokenizer-keywords.js` (built once) and keeps this file focused on
16
+ * the lexer itself (~120 lines).
17
+ */
18
+
19
+ import { HighlightType, HighlightLabel } from '../proto/enums.js'
20
+ import { KEYWORDS, SLASH_COMMENT_LANGS, HASH_COMMENT_LANGS, BLOCK_COMMENT_LANGS } from './code-tokenizer-keywords.js'
21
+
22
+ /**
23
+ * @typedef {object} TokenizedCode
24
+ * @property {Array<{ codeContent: string, highlightType: number }>} codeBlock
25
+ * @property {Array<{ content: string, type: string }>} unifiedBlocks
26
+ */
27
+
28
+ /** Identifier character class per language (CSS & HTML allow `-` / `:`). */
29
+ function identifierChar(lang) {
30
+ switch (lang) {
31
+ case 'css':
32
+ return /[a-zA-Z0-9_$-]/
33
+ case 'html':
34
+ return /[a-zA-Z0-9_$:-]/
35
+ default:
36
+ return /[a-zA-Z0-9_$]/
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Tokenize a code string for syntax highlighting.
42
+ *
43
+ * @param {string} code
44
+ * @param {string} [lang='javascript'] Lower-case language id. Unknown ids degrade gracefully to plain text.
45
+ * @returns {TokenizedCode}
46
+ */
47
+ export function tokenizeCode(code, lang = 'javascript') {
48
+ if (typeof code !== 'string' || code.length === 0) {
49
+ return { codeBlock: [], unifiedBlocks: [] }
50
+ }
51
+
52
+ // Plain-text fast-path.
53
+ if (!lang || lang === 'txt' || lang === 'text' || lang === 'plaintext') {
54
+ return {
55
+ codeBlock: [{ codeContent: code, highlightType: HighlightType.DEFAULT }],
56
+ unifiedBlocks: [{ content: code, type: HighlightLabel[HighlightType.DEFAULT] }],
57
+ }
58
+ }
59
+
60
+ const lower = lang.toLowerCase()
61
+ const keywords = KEYWORDS[lower] ?? new Set()
62
+ const isIdent = identifierChar(lower)
63
+ const supportsSlashComments = SLASH_COMMENT_LANGS.has(lower)
64
+ const supportsHashComments = HASH_COMMENT_LANGS.has(lower)
65
+ const supportsBlockComments = BLOCK_COMMENT_LANGS.has(lower)
66
+
67
+ /** @type {Array<{ codeContent: string, highlightType: number }>} */
68
+ const tokens = []
69
+
70
+ /**
71
+ * Push a token, merging with the previous one if both share the same type.
72
+ */
73
+ const push = (content, type) => {
74
+ if (!content) return
75
+ const last = tokens[tokens.length - 1]
76
+ if (last && last.highlightType === type) last.codeContent += content
77
+ else tokens.push({ codeContent: content, highlightType: type })
78
+ }
79
+
80
+ let i = 0
81
+ while (i < code.length) {
82
+ const c = code[i]
83
+
84
+ // Whitespace.
85
+ if (/\s/.test(c)) {
86
+ const start = i
87
+ while (i < code.length && /\s/.test(code[i])) i++
88
+ push(code.slice(start, i), HighlightType.DEFAULT)
89
+ continue
90
+ }
91
+
92
+ // Block comment: /* ... */
93
+ if (supportsBlockComments && c === '/' && code[i + 1] === '*') {
94
+ const start = i
95
+ i += 2
96
+ while (i < code.length && !(code[i] === '*' && code[i + 1] === '/')) i++
97
+ i = Math.min(i + 2, code.length)
98
+ push(code.slice(start, i), HighlightType.COMMENT)
99
+ continue
100
+ }
101
+
102
+ // Slash line comment: //
103
+ if (supportsSlashComments && c === '/' && code[i + 1] === '/') {
104
+ const start = i
105
+ while (i < code.length && code[i] !== '\n') i++
106
+ push(code.slice(start, i), HighlightType.COMMENT)
107
+ continue
108
+ }
109
+
110
+ // Hash line comment: # (python, bash, php, rust)
111
+ if (supportsHashComments && c === '#') {
112
+ const start = i
113
+ while (i < code.length && code[i] !== '\n') i++
114
+ push(code.slice(start, i), HighlightType.COMMENT)
115
+ continue
116
+ }
117
+
118
+ // String literals: ', ", `
119
+ if (c === '"' || c === "'" || c === '`') {
120
+ const start = i
121
+ const quote = c
122
+ i++
123
+ while (i < code.length) {
124
+ if (code[i] === '\\' && i + 1 < code.length) i += 2
125
+ else if (code[i] === quote) {
126
+ i++
127
+ break
128
+ } else i++
129
+ }
130
+ push(code.slice(start, i), HighlightType.STRING)
131
+ continue
132
+ }
133
+
134
+ // Numbers.
135
+ if (/[0-9]/.test(c)) {
136
+ const start = i
137
+ while (i < code.length && /[0-9._a-fxA-fX]/.test(code[i])) i++
138
+ push(code.slice(start, i), HighlightType.NUMBER)
139
+ continue
140
+ }
141
+
142
+ // Identifiers & keywords.
143
+ if (/[a-zA-Z_$]/.test(c)) {
144
+ const start = i
145
+ while (i < code.length && isIdent.test(code[i])) i++
146
+ const word = code.slice(start, i)
147
+
148
+ let type = HighlightType.DEFAULT
149
+
150
+ if (keywords.has(word)) {
151
+ type = HighlightType.KEYWORD
152
+ } else if (lower === 'css') {
153
+ // Property name? next non-space char is ':'.
154
+ let j = i
155
+ while (j < code.length && /\s/.test(code[j])) j++
156
+ if (code[j] === ':') type = HighlightType.KEYWORD
157
+ } else if (lower === 'html') {
158
+ // Tag name? previous non-space char is '<' or '</'.
159
+ let p = start - 1
160
+ while (p >= 0 && /\s/.test(code[p])) p--
161
+ if (code[p] === '<' || (code[p] === '/' && code[p - 1] === '<')) {
162
+ type = HighlightType.KEYWORD
163
+ }
164
+ }
165
+
166
+ // Function call? Next non-space char is '('.
167
+ if (type === HighlightType.DEFAULT) {
168
+ let j = i
169
+ while (j < code.length && /\s/.test(code[j])) j++
170
+ if (code[j] === '(') type = HighlightType.METHOD
171
+ }
172
+
173
+ push(word, type)
174
+ continue
175
+ }
176
+
177
+ // Everything else: punctuation.
178
+ push(c, HighlightType.DEFAULT)
179
+ i++
180
+ }
181
+
182
+ return {
183
+ codeBlock: tokens,
184
+ unifiedBlocks: tokens.map((t) => ({
185
+ content: t.codeContent,
186
+ type: HighlightLabel[t.highlightType],
187
+ })),
188
+ }
189
+ }
190
+
191
+ export default tokenizeCode
@@ -0,0 +1 @@
1
+ export * from '../index.d.ts'
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @file parsers/index.js
3
+ * @module lumina/parsers
4
+ *
5
+ * Barrel re-export for the parsers layer.
6
+ */
7
+
8
+ export { extractInlineEntities } from './inline-entity.js'
9
+ export { tokenizeCode } from './code-tokenizer.js'
10
+ export { toTableMetadata } from './table-metadata.js'
11
+ export { KEYWORDS } from './code-tokenizer-keywords.js'
@@ -0,0 +1,231 @@
1
+ /**
2
+ * @file inline-entity.js
3
+ * @module lumina/parsers/inline-entity
4
+ *
5
+ * extractInlineEntities — parses three markdown-like inline syntaxes inside
6
+ * AI-rich text content and returns the rewritten text plus proto-ready
7
+ * inline-entity metadata.
8
+ *
9
+ * Supported syntaxes:
10
+ *
11
+ * [text](url) → hyperlink (untrusted if `url` starts with `!`)
12
+ * [](url) → citation (renders as a numeric superscript)
13
+ * [text]<url|w|h|fh|p> → latex (math image with dimensions)
14
+ *
15
+ * The rewritten text replaces each `[...]` span with `{{KEY}}...{{/KEY}}`
16
+ * placeholders that WhatsApp's rich-response renderer substitutes back with
17
+ * the entity's interactive UI element.
18
+ *
19
+ * This is the modernised, de-obfuscated successor to the legacy `extractIE`
20
+ * function in `_build-m.js`. Key changes:
21
+ *
22
+ * 1. The `NIXEL_` prefix (obfuscated as `\u004E\u0049\u0058\u0045\u004C_`)
23
+ * is replaced by the configurable `LUMINA_` prefix.
24
+ * 2. The off-by-one bug on citation keys is fixed: every counter is now
25
+ * zero-based and keys match `reference_id` consistently.
26
+ * 3. Magic numbers (font_height, padding, default width/height) are pulled
27
+ * into named constants.
28
+ * 4. The `createIE` switch-by-if is replaced by a typed lookup table.
29
+ * 5. Pure function, no side-effects — easily unit-testable.
30
+ */
31
+
32
+ import { entityKey } from '../utils/id.js'
33
+
34
+ /** @typedef {'hyperlink'|'citation'|'latex'} EntityType */
35
+
36
+ /**
37
+ * Default LaTeX rendering parameters. Sourced from the legacy implementation.
38
+ * Adjust if WhatsApp updates its rendering defaults.
39
+ */
40
+ const LATEX_DEFAULTS = Object.freeze({
41
+ width: 100,
42
+ height: 100,
43
+ fontHeight: 83.333333333333,
44
+ padding: 15,
45
+ })
46
+
47
+ /**
48
+ * Per-type metadata shaper. Replaces the legacy `createIE` switch.
49
+ *
50
+ * @type {Record<EntityType, (item: any) => object>}
51
+ */
52
+ const SHAPERS = {
53
+ hyperlink: (item) => ({
54
+ key: item.key,
55
+ metadata: {
56
+ display_name: item.text,
57
+ is_trusted: item.isTrusted,
58
+ url: item.url,
59
+ __typename: 'GenAIInlineLinkItem',
60
+ },
61
+ }),
62
+ citation: (item) => ({
63
+ key: item.key,
64
+ metadata: {
65
+ reference_id: item.referenceId,
66
+ reference_url: item.url,
67
+ reference_title: item.url,
68
+ reference_display_name: item.url,
69
+ sources: [],
70
+ __typename: 'GenAISearchCitationItem',
71
+ },
72
+ }),
73
+ latex: (item) => ({
74
+ key: item.key,
75
+ metadata: {
76
+ latex_expression: item.text,
77
+ latex_image: {
78
+ url: item.url,
79
+ width: Number(item.width) || LATEX_DEFAULTS.width,
80
+ height: Number(item.height) || LATEX_DEFAULTS.height,
81
+ },
82
+ font_height: Number(item.fontHeight) || LATEX_DEFAULTS.fontHeight,
83
+ padding: Number(item.padding) || LATEX_DEFAULTS.padding,
84
+ __typename: 'GenAILatexItem',
85
+ },
86
+ }),
87
+ }
88
+
89
+ /**
90
+ * @typedef {object} ExtractOptions
91
+ * @property {boolean} [extract=true] Master switch — when false, returns the text unchanged.
92
+ * @property {boolean} [hyperlink=true] Parse `[text](url)` syntax.
93
+ * @property {boolean} [citation=true] Parse `[](url)` syntax.
94
+ * @property {boolean} [latex=true] Parse `[text]<url|w|h|fh|p>` syntax.
95
+ * @property {string} [prefix='LUMINA'] Override the entity-key prefix.
96
+ */
97
+
98
+ /**
99
+ * @typedef {object} ExtractResult
100
+ * @property {string} text Rewritten text with `{{KEY}}…{{/KEY}}` placeholders.
101
+ * @property {Array<{ type: EntityType, key: string, [k: string]: any }>} entities
102
+ * Raw per-entity descriptors (one per matched span).
103
+ * @property {Array<{ key: string, metadata: object }>} metadata
104
+ * Proto-ready inline-entity metadata, ready to attach to a primitive.
105
+ */
106
+
107
+ /**
108
+ * Parse inline entities from text.
109
+ *
110
+ * @param {string} text
111
+ * @param {ExtractOptions} [opts]
112
+ * @returns {ExtractResult}
113
+ */
114
+ export function extractInlineEntities(text, opts = {}) {
115
+ const {
116
+ extract = true,
117
+ hyperlink = true,
118
+ citation = true,
119
+ latex = true,
120
+ prefix = 'LUMINA',
121
+ } = opts
122
+
123
+ if (!extract || typeof text !== 'string' || text.length === 0) {
124
+ return { text: text ?? '', entities: [], metadata: [] }
125
+ }
126
+
127
+ /** @type {Array<{ type: EntityType, key: string, [k: string]: any }>} */
128
+ const entities = []
129
+ /** @type {Array<{ key: string, metadata: object }>} */
130
+ const metadata = []
131
+ let rewritten = ''
132
+ let last = 0
133
+ let citationCount = 0
134
+ let hyperlinkCount = 0
135
+ let latexCount = 0
136
+
137
+ /** Bracket stack — supports nested brackets inside the link text. */
138
+ const stack = []
139
+
140
+ for (let i = 0; i < text.length; i++) {
141
+ const ch = text[i]
142
+
143
+ // Push `[` (escaped `\[` ignored).
144
+ if (ch === '[' && text[i - 1] !== '\\') {
145
+ stack.push(i)
146
+ continue
147
+ }
148
+
149
+ // `]` only triggers when followed by `(` (link) or `<` (latex).
150
+ if (ch === ']' && (text[i + 1] === '(' || text[i + 1] === '<')) {
151
+ const start = stack.pop()
152
+ if (start == null) continue
153
+
154
+ const open = text[i + 1]
155
+ const close = open === '(' ? ')' : '>'
156
+ const type = /** @type {EntityType} */ (open === '(' ? 'hyperlink-or-citation' : 'latex')
157
+
158
+ // Walk to the matching close, respecting backslash escapes & nesting.
159
+ let end = i + 2
160
+ let depth = 1
161
+ while (end < text.length && depth) {
162
+ if (text[end] === open && text[end - 1] !== '\\') depth++
163
+ else if (text[end] === close && text[end - 1] !== '\\') depth--
164
+ end++
165
+ }
166
+ if (depth) continue // unbalanced — skip.
167
+
168
+ const raw = text.slice(start + 1, i).trim()
169
+ const url = text.slice(i + 2, end - 1).trim()
170
+
171
+ /** @type {{ type: EntityType, key: string, [k: string]: any } | null} */
172
+ let entry = null
173
+
174
+ if (type === 'latex') {
175
+ if (!latex) continue
176
+ const [txt = '', width = null, height = null, fontHeight = null, padding = null] =
177
+ raw.split('|')
178
+ const key = entityKey('LATEX', latexCount++)
179
+ entry = {
180
+ type: 'latex',
181
+ key,
182
+ text: txt,
183
+ url,
184
+ width,
185
+ height,
186
+ fontHeight,
187
+ padding,
188
+ }
189
+ rewritten += text.slice(last, start) + `{{${key}}}${txt || 'image'}{{/${key}}}`
190
+ } else if (raw) {
191
+ if (!hyperlink) continue
192
+ // Untrusted link → URL prefixed with `!`.
193
+ const trusted = !url.startsWith('!')
194
+ const cleanUrl = trusted ? url : url.slice(1)
195
+ const key = entityKey('HYPERLINK', hyperlinkCount++)
196
+ entry = {
197
+ type: 'hyperlink',
198
+ key,
199
+ text: raw,
200
+ url: cleanUrl,
201
+ isTrusted: trusted,
202
+ }
203
+ rewritten += text.slice(last, start) + `{{${key}}}${cleanUrl}{{/${key}}}`
204
+ } else {
205
+ if (!citation) continue
206
+ const key = entityKey('CITATION', citationCount)
207
+ citationCount++
208
+ entry = {
209
+ type: 'citation',
210
+ key,
211
+ referenceId: citationCount, // 1-based, matches key index (zero-based +1)
212
+ text: '',
213
+ url,
214
+ }
215
+ rewritten += text.slice(last, start) + `{{${key}}}${url}{{/${key}}}`
216
+ }
217
+
218
+ last = end
219
+ i = end - 1
220
+
221
+ entities.push(entry)
222
+ const shaped = SHAPERS[entry.type](entry)
223
+ if (shaped) metadata.push(shaped)
224
+ }
225
+ }
226
+
227
+ rewritten += text.slice(last)
228
+ return { text: rewritten, entities, metadata }
229
+ }
230
+
231
+ export default extractInlineEntities
@@ -0,0 +1,69 @@
1
+ /**
2
+ * @file table-metadata.js
3
+ * @module lumina/parsers/table-metadata
4
+ *
5
+ * Convert a 2-D array of strings into the metadata shape WhatsApp expects
6
+ * inside a `richResponseMessage.submessages[].tableMetadata`.
7
+ *
8
+ * The legacy `AIRich.toTableMetadata` always returned `title: ''` with no
9
+ * way to set it. Lumina accepts a `title` option.
10
+ */
11
+
12
+ import { extractInlineEntities } from './inline-entity.js'
13
+
14
+ /**
15
+ * @typedef {object} TableMetadata
16
+ * @property {string} title
17
+ * @property {Array<{ items: string[], isHeading?: boolean }>} rows Submessage metadata.
18
+ * @property {Array<{ is_header: boolean, cells: string[], markdown_cells?: Array<{ text: string, inline_entities?: object[] }> }>} unifiedRows Section primitive rows.
19
+ */
20
+
21
+ /**
22
+ * @param {string[][]} table 2-D array of strings. First row is the header.
23
+ * @param {object} [opts]
24
+ * @param {string} [opts.title='']
25
+ * @param {boolean} [opts.hyperlink=true]
26
+ * @param {boolean} [opts.citation=true]
27
+ * @param {boolean} [opts.latex=true]
28
+ * @returns {TableMetadata}
29
+ */
30
+ export function toTableMetadata(table, opts = {}) {
31
+ if (!Array.isArray(table) || !table.every((row) => Array.isArray(row) && row.every((c) => typeof c === 'string'))) {
32
+ throw new TypeError('Table must be a 2-D array of strings')
33
+ }
34
+
35
+ const { title = '', hyperlink = true, citation = true, latex = true } = opts
36
+
37
+ const [header, ...rows] = table
38
+ const maxLen = Math.max(header.length, ...rows.map((r) => r.length))
39
+
40
+ /** Pad a row to the table's max column count. */
41
+ const normalize = (r) => [...r, ...Array(Math.max(0, maxLen - r.length)).fill('')]
42
+
43
+ const unifiedRows = [
44
+ { is_header: true, cells: normalize(header) },
45
+ ...rows.map((r) => ({ is_header: false, cells: normalize(r) })),
46
+ ].map((row) => {
47
+ const markdownCells = row.cells.map((cell) => {
48
+ const extracted = extractInlineEntities(cell, { hyperlink, citation, latex })
49
+ const out = { text: extracted.text }
50
+ if (extracted.metadata.length) out.inline_entities = extracted.metadata
51
+ return out
52
+ })
53
+
54
+ const hasInline = markdownCells.some((c) => c.inline_entities?.length)
55
+ return {
56
+ ...row,
57
+ ...(hasInline ? { markdown_cells: markdownCells } : {}),
58
+ }
59
+ })
60
+
61
+ const rowsMeta = unifiedRows.map((r) => ({
62
+ items: r.cells,
63
+ ...(r.is_header ? { isHeading: true } : {}),
64
+ }))
65
+
66
+ return { title, rows: rowsMeta, unifiedRows }
67
+ }
68
+
69
+ export default toTableMetadata
@@ -0,0 +1,170 @@
1
+ /**
2
+ * @file enums.js
3
+ * @module lumina/proto/enums
4
+ *
5
+ * Single source of truth for every magic number / magic string the WhatsApp
6
+ * protocol expects inside interactive messages, rich responses, and relay
7
+ * nodes. Update the wire format here once, and the whole framework follows.
8
+ *
9
+ * Replaces the 20+ scattered magic values documented in Tahap-1 analysis:
10
+ * - messageType: 1, 2, 4, 5, 9
11
+ * - forwardOrigin: 4
12
+ * - headerType: 0,1,3,4,5,6
13
+ * - native_flow v: '9'
14
+ * - botJid: '0@bot'
15
+ * - 12x __typename string literals
16
+ * - highlightType: 0-5
17
+ * - imagine_type: 'IMAGE' | 'ANIMATE'
18
+ * - etc.
19
+ */
20
+
21
+ /**
22
+ * Rich-response submessage type. Stored in `richResponseMessage.submessages[].messageType`.
23
+ */
24
+ export const MessageType = Object.freeze({
25
+ /** Grid-image rich response. */
26
+ RICH_RESPONSE: 1,
27
+ /** Plain text submessage (also used as fallback for video/product/post). */
28
+ TEXT: 2,
29
+ /** Table metadata submessage. */
30
+ TABLE: 4,
31
+ /** Code-block metadata submessage. */
32
+ CODE: 5,
33
+ /** Reels / contentItems metadata submessage. */
34
+ REELS: 9,
35
+ })
36
+
37
+ /**
38
+ * `contextInfo.forwardOrigin` enum used by WhatsApp for AI bot messages.
39
+ */
40
+ export const ForwardOrigin = Object.freeze({
41
+ BOT: 4,
42
+ })
43
+
44
+ /**
45
+ * ButtonsMessage headerType enum. The legacy ButtonV2 fallback used 6
46
+ * (LOCATION_THUMBNAIL) without documenting it — now explicit.
47
+ */
48
+ export const HeaderType = Object.freeze({
49
+ EMPTY: 0,
50
+ TEXT: 1,
51
+ IMAGE: 3,
52
+ VIDEO: 4,
53
+ DOCUMENT: 5,
54
+ /** Pre-existing hack: location message with a JPEG thumbnail. */
55
+ LOCATION_THUMBNAIL: 6,
56
+ })
57
+
58
+ /**
59
+ * Native-flow biz/interactive node versions.
60
+ */
61
+ export const NativeFlow = Object.freeze({
62
+ /** Outer `<interactive v="1">` version. */
63
+ OUTER_VERSION: '1',
64
+ /** Inner `<native_flow v="9">` version. */
65
+ FLOW_VERSION: '9',
66
+ /** Default flow name when mixing button types. */
67
+ FLOW_NAME_MIXED: 'mixed',
68
+ TYPE: 'native_flow',
69
+ BIZ_TAG: 'biz',
70
+ INTERACTIVE_TAG: 'interactive',
71
+ })
72
+
73
+ /**
74
+ * Hardcoded bot JID that WhatsApp expects inside `forwardedAiBotMessageInfo`.
75
+ */
76
+ export const BOT_JID = '0@bot'
77
+
78
+ /**
79
+ * Layout view-model kinds. Each becomes `GenAI{Kind}LayoutViewModel`.
80
+ */
81
+ export const LayoutKind = Object.freeze({
82
+ SINGLE: 'Single',
83
+ HSCROLL: 'HScroll',
84
+ ACTION_ROW: 'ActionRow',
85
+ })
86
+
87
+ /**
88
+ * Code-tokenizer highlight type numbers → string labels. Matches the
89
+ * `TYPE_MAP` constant in the legacy AIRich.tokenizer.
90
+ */
91
+ export const HighlightType = Object.freeze({
92
+ DEFAULT: 0,
93
+ KEYWORD: 1,
94
+ METHOD: 2,
95
+ STRING: 3,
96
+ NUMBER: 4,
97
+ COMMENT: 5,
98
+ })
99
+
100
+ /** String labels corresponding to {@link HighlightType}. */
101
+ export const HighlightLabel = Object.freeze({
102
+ 0: 'DEFAULT',
103
+ 1: 'KEYWORD',
104
+ 2: 'METHOD',
105
+ 3: 'STR',
106
+ 4: 'NUMBER',
107
+ 5: 'COMMENT',
108
+ })
109
+
110
+ /** `imagine_type` field on GenAIImaginePrimitive. */
111
+ export const ImagineType = Object.freeze({
112
+ IMAGE: 'IMAGE',
113
+ ANIMATE: 'ANIMATE',
114
+ })
115
+
116
+ /** `source_type` field on GenAISearchResultPrimitive. */
117
+ export const SourceType = Object.freeze({
118
+ THIRD_PARTY: 'THIRD_PARTY',
119
+ })
120
+
121
+ /** `prompt_type` field on GenAIFollowUpSuggestionPillPrimitive. */
122
+ export const PromptType = Object.freeze({
123
+ SUGGESTED_PROMPT: 'SUGGESTED_PROMPT',
124
+ })
125
+
126
+ /** `sessionTransparencyType` field on botMetadata. */
127
+ export const SessionTransparencyType = Object.freeze({
128
+ DEFAULT: 1,
129
+ })
130
+
131
+ /**
132
+ * All GenAI `__typename` strings. Single source of truth — typo-proof.
133
+ *
134
+ * NOTE: legacy code had `GenATableUXPrimitive` (missing the second 'I').
135
+ * Verified against current WhatsApp Web wire format → the correct spelling
136
+ * is `GenAITableUXPrimitive`. Lumina corrects this.
137
+ */
138
+ export const TYPENAME = Object.freeze({
139
+ MARKDOWN_TEXT: 'GenAIMarkdownTextUXPrimitive',
140
+ CODE: 'GenAICodeUXPrimitive',
141
+ TABLE: 'GenAITableUXPrimitive',
142
+ SEARCH_RESULT: 'GenAISearchResultPrimitive',
143
+ REEL: 'GenAIReelPrimitive',
144
+ IMAGINE: 'GenAIImaginePrimitive',
145
+ PRODUCT_CARD: 'GenAIProductItemCardPrimitive',
146
+ POST: 'GenAIPostPrimitive',
147
+ METADATA_TEXT: 'GenAIMetadataTextPrimitive',
148
+ FOLLOW_UP_PILL: 'GenAIFollowUpSuggestionPillPrimitive',
149
+ UNIFIED_SECTION: 'GenAIUnifiedResponseSection',
150
+ /** Layout view-models are suffixed `GenAI{Kind}LayoutViewModel`. */
151
+ layout(kind) {
152
+ return `GenAI${kind}LayoutViewModel`
153
+ },
154
+ })
155
+
156
+ export default {
157
+ MessageType,
158
+ ForwardOrigin,
159
+ HeaderType,
160
+ NativeFlow,
161
+ BOT_JID,
162
+ LayoutKind,
163
+ HighlightType,
164
+ HighlightLabel,
165
+ ImagineType,
166
+ SourceType,
167
+ PromptType,
168
+ SessionTransparencyType,
169
+ TYPENAME,
170
+ }
@@ -0,0 +1 @@
1
+ export * from '../index.d.ts'
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @file proto/index.js
3
+ * @module lumina/proto
4
+ *
5
+ * Barrel re-export for the entire proto catalog. Subpath `@kyyinfinite/lumina/proto`.
6
+ */
7
+
8
+ export * from './enums.js'
9
+ export * from './primitives.js'
10
+ export * from './layouts.js'
11
+ export * from './relay-nodes.js'
12
+ export * from './rich-response.js'
13
+ export { ProtoUpdater, transformToESM, applyKnownFixes } from './updater.js'