@oxidezap/baileyrs 0.1.0 → 0.1.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.
package/README.md CHANGED
@@ -220,6 +220,76 @@ A few behaviors that differ from upstream — almost always to your advantage:
220
220
  `(err as Boom).output.statusCode` pattern works unchanged. If your
221
221
  `package.json` was pulling `@hapi/boom` only for baileys, you can drop
222
222
  the dependency.
223
+ - **Your key store also holds bridge state, so "empty" is not "unpaired".**
224
+ See [Bridge state in your key store](#bridge-state-in-your-key-store) — this
225
+ one can break a boot path, so it has its own section.
226
+
227
+ ### Bridge state in your key store
228
+
229
+ Upstream Baileys keeps engine state in `creds` (persisted by `saveCreds`) and
230
+ puts only Signal key material in `keys`. baileyrs uses that same `keys` store
231
+ as the persistence channel for the Rust core's own state as well: its device
232
+ record, and the byte-level records the Signal namespaces are projected from.
233
+ Those live under namespaces reserved with the **`bridge-` prefix**:
234
+
235
+ | namespace | what it holds |
236
+ | --- | --- |
237
+ | `bridge-native-*` | the core's own encoding of a namespace that also has a Baileys projection (`bridge-native-session`, `bridge-native-prekey`, `bridge-native-device`, …) |
238
+ | `bridge-*` | core records with no Baileys equivalent (`bridge-signed-prekey`, `bridge-sent-message`, `bridge-msg-secret`, `bridge-meta`, …) |
239
+
240
+ No upstream Baileys namespace starts with `bridge-`, and none of your data is
241
+ stored under one. Which of them you actually see depends on what the engine
242
+ touches; `bridge-native-device` shows up first, because the core reads its
243
+ device record while the socket is still `connecting` — **before any QR, before
244
+ any pairing**.
245
+
246
+ That last point is the one that bites. A store that counted rows to decide
247
+ whether this was a first run reports a session that does not exist:
248
+
249
+ ```js
250
+ // WRONG on baileyrs: bridge-native-device is already in the table before pairing,
251
+ // so this never reports empty again — the bot skips its pairing flow and hangs.
252
+ const isEmpty = () => !creds.registered && !creds.me?.id && countKeys() === 0
253
+
254
+ // Right, on baileyrs and upstream alike: creds are the pairing record.
255
+ const isEmpty = () => !creds.registered && !creds.me?.id
256
+ ```
257
+
258
+ **A non-empty key store is not evidence of a session.** `creds.registered` and
259
+ `creds.me?.id` are, and they are the only thing to check.
260
+
261
+ When you need to present a store the way upstream would — counting rows,
262
+ listing the Signal namespaces, exporting an upstream-shaped dump — filter the
263
+ bridge rows out with the exported classifier rather than matching the prefix
264
+ yourself. It is derived from the internal routing catalog, so a namespace added
265
+ in a later release is covered without you changing anything:
266
+
267
+ ```ts
268
+ // Aliased install? Import from '@whiskeysockets/baileys' instead; it resolves here.
269
+ import { BRIDGE_INTERNAL_KEY_TYPES, isBridgeInternalKeyType } from '@oxidezap/baileyrs'
270
+
271
+ isBridgeInternalKeyType('bridge-native-device') // true
272
+ isBridgeInternalKeyType('pre-key') // false
273
+
274
+ // Every bridge-internal namespace, e.g. for a SQL `NOT IN (...)` clause. The
275
+ // store also holds the Baileys namespaces the engine projects into it.
276
+ BRIDGE_INTERNAL_KEY_TYPES
277
+ ```
278
+
279
+ > **Do not drop these rows from a backup or a store-to-store move.** They are
280
+ > effective state, not metadata. `bridge-signed-prekey`, `bridge-sender-key-devices`,
281
+ > `bridge-base-key`, `bridge-sent-message`, `bridge-msg-secret`,
282
+ > `bridge-mutation-mac` and `bridge-meta` have no Baileys projection at all, and
283
+ > a Signal session that turned native-only lives under `bridge-native-session`
284
+ > alone. Restoring only the non-bridge rows rolls those sessions back or loses
285
+ > the state outright. Filter the bridge rows only where the destination is an
286
+ > upstream-shaped view that cannot represent them; a full backup, or a move
287
+ > between two baileyrs stores, carries every `bridge-` row across.
288
+
289
+ This applies to stores you own — the upstream `{ creds, keys }` shape that
290
+ baileyrs auto-wraps, including `useLegacyMultiFileAuthState`. It does not apply
291
+ to `useMultiFileAuthState`, whose `keys` is a projection over the engine's own
292
+ store; the `bridge-` namespaces never surface through it.
223
293
 
224
294
  ## Disclaimer
225
295
 
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Namespace prefix reserved for bridge-internal state. Every namespace the
3
+ * routing catalog writes back to a legacy key store starts with it, and no
4
+ * Baileys Signal namespace does, so the prefix is treated as reserved: a
5
+ * namespace added to the catalog later is classified without a code change.
6
+ */
7
+ export declare const BRIDGE_INTERNAL_KEY_PREFIX = "bridge-";
8
+ /**
9
+ * Every namespace `wrapLegacyStore` can write that is not a Signal key.
10
+ * Derived from the routing catalog so it cannot drift from what is written.
11
+ */
12
+ export declare const BRIDGE_INTERNAL_KEY_TYPES: readonly string[];
13
+ /**
14
+ * Whether a `SignalKeyStore` namespace holds bridge-internal state rather than
15
+ * Signal key material. Custom stores use it to skip those rows when they
16
+ * enumerate, count or migrate. Never throws: anything that is not a known
17
+ * namespace, including a non-string, is reported as not internal.
18
+ */
19
+ export declare function isBridgeInternalKeyType(type: unknown): boolean;
20
+ //# sourceMappingURL=namespaces.d.ts.map
@@ -0,0 +1,27 @@
1
+ // Public classifier for the namespaces the bridge parks in a consumer's key store.
2
+ import { StoreCatalog } from './constants.js';
3
+ /**
4
+ * Namespace prefix reserved for bridge-internal state. Every namespace the
5
+ * routing catalog writes back to a legacy key store starts with it, and no
6
+ * Baileys Signal namespace does, so the prefix is treated as reserved: a
7
+ * namespace added to the catalog later is classified without a code change.
8
+ */
9
+ export const BRIDGE_INTERNAL_KEY_PREFIX = 'bridge-';
10
+ /**
11
+ * Every namespace `wrapLegacyStore` can write that is not a Signal key.
12
+ * Derived from the routing catalog so it cannot drift from what is written.
13
+ */
14
+ export const BRIDGE_INTERNAL_KEY_TYPES = Object.freeze([...new Set(Object.values(StoreCatalog).map(route => route.nativeType))].toSorted());
15
+ const internalTypes = new Set(BRIDGE_INTERNAL_KEY_TYPES);
16
+ /**
17
+ * Whether a `SignalKeyStore` namespace holds bridge-internal state rather than
18
+ * Signal key material. Custom stores use it to skip those rows when they
19
+ * enumerate, count or migrate. Never throws: anything that is not a known
20
+ * namespace, including a non-string, is reported as not internal.
21
+ */
22
+ export function isBridgeInternalKeyType(type) {
23
+ if (typeof type !== 'string' || type.length === 0)
24
+ return false;
25
+ return type.startsWith(BRIDGE_INTERNAL_KEY_PREFIX) || internalTypes.has(type);
26
+ }
27
+ //# sourceMappingURL=namespaces.js.map
@@ -48,16 +48,21 @@ function legacySignalAddress(address) {
48
48
  : `${parsed.user}${SignalAddressSyntax.DOMAIN_TYPE}${parsed.domainType}`;
49
49
  return `${user}${SignalAddressSyntax.SIGNAL_DEVICE}${parsed.device}`;
50
50
  }
51
+ /** A sender key is `<chatJid>:<signalAddress>`, and the chat is a group, status
52
+ * or a broadcast list. Which chats fan out through sender keys is the core's
53
+ * namespace, so the boundary validates the JID shape, not the domain. No JID
54
+ * carries whitespace or a control character, and letting one through would
55
+ * mint a storage key that no later lookup can match. */
56
+ const CHAT_JID = /^[^\s:@\p{Cc}]+@[^\s:@\p{Cc}]+$/u;
51
57
  function legacySenderKey(key) {
52
- const groupTerminator = `${SignalAddressSyntax.DOMAIN}${SignalDomain.GROUP}${SignalAddressSyntax.JID_DEVICE}`;
53
- const groupEnd = key.indexOf(groupTerminator);
54
- if (groupEnd < 0)
58
+ const chatDomain = key.indexOf(SignalAddressSyntax.DOMAIN);
59
+ const chatEnd = chatDomain < 0 ? -1 : key.indexOf(SignalAddressSyntax.JID_DEVICE, chatDomain);
60
+ const chat = chatEnd < 0 ? '' : key.slice(0, chatEnd);
61
+ if (!CHAT_JID.test(chat))
55
62
  throw new TypeError(`invalid native sender-key address: ${key}`);
56
- const addressStart = groupEnd + groupTerminator.length;
57
- const group = key.slice(0, addressStart - SignalAddressSyntax.JID_DEVICE.length);
58
- const address = legacySignalAddress(key.slice(addressStart));
63
+ const address = legacySignalAddress(key.slice(chatEnd + SignalAddressSyntax.JID_DEVICE.length));
59
64
  const deviceSeparator = address.lastIndexOf(SignalAddressSyntax.SIGNAL_DEVICE);
60
- return [group, address.slice(0, deviceSeparator), address.slice(deviceSeparator + 1)].join(SignalAddressSyntax.SENDER_KEY_PART);
65
+ return [chat, address.slice(0, deviceSeparator), address.slice(deviceSeparator + 1)].join(SignalAddressSyntax.SENDER_KEY_PART);
61
66
  }
62
67
  const passthroughKey = (_store, key) => key;
63
68
  const keyTranslators = Object.freeze({
@@ -114,7 +119,7 @@ function nativeSignalAddress(address) {
114
119
  }
115
120
  function nativeSenderKey(key) {
116
121
  const parts = key.split(SignalAddressSyntax.SENDER_KEY_PART);
117
- if (parts.length !== 3 || !parts[0] || !parts[1] || !parts[2]) {
122
+ if (parts.length !== 3 || !CHAT_JID.test(parts[0]) || !parts[1] || !parts[2]) {
118
123
  throw new TypeError(`invalid legacy sender-key address: ${key}`);
119
124
  }
120
125
  return `${parts[0]}${SignalAddressSyntax.JID_DEVICE}${nativeSignalAddress(`${parts[1]}.${parts[2]}`)}`;
@@ -0,0 +1,15 @@
1
+ import type { NewsletterMetadataResult } from '@oxidezap/whatsapp-rust-bridge';
2
+ import type { NewsletterMetadata, NewsletterViewRole } from '../Types/Newsletter.js';
3
+ /** Upstream's `NewsletterViewRole`, or undefined for a role it does not name. */
4
+ export declare const bridgeNewsletterRoleToBaileys: (role: string | undefined) => NewsletterViewRole | undefined;
5
+ /**
6
+ * Neutral newsletter metadata in upstream's shape.
7
+ *
8
+ * Four upstream fields have no source in the bridge result and stay absent
9
+ * rather than being invented: `owner` (the result carries the viewer's role,
10
+ * not the owner's jid), `mute_state` (the result's `state` is the newsletter's
11
+ * lifecycle, Active/Suspended, not a mute), `reaction_codes`, and
12
+ * `thread_metadata`.
13
+ */
14
+ export declare const bridgeNewsletterMetadataToBaileys: (result: NewsletterMetadataResult) => NewsletterMetadata;
15
+ //# sourceMappingURL=newsletter-results.d.ts.map
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The bridge spells these the way the core's enums are named, and upstream
3
+ * spells them in screaming case. Written out rather than upper-cased blindly so
4
+ * a variant the core adds later shows up as `undefined` instead of as a string
5
+ * nobody declared.
6
+ */
7
+ const VERIFICATION = {
8
+ Verified: 'VERIFIED',
9
+ Unverified: 'UNVERIFIED'
10
+ };
11
+ const VIEW_ROLE = {
12
+ Owner: 'OWNER',
13
+ Admin: 'ADMIN',
14
+ Subscriber: 'SUBSCRIBER',
15
+ Guest: 'GUEST'
16
+ };
17
+ /** Upstream's `NewsletterViewRole`, or undefined for a role it does not name. */
18
+ export const bridgeNewsletterRoleToBaileys = (role) => role === undefined ? undefined : VIEW_ROLE[role];
19
+ /**
20
+ * Neutral newsletter metadata in upstream's shape.
21
+ *
22
+ * Four upstream fields have no source in the bridge result and stay absent
23
+ * rather than being invented: `owner` (the result carries the viewer's role,
24
+ * not the owner's jid), `mute_state` (the result's `state` is the newsletter's
25
+ * lifecycle, Active/Suspended, not a mute), `reaction_codes`, and
26
+ * `thread_metadata`.
27
+ */
28
+ export const bridgeNewsletterMetadataToBaileys = (result) => ({
29
+ id: result.jid,
30
+ name: result.name,
31
+ ...(result.description !== undefined ? { description: result.description } : {}),
32
+ ...(result.inviteCode !== undefined ? { invite: result.inviteCode } : {}),
33
+ ...(result.creationTime !== undefined ? { creation_time: result.creationTime } : {}),
34
+ subscribers: result.subscriberCount,
35
+ ...(VERIFICATION[result.verification] !== undefined ? { verification: VERIFICATION[result.verification] } : {}),
36
+ ...(result.pictureUrl !== undefined ? { picture: { url: result.pictureUrl } } : {})
37
+ });
38
+ //# sourceMappingURL=newsletter-results.js.map
@@ -0,0 +1,29 @@
1
+ import type { OrderResult } from '@oxidezap/whatsapp-rust-bridge';
2
+ import type { CatalogPage, CollectionsPage, GetCatalogOptions, ProductCreate, ProductUpdate } from '../Types/Product.js';
3
+ import type { UpdateBussinesProfileProps } from '../Types/Bussines.js';
4
+ import type { WAMediaUpload } from '../Types/Message.js';
5
+ import type { SocketContext } from './types.js';
6
+ export declare const makeBusinessMethods: (ctx: SocketContext) => {
7
+ getCatalog: ({ jid, limit, cursor }: GetCatalogOptions) => Promise<CatalogPage>;
8
+ getCollections: (jid?: string, limit?: number) => Promise<CollectionsPage>;
9
+ /**
10
+ * `sellerJid` is not in upstream's signature because upstream reads orders
11
+ * over the legacy `fb:thrift_iq` route, which addresses the server. The
12
+ * route the real client uses is a MEX query keyed by the seller, so the JID
13
+ * is required here. It is on the order message the token came from.
14
+ */
15
+ getOrderDetails: (orderId: string, tokenBase64: string, sellerJid?: string) => Promise<OrderResult>;
16
+ updateBussinesProfile: (args: UpdateBussinesProfileProps) => Promise<void>;
17
+ /**
18
+ * Declared but not callable end to end. The core and the bridge take the
19
+ * `{fbid, meta_hmac, ts}` receipt of a cover photo upload, and this
20
+ * package's upload path cannot produce one: it requires a url and a
21
+ * direct path, which that endpoint does not return.
22
+ */
23
+ updateCoverPhoto: (photo: WAMediaUpload) => Promise<never>;
24
+ removeCoverPhoto: (id: string) => Promise<void>;
25
+ productCreate: (create: ProductCreate) => Promise<never>;
26
+ productUpdate: (productId: string, update: ProductUpdate) => Promise<never>;
27
+ productDelete: (productIds: string[]) => Promise<never>;
28
+ };
29
+ //# sourceMappingURL=business.d.ts.map
@@ -0,0 +1,104 @@
1
+ import { Boom } from '../Utils/boom.js';
2
+ import { jidNormalizedUser } from '../WABinary/jid-utils.js';
3
+ /**
4
+ * Product create, update and delete exist only in Baileys: the operations they
5
+ * send appear in no WhatsApp Web bundle. Declared so a migrating caller reads
6
+ * why instead of a bare TypeError, and rejecting because there is nothing to
7
+ * call.
8
+ */
9
+ const noProductWriteRoute = (method, ...ignored) => {
10
+ void ignored;
11
+ throw new Boom(`${method} is not supported: the WhatsApp Web client has no product create, edit or delete operation, so there is nothing for this to call. Manage the catalog from the WhatsApp Business app.`, { statusCode: 501 });
12
+ };
13
+ /**
14
+ * Upstream types these as strings, so an empty one is reachable from an
15
+ * unfilled form. `Number('')` is midnight, which would quietly rewrite the
16
+ * schedule instead of being refused.
17
+ */
18
+ const minutesPastMidnight = (value, which) => {
19
+ const minutes = Number(value);
20
+ if (value.trim() === '' || !Number.isInteger(minutes) || minutes < 0 || minutes > 1440) {
21
+ throw new Boom(`updateBussinesProfile: ${which} time '${value}' is not a count of minutes past midnight`, {
22
+ statusCode: 400
23
+ });
24
+ }
25
+ return minutes;
26
+ };
27
+ /**
28
+ * Both catalog reads take an optional jid and mean "mine" without one, as
29
+ * upstream does. `getMe` rather than `getUser`, because it reads through to the
30
+ * persisted credentials, which is the same place upstream reads from and is
31
+ * populated before the socket finishes its own initialisation.
32
+ */
33
+ const catalogSubject = (method, ctx, jid) => {
34
+ const subject = jidNormalizedUser(jid || ctx.getMe()?.id);
35
+ if (!subject) {
36
+ throw new Boom(`${method}: no jid was given and there is no authenticated account, so there is no own catalog to read`, {
37
+ statusCode: 400
38
+ });
39
+ }
40
+ return subject;
41
+ };
42
+ export const makeBusinessMethods = (ctx) => ({
43
+ getCatalog: async ({ jid, limit, cursor }) => {
44
+ const subject = catalogSubject('getCatalog', ctx, jid);
45
+ return await (await ctx.getClient()).getCatalog(subject, { limit, after: cursor });
46
+ },
47
+ getCollections: async (jid, limit) => {
48
+ const subject = catalogSubject('getCollections', ctx, jid);
49
+ return await (await ctx.getClient()).getCollections(subject, { collectionLimit: limit });
50
+ },
51
+ /**
52
+ * `sellerJid` is not in upstream's signature because upstream reads orders
53
+ * over the legacy `fb:thrift_iq` route, which addresses the server. The
54
+ * route the real client uses is a MEX query keyed by the seller, so the JID
55
+ * is required here. It is on the order message the token came from.
56
+ */
57
+ getOrderDetails: async (orderId, tokenBase64, sellerJid) => {
58
+ if (!sellerJid) {
59
+ throw new Boom('getOrderDetails: a third argument with the seller jid is required, because orders are read through a query keyed by the business rather than the legacy server-addressed one', { statusCode: 400 });
60
+ }
61
+ return await (await ctx.getClient()).getOrder(sellerJid, orderId, tokenBase64);
62
+ },
63
+ updateBussinesProfile: async (args) => {
64
+ const update = {
65
+ address: args.address,
66
+ description: args.description,
67
+ email: args.email,
68
+ websites: args.websites,
69
+ ...(args.hours !== undefined
70
+ ? {
71
+ businessHours: {
72
+ timezone: args.hours.timezone,
73
+ // Minutes past midnight as a number; upstream types the two
74
+ // as strings and the core rejects them on the other modes.
75
+ config: args.hours.days.map(day => ({
76
+ dayOfWeek: day.day,
77
+ mode: day.mode,
78
+ openTime: day.mode === 'specific_hours' ? minutesPastMidnight(day.openTimeInMinutes, 'open') : undefined,
79
+ closeTime: day.mode === 'specific_hours' ? minutesPastMidnight(day.closeTimeInMinutes, 'close') : undefined
80
+ }))
81
+ }
82
+ }
83
+ : {})
84
+ };
85
+ await (await ctx.getClient()).updateBusinessProfile(update);
86
+ },
87
+ /**
88
+ * Declared but not callable end to end. The core and the bridge take the
89
+ * `{fbid, meta_hmac, ts}` receipt of a cover photo upload, and this
90
+ * package's upload path cannot produce one: it requires a url and a
91
+ * direct path, which that endpoint does not return.
92
+ */
93
+ updateCoverPhoto: async (photo) => {
94
+ void photo;
95
+ throw new Boom('updateCoverPhoto is not available yet: the cover photo upload returns an {fbid, meta_hmac, ts} receipt that this package cannot obtain. removeCoverPhoto works.', { statusCode: 501 });
96
+ },
97
+ removeCoverPhoto: async (id) => {
98
+ await (await ctx.getClient()).removeBusinessCoverPhoto(id);
99
+ },
100
+ productCreate: async (create) => noProductWriteRoute('productCreate', create),
101
+ productUpdate: async (productId, update) => noProductWriteRoute('productUpdate', productId, update),
102
+ productDelete: async (productIds) => noProductWriteRoute('productDelete', productIds)
103
+ });
104
+ //# sourceMappingURL=business.js.map
@@ -1,4 +1,7 @@
1
- import type { ChatModification, WAPatchName } from '../Types/index.js';
1
+ import type { proto } from '../WAProto/runtime.js';
2
+ import type { QuickReplyAction } from '../Types/Bussines.js';
3
+ import type { LabelActionBody } from '../Types/Label.js';
4
+ import type { ChatModification } from '../Types/index.js';
2
5
  import type { SocketContext } from './types.js';
3
6
  export declare const makeChatActionMethods: (ctx: SocketContext) => {
4
7
  pinChat: (jid: string, pin: boolean) => Promise<void>;
@@ -9,17 +12,23 @@ export declare const makeChatActionMethods: (ctx: SocketContext) => {
9
12
  * Compatibility wrapper for original Baileys chatModify API.
10
13
  * Routes to the appropriate bridge method based on the modification type.
11
14
  *
12
- * Fully supported: archive, pin, mute, star, markRead, delete, deleteForMe, pushNameSetting, contact, clear
13
- * Not yet in bridge (app-state patches): disableLinkPreviews, labels, quickReply
15
+ * Every variant either runs or throws. A variant that resolved without
16
+ * doing anything told the caller their chat was labelled when nothing was
17
+ * synced, and no signature or type catches that.
14
18
  */
15
19
  chatModify: (mod: ChatModification, jid: string) => Promise<void>;
16
- /**
17
- * Force re-sync of app state collections.
18
- *
19
- * In the Rust bridge architecture, app state is managed internally by the engine
20
- * and synced automatically on connect. This method is a no-op provided for API
21
- * compatibility with upstream Baileys.
22
- */
23
- resyncAppState: (_collections?: readonly WAPatchName[], _isInitialSync?: boolean) => Promise<void>;
20
+ addOrEditContact: (jid: string, contact: proto.SyncActionValue.IContactAction) => Promise<void>;
21
+ removeContact: (jid: string) => Promise<void>;
22
+ addLabel: (jid: string, labels: LabelActionBody) => Promise<void>;
23
+ addChatLabel: (jid: string, labelId: string) => Promise<void>;
24
+ removeChatLabel: (jid: string, labelId: string) => Promise<void>;
25
+ addMessageLabel: (jid: string, messageId: string, labelId: string) => Promise<void>;
26
+ removeMessageLabel: (jid: string, messageId: string, labelId: string) => Promise<void>;
27
+ star: (jid: string, messages: {
28
+ id: string;
29
+ fromMe?: boolean;
30
+ }[], star: boolean) => Promise<void>;
31
+ addOrEditQuickReply: (quickReply: QuickReplyAction) => Promise<void>;
32
+ removeQuickReply: (timestamp: string) => Promise<void>;
24
33
  };
25
34
  //# sourceMappingURL=chat-actions.d.ts.map