@drawbridge/drawbridge-agents 0.1.30 → 0.1.31
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.
|
@@ -226,6 +226,20 @@ skill) so it stops living only in tribal memory.
|
|
|
226
226
|
- The `write_resource_feedbacks` and `read_publications` scopes live in drawbridge-shopify-app's
|
|
227
227
|
`shopify.app.*.toml` but power other repos: sync's product ResourceFeedback sends and the
|
|
228
228
|
embedded app's publishing section. Removing either → silent Shopify userErrors, no crash.
|
|
229
|
+
- **No Shopify webhook reaches the embedded app.** Every `[[webhooks.subscriptions]]` in
|
|
230
|
+
drawbridge-shopify-app's `shopify.app.*.toml` — `app/uninstalled` included — is delivered to
|
|
231
|
+
drawbridge-webhooks (`route/shopify.js`, topic-agnostic → `buffer` doc), which drawbridge-sync
|
|
232
|
+
consumes (`lib/buffer.js` `shopify.app/uninstalled` → delete connections, hard-delete the `shop`
|
|
233
|
+
record). So `app/routes/webhooks.app.uninstalled.tsx` is unreachable and **nothing in the
|
|
234
|
+
embedded app may hold per-shop state that needs invalidating on uninstall** — no memo, no cache,
|
|
235
|
+
no "already done" set. Violated once: `ensureChannel` cached "this shop is bound to our channel
|
|
236
|
+
spec" in a process-local Set cleared only by that dead route, so uninstall → reinstall skipped
|
|
237
|
+
`channelCreate` and the merchant silently lost the Drawbridge sales channel (2026-08-07 QA).
|
|
238
|
+
- Shopify Admin GraphQL failures come in **two shapes**, and sales-channel code must handle both:
|
|
239
|
+
domain failures land in `userErrors` (HTTP 200), while a revoked/expired offline token,
|
|
240
|
+
throttling, or a field missing on the API version land in a **top-level `errors`** — a bare
|
|
241
|
+
string for a 401, an array of objects otherwise — with a null `data`. Reading only `userErrors`
|
|
242
|
+
turns an auth failure into a silent success.
|
|
229
243
|
- `SHOPIFY_REQUIRED_SCOPES` (`@drawbridge/shopify` `lib/constants.js`) must stay in lockstep with
|
|
230
244
|
`[access_scopes]` in drawbridge-shopify-app's `shopify.app.*.toml` (both files). drawbridge-api
|
|
231
245
|
diffs each store's granted scopes against it to flag "update permissions" on the connections
|
package/package.json
CHANGED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// What communication channels are actually flowing, HubSpot's count against ours.
|
|
2
|
+
//
|
|
3
|
+
// Exists because "the integration is connected" and "the data is arriving" are
|
|
4
|
+
// different claims, and the second is the only one that matters here. HubSpot's
|
|
5
|
+
// communication object holds SMS, WhatsApp and LinkedIn messages, told apart by
|
|
6
|
+
// channel — so whatever writes LinkedIn messages into that object, this pipeline
|
|
7
|
+
// reads. The question is never whether we can read them; it is whether anything is
|
|
8
|
+
// writing them.
|
|
9
|
+
//
|
|
10
|
+
// Same shape as the `missing` figure on the contact ingest, which is what caught 50
|
|
11
|
+
// contacts that had been silently absent for the life of the project.
|
|
12
|
+
|
|
13
|
+
const { growth } = require( '../lib/mongodb' );
|
|
14
|
+
const { enabled, request } = require( '../lib/hubspot' );
|
|
15
|
+
const { run } = require( '../lib/cycle' );
|
|
16
|
+
|
|
17
|
+
// The channels HubSpot files under the communication object. PHYSICAL_MAIL is
|
|
18
|
+
// hidden on this portal but counted anyway, because a hidden property still holds
|
|
19
|
+
// records.
|
|
20
|
+
const CHANNELS = [
|
|
21
|
+
'LINKEDIN_MESSAGE',
|
|
22
|
+
'SMS',
|
|
23
|
+
'WHATS_APP',
|
|
24
|
+
'PHYSICAL_MAIL',
|
|
25
|
+
'CUSTOM_CHANNEL_CONVERSATION'
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
run( 'channels', async () => {
|
|
29
|
+
|
|
30
|
+
if( ! enabled() ) return { skipped : 'HUBSPOT_ACCESS_TOKEN not set' };
|
|
31
|
+
|
|
32
|
+
const { db, database } = await growth();
|
|
33
|
+
|
|
34
|
+
const report = {};
|
|
35
|
+
|
|
36
|
+
for( const channel of CHANNELS ){
|
|
37
|
+
|
|
38
|
+
const body = await request({
|
|
39
|
+
body : {
|
|
40
|
+
filterGroups : [
|
|
41
|
+
{
|
|
42
|
+
filters : [
|
|
43
|
+
{
|
|
44
|
+
operator : 'EQ',
|
|
45
|
+
propertyName : 'hs_communication_channel_type',
|
|
46
|
+
value : channel
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
],
|
|
51
|
+
limit : 1
|
|
52
|
+
},
|
|
53
|
+
method : 'POST',
|
|
54
|
+
path : '/crm/v3/objects/communications/search'
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const inHubspot = Number( body?.total || 0 );
|
|
58
|
+
|
|
59
|
+
// Ours is counted on the touch, which is one row per contact on the message,
|
|
60
|
+
// so it can legitimately exceed HubSpot's count of messages.
|
|
61
|
+
const held = await database.collection( 'touch' ).countDocuments({
|
|
62
|
+
channel,
|
|
63
|
+
type : 'communication'
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
report[ channel ] = {
|
|
67
|
+
held,
|
|
68
|
+
inHubspot,
|
|
69
|
+
// The number that answers "is anything writing this channel".
|
|
70
|
+
missing : inHubspot === 0 ? 0 : Math.max( 0, inHubspot - held )
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const flowing = Object.entries( report )
|
|
76
|
+
.filter( ( [ , value ] ) => value.inHubspot > 0 )
|
|
77
|
+
.map( ( [ key ] ) => key );
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
byChannel : report,
|
|
81
|
+
// Empty means nothing outside email, calls, meetings, notes and tasks is
|
|
82
|
+
// reaching HubSpot at all - which is a setup answer, not a pipeline answer.
|
|
83
|
+
flowing : flowing.length ? flowing : 'nothing beyond email/calls/meetings/notes/tasks',
|
|
84
|
+
note : 'LINKEDIN_MESSAGE at 0 means no tool is writing LinkedIn messages into'
|
|
85
|
+
+ ' HubSpot. Whatever writes them, this pipeline already reads them.'
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
});
|