@drawbridge/drawbridge-utils 0.0.91 → 0.0.93
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/dist/plans.cjs +55 -0
- package/dist/plans.d.cts +56 -0
- package/dist/plans.d.ts +56 -0
- package/dist/plans.js +55 -0
- package/dist/social.cjs +158 -0
- package/dist/social.d.cts +163 -0
- package/dist/social.d.ts +163 -0
- package/dist/social.js +133 -0
- package/package.json +6 -1
package/dist/plans.cjs
CHANGED
|
@@ -420,6 +420,61 @@ var plans = {
|
|
|
420
420
|
overages: { actions: "1.5" },
|
|
421
421
|
title: "Elite",
|
|
422
422
|
conversion: 0.5
|
|
423
|
+
},
|
|
424
|
+
// Negotiated tier. Unlike the four sellable plans it has NO catalog price:
|
|
425
|
+
// each enterprise subscription bills from its own Stripe price, minted
|
|
426
|
+
// under STRIPE_PRODUCT_ENTERPRISE and recorded on items.plan.price.
|
|
427
|
+
//
|
|
428
|
+
// That absent price is the whole marker — this entry carries no flag of its
|
|
429
|
+
// own, deliberately. A plan with no stripePriceId cannot be sold self-serve
|
|
430
|
+
// because subscribe and change would hand Stripe `price : undefined`, so
|
|
431
|
+
// "is it selectable" reads straight off the thing that makes it true. Two
|
|
432
|
+
// flags were considered and dropped: `status : drafted | published` (it
|
|
433
|
+
// collides with subscription.status and organization.status, which extend.js
|
|
434
|
+
// attaches to the same payload) and `type : default | custom` (its one real
|
|
435
|
+
// job — catching a sellable plan whose STRIPE_PRICE_* env went missing — is
|
|
436
|
+
// already done by assertEnv at api boot). Negotiated-ness is recorded where
|
|
437
|
+
// it actually varies: subscription.type on the document, set by the admin.
|
|
438
|
+
//
|
|
439
|
+
// Every value below is a FALLBACK, not the deal. Real allowances come from
|
|
440
|
+
// subscription.overrides per customer. They deliberately mirror Elite
|
|
441
|
+
// rather than being infinite or zero: a half-configured enterprise
|
|
442
|
+
// subscription then degrades to the best sellable tier — a known-good,
|
|
443
|
+
// billable state — instead of billing nothing or granting everything.
|
|
444
|
+
//
|
|
445
|
+
// Placed LAST so the array-position direction logic in drawbridge-api
|
|
446
|
+
// (organization-subscription.js `plans.findIndex`) reads a move onto
|
|
447
|
+
// enterprise as an upgrade and a move off it as a downgrade.
|
|
448
|
+
DBENTERPRISE: {
|
|
449
|
+
features: all.features([
|
|
450
|
+
organization.advertisements.key,
|
|
451
|
+
organization.analytics.key,
|
|
452
|
+
organization.members.key,
|
|
453
|
+
organization.subdomain.key,
|
|
454
|
+
page.slug.key
|
|
455
|
+
]),
|
|
456
|
+
limits: all.limits({ actions: 1e5, members: infinite, storage: gigabyte * 100 }),
|
|
457
|
+
marketing: {
|
|
458
|
+
description: "Negotiated terms tailored to your organization.",
|
|
459
|
+
features: [
|
|
460
|
+
"Analytics",
|
|
461
|
+
"Custom subdomain / URLs",
|
|
462
|
+
"Confirmation page ads",
|
|
463
|
+
"Negotiated pricing and limits"
|
|
464
|
+
],
|
|
465
|
+
limits: [
|
|
466
|
+
["Actions per month", "Custom"],
|
|
467
|
+
["Affiliates", "Unlimited"],
|
|
468
|
+
["Brands", "Unlimited"],
|
|
469
|
+
["Campaigns", "Unlimited"],
|
|
470
|
+
["Pages", "Unlimited"],
|
|
471
|
+
["Members", "Custom"],
|
|
472
|
+
["Storage", "Custom"]
|
|
473
|
+
]
|
|
474
|
+
},
|
|
475
|
+
overages: { actions: "1.5" },
|
|
476
|
+
title: "Enterprise",
|
|
477
|
+
conversion: 0.5
|
|
423
478
|
}
|
|
424
479
|
};
|
|
425
480
|
var conversionRate = (subscription) => {
|
package/dist/plans.d.cts
CHANGED
|
@@ -192,6 +192,62 @@ const plans = {
|
|
|
192
192
|
overages : { actions : '1.5' },
|
|
193
193
|
title : 'Elite',
|
|
194
194
|
conversion : 0.5
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
// Negotiated tier. Unlike the four sellable plans it has NO catalog price:
|
|
198
|
+
// each enterprise subscription bills from its own Stripe price, minted
|
|
199
|
+
// under STRIPE_PRODUCT_ENTERPRISE and recorded on items.plan.price.
|
|
200
|
+
//
|
|
201
|
+
// That absent price is the whole marker — this entry carries no flag of its
|
|
202
|
+
// own, deliberately. A plan with no stripePriceId cannot be sold self-serve
|
|
203
|
+
// because subscribe and change would hand Stripe `price : undefined`, so
|
|
204
|
+
// "is it selectable" reads straight off the thing that makes it true. Two
|
|
205
|
+
// flags were considered and dropped: `status : drafted | published` (it
|
|
206
|
+
// collides with subscription.status and organization.status, which extend.js
|
|
207
|
+
// attaches to the same payload) and `type : default | custom` (its one real
|
|
208
|
+
// job — catching a sellable plan whose STRIPE_PRICE_* env went missing — is
|
|
209
|
+
// already done by assertEnv at api boot). Negotiated-ness is recorded where
|
|
210
|
+
// it actually varies: subscription.type on the document, set by the admin.
|
|
211
|
+
//
|
|
212
|
+
// Every value below is a FALLBACK, not the deal. Real allowances come from
|
|
213
|
+
// subscription.overrides per customer. They deliberately mirror Elite
|
|
214
|
+
// rather than being infinite or zero: a half-configured enterprise
|
|
215
|
+
// subscription then degrades to the best sellable tier — a known-good,
|
|
216
|
+
// billable state — instead of billing nothing or granting everything.
|
|
217
|
+
//
|
|
218
|
+
// Placed LAST so the array-position direction logic in drawbridge-api
|
|
219
|
+
// (organization-subscription.js `plans.findIndex`) reads a move onto
|
|
220
|
+
// enterprise as an upgrade and a move off it as a downgrade.
|
|
221
|
+
DBENTERPRISE : {
|
|
222
|
+
features : all.features([
|
|
223
|
+
organization.advertisements.key,
|
|
224
|
+
organization.analytics.key,
|
|
225
|
+
organization.members.key,
|
|
226
|
+
organization.subdomain.key,
|
|
227
|
+
page.slug.key
|
|
228
|
+
]),
|
|
229
|
+
limits : all.limits({ actions : 100000, members : infinite, storage : gigabyte * 100 }),
|
|
230
|
+
marketing : {
|
|
231
|
+
description : 'Negotiated terms tailored to your organization.',
|
|
232
|
+
features : [
|
|
233
|
+
'Analytics',
|
|
234
|
+
'Custom subdomain / URLs',
|
|
235
|
+
'Confirmation page ads',
|
|
236
|
+
'Negotiated pricing and limits'
|
|
237
|
+
],
|
|
238
|
+
limits : [
|
|
239
|
+
[ 'Actions per month', 'Custom' ],
|
|
240
|
+
[ 'Affiliates', 'Unlimited' ],
|
|
241
|
+
[ 'Brands', 'Unlimited' ],
|
|
242
|
+
[ 'Campaigns', 'Unlimited' ],
|
|
243
|
+
[ 'Pages', 'Unlimited' ],
|
|
244
|
+
[ 'Members', 'Custom' ],
|
|
245
|
+
[ 'Storage', 'Custom' ]
|
|
246
|
+
]
|
|
247
|
+
},
|
|
248
|
+
overages : { actions : '1.5' },
|
|
249
|
+
title : 'Enterprise',
|
|
250
|
+
conversion : 0.5
|
|
195
251
|
}
|
|
196
252
|
};
|
|
197
253
|
|
package/dist/plans.d.ts
CHANGED
|
@@ -192,6 +192,62 @@ const plans = {
|
|
|
192
192
|
overages : { actions : '1.5' },
|
|
193
193
|
title : 'Elite',
|
|
194
194
|
conversion : 0.5
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
// Negotiated tier. Unlike the four sellable plans it has NO catalog price:
|
|
198
|
+
// each enterprise subscription bills from its own Stripe price, minted
|
|
199
|
+
// under STRIPE_PRODUCT_ENTERPRISE and recorded on items.plan.price.
|
|
200
|
+
//
|
|
201
|
+
// That absent price is the whole marker — this entry carries no flag of its
|
|
202
|
+
// own, deliberately. A plan with no stripePriceId cannot be sold self-serve
|
|
203
|
+
// because subscribe and change would hand Stripe `price : undefined`, so
|
|
204
|
+
// "is it selectable" reads straight off the thing that makes it true. Two
|
|
205
|
+
// flags were considered and dropped: `status : drafted | published` (it
|
|
206
|
+
// collides with subscription.status and organization.status, which extend.js
|
|
207
|
+
// attaches to the same payload) and `type : default | custom` (its one real
|
|
208
|
+
// job — catching a sellable plan whose STRIPE_PRICE_* env went missing — is
|
|
209
|
+
// already done by assertEnv at api boot). Negotiated-ness is recorded where
|
|
210
|
+
// it actually varies: subscription.type on the document, set by the admin.
|
|
211
|
+
//
|
|
212
|
+
// Every value below is a FALLBACK, not the deal. Real allowances come from
|
|
213
|
+
// subscription.overrides per customer. They deliberately mirror Elite
|
|
214
|
+
// rather than being infinite or zero: a half-configured enterprise
|
|
215
|
+
// subscription then degrades to the best sellable tier — a known-good,
|
|
216
|
+
// billable state — instead of billing nothing or granting everything.
|
|
217
|
+
//
|
|
218
|
+
// Placed LAST so the array-position direction logic in drawbridge-api
|
|
219
|
+
// (organization-subscription.js `plans.findIndex`) reads a move onto
|
|
220
|
+
// enterprise as an upgrade and a move off it as a downgrade.
|
|
221
|
+
DBENTERPRISE : {
|
|
222
|
+
features : all.features([
|
|
223
|
+
organization.advertisements.key,
|
|
224
|
+
organization.analytics.key,
|
|
225
|
+
organization.members.key,
|
|
226
|
+
organization.subdomain.key,
|
|
227
|
+
page.slug.key
|
|
228
|
+
]),
|
|
229
|
+
limits : all.limits({ actions : 100000, members : infinite, storage : gigabyte * 100 }),
|
|
230
|
+
marketing : {
|
|
231
|
+
description : 'Negotiated terms tailored to your organization.',
|
|
232
|
+
features : [
|
|
233
|
+
'Analytics',
|
|
234
|
+
'Custom subdomain / URLs',
|
|
235
|
+
'Confirmation page ads',
|
|
236
|
+
'Negotiated pricing and limits'
|
|
237
|
+
],
|
|
238
|
+
limits : [
|
|
239
|
+
[ 'Actions per month', 'Custom' ],
|
|
240
|
+
[ 'Affiliates', 'Unlimited' ],
|
|
241
|
+
[ 'Brands', 'Unlimited' ],
|
|
242
|
+
[ 'Campaigns', 'Unlimited' ],
|
|
243
|
+
[ 'Pages', 'Unlimited' ],
|
|
244
|
+
[ 'Members', 'Custom' ],
|
|
245
|
+
[ 'Storage', 'Custom' ]
|
|
246
|
+
]
|
|
247
|
+
},
|
|
248
|
+
overages : { actions : '1.5' },
|
|
249
|
+
title : 'Enterprise',
|
|
250
|
+
conversion : 0.5
|
|
195
251
|
}
|
|
196
252
|
};
|
|
197
253
|
|
package/dist/plans.js
CHANGED
|
@@ -383,6 +383,61 @@ var plans = {
|
|
|
383
383
|
overages: { actions: "1.5" },
|
|
384
384
|
title: "Elite",
|
|
385
385
|
conversion: 0.5
|
|
386
|
+
},
|
|
387
|
+
// Negotiated tier. Unlike the four sellable plans it has NO catalog price:
|
|
388
|
+
// each enterprise subscription bills from its own Stripe price, minted
|
|
389
|
+
// under STRIPE_PRODUCT_ENTERPRISE and recorded on items.plan.price.
|
|
390
|
+
//
|
|
391
|
+
// That absent price is the whole marker — this entry carries no flag of its
|
|
392
|
+
// own, deliberately. A plan with no stripePriceId cannot be sold self-serve
|
|
393
|
+
// because subscribe and change would hand Stripe `price : undefined`, so
|
|
394
|
+
// "is it selectable" reads straight off the thing that makes it true. Two
|
|
395
|
+
// flags were considered and dropped: `status : drafted | published` (it
|
|
396
|
+
// collides with subscription.status and organization.status, which extend.js
|
|
397
|
+
// attaches to the same payload) and `type : default | custom` (its one real
|
|
398
|
+
// job — catching a sellable plan whose STRIPE_PRICE_* env went missing — is
|
|
399
|
+
// already done by assertEnv at api boot). Negotiated-ness is recorded where
|
|
400
|
+
// it actually varies: subscription.type on the document, set by the admin.
|
|
401
|
+
//
|
|
402
|
+
// Every value below is a FALLBACK, not the deal. Real allowances come from
|
|
403
|
+
// subscription.overrides per customer. They deliberately mirror Elite
|
|
404
|
+
// rather than being infinite or zero: a half-configured enterprise
|
|
405
|
+
// subscription then degrades to the best sellable tier — a known-good,
|
|
406
|
+
// billable state — instead of billing nothing or granting everything.
|
|
407
|
+
//
|
|
408
|
+
// Placed LAST so the array-position direction logic in drawbridge-api
|
|
409
|
+
// (organization-subscription.js `plans.findIndex`) reads a move onto
|
|
410
|
+
// enterprise as an upgrade and a move off it as a downgrade.
|
|
411
|
+
DBENTERPRISE: {
|
|
412
|
+
features: all.features([
|
|
413
|
+
organization.advertisements.key,
|
|
414
|
+
organization.analytics.key,
|
|
415
|
+
organization.members.key,
|
|
416
|
+
organization.subdomain.key,
|
|
417
|
+
page.slug.key
|
|
418
|
+
]),
|
|
419
|
+
limits: all.limits({ actions: 1e5, members: infinite, storage: gigabyte * 100 }),
|
|
420
|
+
marketing: {
|
|
421
|
+
description: "Negotiated terms tailored to your organization.",
|
|
422
|
+
features: [
|
|
423
|
+
"Analytics",
|
|
424
|
+
"Custom subdomain / URLs",
|
|
425
|
+
"Confirmation page ads",
|
|
426
|
+
"Negotiated pricing and limits"
|
|
427
|
+
],
|
|
428
|
+
limits: [
|
|
429
|
+
["Actions per month", "Custom"],
|
|
430
|
+
["Affiliates", "Unlimited"],
|
|
431
|
+
["Brands", "Unlimited"],
|
|
432
|
+
["Campaigns", "Unlimited"],
|
|
433
|
+
["Pages", "Unlimited"],
|
|
434
|
+
["Members", "Custom"],
|
|
435
|
+
["Storage", "Custom"]
|
|
436
|
+
]
|
|
437
|
+
},
|
|
438
|
+
overages: { actions: "1.5" },
|
|
439
|
+
title: "Enterprise",
|
|
440
|
+
conversion: 0.5
|
|
386
441
|
}
|
|
387
442
|
};
|
|
388
443
|
var conversionRate = (subscription) => {
|
package/dist/social.cjs
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
var __export = (target, all) => {
|
|
6
|
+
for (var name in all)
|
|
7
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
8
|
+
};
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
18
|
+
|
|
19
|
+
// lib/social.js
|
|
20
|
+
var social_exports = {};
|
|
21
|
+
__export(social_exports, {
|
|
22
|
+
matchSocial: () => matchSocial,
|
|
23
|
+
platforms: () => platforms
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(social_exports);
|
|
26
|
+
var segment = /^\/.+/;
|
|
27
|
+
var platforms = [
|
|
28
|
+
{
|
|
29
|
+
platform: "instagram",
|
|
30
|
+
label: "Instagram",
|
|
31
|
+
hosts: ["instagram.com"],
|
|
32
|
+
requirePath: true,
|
|
33
|
+
// content permalinks, not profiles
|
|
34
|
+
exclude: /^\/(p|reel|reels|stories|explore)(\/|$)/
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
platform: "facebook",
|
|
38
|
+
label: "Facebook",
|
|
39
|
+
hosts: ["facebook.com", "fb.com"],
|
|
40
|
+
requirePath: true,
|
|
41
|
+
exclude: /^\/(sharer|share|dialog|plugins)(\/|\.|$)/
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
platform: "tiktok",
|
|
45
|
+
label: "TikTok",
|
|
46
|
+
hosts: ["tiktok.com"],
|
|
47
|
+
path: /^\/@./
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
platform: "youtube",
|
|
51
|
+
label: "YouTube",
|
|
52
|
+
hosts: [
|
|
53
|
+
{ host: "youtube.com", path: /^\/(@.|channel\/|c\/|user\/)/ },
|
|
54
|
+
{ host: "youtu.be", requirePath: true }
|
|
55
|
+
]
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
platform: "x",
|
|
59
|
+
label: "X",
|
|
60
|
+
hosts: ["x.com", "twitter.com"],
|
|
61
|
+
requirePath: true,
|
|
62
|
+
exclude: /^\/(intent|share)(\/|$)/
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
platform: "linkedin",
|
|
66
|
+
label: "LinkedIn",
|
|
67
|
+
hosts: ["linkedin.com"],
|
|
68
|
+
path: /^\/(company|in)\/./
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
platform: "pinterest",
|
|
72
|
+
label: "Pinterest",
|
|
73
|
+
hosts: ["pinterest.com"],
|
|
74
|
+
requirePath: true,
|
|
75
|
+
exclude: /^\/pin(\/|$)/
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
platform: "threads",
|
|
79
|
+
label: "Threads",
|
|
80
|
+
hosts: ["threads.net", "threads.com"],
|
|
81
|
+
path: /^\/@./
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
platform: "snapchat",
|
|
85
|
+
label: "Snapchat",
|
|
86
|
+
hosts: ["snapchat.com"],
|
|
87
|
+
path: /^\/add\/./
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
platform: "whatsapp",
|
|
91
|
+
label: "WhatsApp",
|
|
92
|
+
hosts: [
|
|
93
|
+
{ host: "wa.me", requirePath: true },
|
|
94
|
+
{ host: "api.whatsapp.com", path: /^\/send/ }
|
|
95
|
+
]
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
platform: "spotify",
|
|
99
|
+
label: "Spotify",
|
|
100
|
+
hosts: ["open.spotify.com"],
|
|
101
|
+
path: /^\/(artist|show|user|playlist)\/./
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
platform: "discord",
|
|
105
|
+
label: "Discord",
|
|
106
|
+
hosts: [
|
|
107
|
+
{ host: "discord.gg", requirePath: true },
|
|
108
|
+
{ host: "discord.com", path: /^\/invite\/./ }
|
|
109
|
+
]
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
platform: "twitch",
|
|
113
|
+
label: "Twitch",
|
|
114
|
+
hosts: ["twitch.tv"],
|
|
115
|
+
requirePath: true,
|
|
116
|
+
exclude: /^\/(videos|directory)(\/|$)/
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
platform: "github",
|
|
120
|
+
label: "GitHub",
|
|
121
|
+
hosts: ["github.com"],
|
|
122
|
+
requirePath: true
|
|
123
|
+
}
|
|
124
|
+
];
|
|
125
|
+
var matchSocial = (url) => {
|
|
126
|
+
if (!url || typeof url !== "string") return null;
|
|
127
|
+
let parsed;
|
|
128
|
+
try {
|
|
129
|
+
parsed = new URL(url.includes("://") ? url : "https://" + url.replace(/^\/+/, ""));
|
|
130
|
+
} catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
;
|
|
134
|
+
if (!/^https?:$/.test(parsed.protocol)) return null;
|
|
135
|
+
const host = parsed.hostname.toLowerCase().replace(/^(www|m)\./, "");
|
|
136
|
+
const path = parsed.pathname.replace(/\/+$/, "") || "/";
|
|
137
|
+
for (const entry of platforms) {
|
|
138
|
+
const matched = entry.hosts.map((h) => typeof h === "string" ? { host: h } : h).find((h) => host === h.host || host.endsWith("." + h.host));
|
|
139
|
+
if (!matched) continue;
|
|
140
|
+
const exclude = matched.exclude || entry.exclude;
|
|
141
|
+
const pattern = matched.path || entry.path;
|
|
142
|
+
const requirePath = matched.requirePath ?? entry.requirePath;
|
|
143
|
+
if (exclude && exclude.test(path)) return null;
|
|
144
|
+
if (pattern && !pattern.test(path)) return null;
|
|
145
|
+
if (requirePath && !segment.test(path)) return null;
|
|
146
|
+
return {
|
|
147
|
+
label: entry.label,
|
|
148
|
+
platform: entry.platform
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
;
|
|
152
|
+
return null;
|
|
153
|
+
};
|
|
154
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
155
|
+
0 && (module.exports = {
|
|
156
|
+
matchSocial,
|
|
157
|
+
platforms
|
|
158
|
+
});
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// Social platform detection — the single source of truth for the platform-slug
|
|
2
|
+
// vocabulary shared across the family: sync writes link.icon from it during AI
|
|
3
|
+
// campaign generation, app-web derives the link form's live icon preview and
|
|
4
|
+
// the icon persisted on save, and @drawbridge/components keys its social glyph
|
|
5
|
+
// map on the same slugs. Adding a platform = one entry here + one glyph there.
|
|
6
|
+
//
|
|
7
|
+
// Order is load-bearing: it is the canonical display order for auto-created
|
|
8
|
+
// social links on the share page.
|
|
9
|
+
|
|
10
|
+
const segment = /^\/.+/;
|
|
11
|
+
|
|
12
|
+
const platforms = [
|
|
13
|
+
{
|
|
14
|
+
platform : 'instagram',
|
|
15
|
+
label : 'Instagram',
|
|
16
|
+
hosts : [ 'instagram.com' ],
|
|
17
|
+
requirePath : true,
|
|
18
|
+
// content permalinks, not profiles
|
|
19
|
+
exclude : /^\/(p|reel|reels|stories|explore)(\/|$)/
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
platform : 'facebook',
|
|
23
|
+
label : 'Facebook',
|
|
24
|
+
hosts : [ 'facebook.com', 'fb.com' ],
|
|
25
|
+
requirePath : true,
|
|
26
|
+
exclude : /^\/(sharer|share|dialog|plugins)(\/|\.|$)/
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
platform : 'tiktok',
|
|
30
|
+
label : 'TikTok',
|
|
31
|
+
hosts : [ 'tiktok.com' ],
|
|
32
|
+
path : /^\/@./
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
platform : 'youtube',
|
|
36
|
+
label : 'YouTube',
|
|
37
|
+
hosts : [
|
|
38
|
+
{ host : 'youtube.com', path : /^\/(@.|channel\/|c\/|user\/)/ },
|
|
39
|
+
{ host : 'youtu.be', requirePath : true }
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
platform : 'x',
|
|
44
|
+
label : 'X',
|
|
45
|
+
hosts : [ 'x.com', 'twitter.com' ],
|
|
46
|
+
requirePath : true,
|
|
47
|
+
exclude : /^\/(intent|share)(\/|$)/
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
platform : 'linkedin',
|
|
51
|
+
label : 'LinkedIn',
|
|
52
|
+
hosts : [ 'linkedin.com' ],
|
|
53
|
+
path : /^\/(company|in)\/./
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
platform : 'pinterest',
|
|
57
|
+
label : 'Pinterest',
|
|
58
|
+
hosts : [ 'pinterest.com' ],
|
|
59
|
+
requirePath : true,
|
|
60
|
+
exclude : /^\/pin(\/|$)/
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
platform : 'threads',
|
|
64
|
+
label : 'Threads',
|
|
65
|
+
hosts : [ 'threads.net', 'threads.com' ],
|
|
66
|
+
path : /^\/@./
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
platform : 'snapchat',
|
|
70
|
+
label : 'Snapchat',
|
|
71
|
+
hosts : [ 'snapchat.com' ],
|
|
72
|
+
path : /^\/add\/./
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
platform : 'whatsapp',
|
|
76
|
+
label : 'WhatsApp',
|
|
77
|
+
hosts : [
|
|
78
|
+
{ host : 'wa.me', requirePath : true },
|
|
79
|
+
{ host : 'api.whatsapp.com', path : /^\/send/ }
|
|
80
|
+
]
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
platform : 'spotify',
|
|
84
|
+
label : 'Spotify',
|
|
85
|
+
hosts : [ 'open.spotify.com' ],
|
|
86
|
+
path : /^\/(artist|show|user|playlist)\/./
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
platform : 'discord',
|
|
90
|
+
label : 'Discord',
|
|
91
|
+
hosts : [
|
|
92
|
+
{ host : 'discord.gg', requirePath : true },
|
|
93
|
+
{ host : 'discord.com', path : /^\/invite\/./ }
|
|
94
|
+
]
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
platform : 'twitch',
|
|
98
|
+
label : 'Twitch',
|
|
99
|
+
hosts : [ 'twitch.tv' ],
|
|
100
|
+
requirePath : true,
|
|
101
|
+
exclude : /^\/(videos|directory)(\/|$)/
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
platform : 'github',
|
|
105
|
+
label : 'GitHub',
|
|
106
|
+
hosts : [ 'github.com' ],
|
|
107
|
+
requirePath : true
|
|
108
|
+
}
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
// Match a url against the platform table. Tolerates missing schemes and
|
|
112
|
+
// www./m. subdomains; keystroke-safe (never throws) so app-web can call it
|
|
113
|
+
// on every url-field change. Returns { platform, label } or null.
|
|
114
|
+
const matchSocial = ( url ) => {
|
|
115
|
+
|
|
116
|
+
if( ! url || typeof url !== 'string' ) return null;
|
|
117
|
+
|
|
118
|
+
let parsed;
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
|
|
122
|
+
parsed = new URL( url.includes( '://' ) ? url : 'https://' + url.replace( /^\/+/, '' ) );
|
|
123
|
+
|
|
124
|
+
} catch {
|
|
125
|
+
|
|
126
|
+
return null;
|
|
127
|
+
|
|
128
|
+
}
|
|
129
|
+
if( ! /^https?:$/.test( parsed.protocol ) ) return null;
|
|
130
|
+
|
|
131
|
+
const host = parsed.hostname.toLowerCase().replace( /^(www|m)\./, '' );
|
|
132
|
+
const path = parsed.pathname.replace( /\/+$/, '' ) || '/';
|
|
133
|
+
|
|
134
|
+
for( const entry of platforms ){
|
|
135
|
+
|
|
136
|
+
const matched = entry.hosts
|
|
137
|
+
.map( ( h ) => ( typeof h === 'string' ? { host : h } : h ) )
|
|
138
|
+
.find( ( h ) => host === h.host || host.endsWith( '.' + h.host ) );
|
|
139
|
+
|
|
140
|
+
if( ! matched ) continue;
|
|
141
|
+
|
|
142
|
+
// per-host rules override the platform-level ones (discord.gg/<invite>
|
|
143
|
+
// vs discord.com/invite/<invite>); a host match that fails its path
|
|
144
|
+
// rules is terminal — no other platform owns this host
|
|
145
|
+
const exclude = matched.exclude || entry.exclude;
|
|
146
|
+
const pattern = matched.path || entry.path;
|
|
147
|
+
const requirePath = matched.requirePath ?? entry.requirePath;
|
|
148
|
+
|
|
149
|
+
if( exclude && exclude.test( path ) ) return null;
|
|
150
|
+
if( pattern && ! pattern.test( path ) ) return null;
|
|
151
|
+
if( requirePath && ! segment.test( path ) ) return null;
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
label : entry.label,
|
|
155
|
+
platform : entry.platform
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
export { matchSocial, platforms };
|
package/dist/social.d.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// Social platform detection — the single source of truth for the platform-slug
|
|
2
|
+
// vocabulary shared across the family: sync writes link.icon from it during AI
|
|
3
|
+
// campaign generation, app-web derives the link form's live icon preview and
|
|
4
|
+
// the icon persisted on save, and @drawbridge/components keys its social glyph
|
|
5
|
+
// map on the same slugs. Adding a platform = one entry here + one glyph there.
|
|
6
|
+
//
|
|
7
|
+
// Order is load-bearing: it is the canonical display order for auto-created
|
|
8
|
+
// social links on the share page.
|
|
9
|
+
|
|
10
|
+
const segment = /^\/.+/;
|
|
11
|
+
|
|
12
|
+
const platforms = [
|
|
13
|
+
{
|
|
14
|
+
platform : 'instagram',
|
|
15
|
+
label : 'Instagram',
|
|
16
|
+
hosts : [ 'instagram.com' ],
|
|
17
|
+
requirePath : true,
|
|
18
|
+
// content permalinks, not profiles
|
|
19
|
+
exclude : /^\/(p|reel|reels|stories|explore)(\/|$)/
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
platform : 'facebook',
|
|
23
|
+
label : 'Facebook',
|
|
24
|
+
hosts : [ 'facebook.com', 'fb.com' ],
|
|
25
|
+
requirePath : true,
|
|
26
|
+
exclude : /^\/(sharer|share|dialog|plugins)(\/|\.|$)/
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
platform : 'tiktok',
|
|
30
|
+
label : 'TikTok',
|
|
31
|
+
hosts : [ 'tiktok.com' ],
|
|
32
|
+
path : /^\/@./
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
platform : 'youtube',
|
|
36
|
+
label : 'YouTube',
|
|
37
|
+
hosts : [
|
|
38
|
+
{ host : 'youtube.com', path : /^\/(@.|channel\/|c\/|user\/)/ },
|
|
39
|
+
{ host : 'youtu.be', requirePath : true }
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
platform : 'x',
|
|
44
|
+
label : 'X',
|
|
45
|
+
hosts : [ 'x.com', 'twitter.com' ],
|
|
46
|
+
requirePath : true,
|
|
47
|
+
exclude : /^\/(intent|share)(\/|$)/
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
platform : 'linkedin',
|
|
51
|
+
label : 'LinkedIn',
|
|
52
|
+
hosts : [ 'linkedin.com' ],
|
|
53
|
+
path : /^\/(company|in)\/./
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
platform : 'pinterest',
|
|
57
|
+
label : 'Pinterest',
|
|
58
|
+
hosts : [ 'pinterest.com' ],
|
|
59
|
+
requirePath : true,
|
|
60
|
+
exclude : /^\/pin(\/|$)/
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
platform : 'threads',
|
|
64
|
+
label : 'Threads',
|
|
65
|
+
hosts : [ 'threads.net', 'threads.com' ],
|
|
66
|
+
path : /^\/@./
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
platform : 'snapchat',
|
|
70
|
+
label : 'Snapchat',
|
|
71
|
+
hosts : [ 'snapchat.com' ],
|
|
72
|
+
path : /^\/add\/./
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
platform : 'whatsapp',
|
|
76
|
+
label : 'WhatsApp',
|
|
77
|
+
hosts : [
|
|
78
|
+
{ host : 'wa.me', requirePath : true },
|
|
79
|
+
{ host : 'api.whatsapp.com', path : /^\/send/ }
|
|
80
|
+
]
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
platform : 'spotify',
|
|
84
|
+
label : 'Spotify',
|
|
85
|
+
hosts : [ 'open.spotify.com' ],
|
|
86
|
+
path : /^\/(artist|show|user|playlist)\/./
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
platform : 'discord',
|
|
90
|
+
label : 'Discord',
|
|
91
|
+
hosts : [
|
|
92
|
+
{ host : 'discord.gg', requirePath : true },
|
|
93
|
+
{ host : 'discord.com', path : /^\/invite\/./ }
|
|
94
|
+
]
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
platform : 'twitch',
|
|
98
|
+
label : 'Twitch',
|
|
99
|
+
hosts : [ 'twitch.tv' ],
|
|
100
|
+
requirePath : true,
|
|
101
|
+
exclude : /^\/(videos|directory)(\/|$)/
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
platform : 'github',
|
|
105
|
+
label : 'GitHub',
|
|
106
|
+
hosts : [ 'github.com' ],
|
|
107
|
+
requirePath : true
|
|
108
|
+
}
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
// Match a url against the platform table. Tolerates missing schemes and
|
|
112
|
+
// www./m. subdomains; keystroke-safe (never throws) so app-web can call it
|
|
113
|
+
// on every url-field change. Returns { platform, label } or null.
|
|
114
|
+
const matchSocial = ( url ) => {
|
|
115
|
+
|
|
116
|
+
if( ! url || typeof url !== 'string' ) return null;
|
|
117
|
+
|
|
118
|
+
let parsed;
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
|
|
122
|
+
parsed = new URL( url.includes( '://' ) ? url : 'https://' + url.replace( /^\/+/, '' ) );
|
|
123
|
+
|
|
124
|
+
} catch {
|
|
125
|
+
|
|
126
|
+
return null;
|
|
127
|
+
|
|
128
|
+
}
|
|
129
|
+
if( ! /^https?:$/.test( parsed.protocol ) ) return null;
|
|
130
|
+
|
|
131
|
+
const host = parsed.hostname.toLowerCase().replace( /^(www|m)\./, '' );
|
|
132
|
+
const path = parsed.pathname.replace( /\/+$/, '' ) || '/';
|
|
133
|
+
|
|
134
|
+
for( const entry of platforms ){
|
|
135
|
+
|
|
136
|
+
const matched = entry.hosts
|
|
137
|
+
.map( ( h ) => ( typeof h === 'string' ? { host : h } : h ) )
|
|
138
|
+
.find( ( h ) => host === h.host || host.endsWith( '.' + h.host ) );
|
|
139
|
+
|
|
140
|
+
if( ! matched ) continue;
|
|
141
|
+
|
|
142
|
+
// per-host rules override the platform-level ones (discord.gg/<invite>
|
|
143
|
+
// vs discord.com/invite/<invite>); a host match that fails its path
|
|
144
|
+
// rules is terminal — no other platform owns this host
|
|
145
|
+
const exclude = matched.exclude || entry.exclude;
|
|
146
|
+
const pattern = matched.path || entry.path;
|
|
147
|
+
const requirePath = matched.requirePath ?? entry.requirePath;
|
|
148
|
+
|
|
149
|
+
if( exclude && exclude.test( path ) ) return null;
|
|
150
|
+
if( pattern && ! pattern.test( path ) ) return null;
|
|
151
|
+
if( requirePath && ! segment.test( path ) ) return null;
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
label : entry.label,
|
|
155
|
+
platform : entry.platform
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
export { matchSocial, platforms };
|
package/dist/social.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// lib/social.js
|
|
2
|
+
var segment = /^\/.+/;
|
|
3
|
+
var platforms = [
|
|
4
|
+
{
|
|
5
|
+
platform: "instagram",
|
|
6
|
+
label: "Instagram",
|
|
7
|
+
hosts: ["instagram.com"],
|
|
8
|
+
requirePath: true,
|
|
9
|
+
// content permalinks, not profiles
|
|
10
|
+
exclude: /^\/(p|reel|reels|stories|explore)(\/|$)/
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
platform: "facebook",
|
|
14
|
+
label: "Facebook",
|
|
15
|
+
hosts: ["facebook.com", "fb.com"],
|
|
16
|
+
requirePath: true,
|
|
17
|
+
exclude: /^\/(sharer|share|dialog|plugins)(\/|\.|$)/
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
platform: "tiktok",
|
|
21
|
+
label: "TikTok",
|
|
22
|
+
hosts: ["tiktok.com"],
|
|
23
|
+
path: /^\/@./
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
platform: "youtube",
|
|
27
|
+
label: "YouTube",
|
|
28
|
+
hosts: [
|
|
29
|
+
{ host: "youtube.com", path: /^\/(@.|channel\/|c\/|user\/)/ },
|
|
30
|
+
{ host: "youtu.be", requirePath: true }
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
platform: "x",
|
|
35
|
+
label: "X",
|
|
36
|
+
hosts: ["x.com", "twitter.com"],
|
|
37
|
+
requirePath: true,
|
|
38
|
+
exclude: /^\/(intent|share)(\/|$)/
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
platform: "linkedin",
|
|
42
|
+
label: "LinkedIn",
|
|
43
|
+
hosts: ["linkedin.com"],
|
|
44
|
+
path: /^\/(company|in)\/./
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
platform: "pinterest",
|
|
48
|
+
label: "Pinterest",
|
|
49
|
+
hosts: ["pinterest.com"],
|
|
50
|
+
requirePath: true,
|
|
51
|
+
exclude: /^\/pin(\/|$)/
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
platform: "threads",
|
|
55
|
+
label: "Threads",
|
|
56
|
+
hosts: ["threads.net", "threads.com"],
|
|
57
|
+
path: /^\/@./
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
platform: "snapchat",
|
|
61
|
+
label: "Snapchat",
|
|
62
|
+
hosts: ["snapchat.com"],
|
|
63
|
+
path: /^\/add\/./
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
platform: "whatsapp",
|
|
67
|
+
label: "WhatsApp",
|
|
68
|
+
hosts: [
|
|
69
|
+
{ host: "wa.me", requirePath: true },
|
|
70
|
+
{ host: "api.whatsapp.com", path: /^\/send/ }
|
|
71
|
+
]
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
platform: "spotify",
|
|
75
|
+
label: "Spotify",
|
|
76
|
+
hosts: ["open.spotify.com"],
|
|
77
|
+
path: /^\/(artist|show|user|playlist)\/./
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
platform: "discord",
|
|
81
|
+
label: "Discord",
|
|
82
|
+
hosts: [
|
|
83
|
+
{ host: "discord.gg", requirePath: true },
|
|
84
|
+
{ host: "discord.com", path: /^\/invite\/./ }
|
|
85
|
+
]
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
platform: "twitch",
|
|
89
|
+
label: "Twitch",
|
|
90
|
+
hosts: ["twitch.tv"],
|
|
91
|
+
requirePath: true,
|
|
92
|
+
exclude: /^\/(videos|directory)(\/|$)/
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
platform: "github",
|
|
96
|
+
label: "GitHub",
|
|
97
|
+
hosts: ["github.com"],
|
|
98
|
+
requirePath: true
|
|
99
|
+
}
|
|
100
|
+
];
|
|
101
|
+
var matchSocial = (url) => {
|
|
102
|
+
if (!url || typeof url !== "string") return null;
|
|
103
|
+
let parsed;
|
|
104
|
+
try {
|
|
105
|
+
parsed = new URL(url.includes("://") ? url : "https://" + url.replace(/^\/+/, ""));
|
|
106
|
+
} catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
;
|
|
110
|
+
if (!/^https?:$/.test(parsed.protocol)) return null;
|
|
111
|
+
const host = parsed.hostname.toLowerCase().replace(/^(www|m)\./, "");
|
|
112
|
+
const path = parsed.pathname.replace(/\/+$/, "") || "/";
|
|
113
|
+
for (const entry of platforms) {
|
|
114
|
+
const matched = entry.hosts.map((h) => typeof h === "string" ? { host: h } : h).find((h) => host === h.host || host.endsWith("." + h.host));
|
|
115
|
+
if (!matched) continue;
|
|
116
|
+
const exclude = matched.exclude || entry.exclude;
|
|
117
|
+
const pattern = matched.path || entry.path;
|
|
118
|
+
const requirePath = matched.requirePath ?? entry.requirePath;
|
|
119
|
+
if (exclude && exclude.test(path)) return null;
|
|
120
|
+
if (pattern && !pattern.test(path)) return null;
|
|
121
|
+
if (requirePath && !segment.test(path)) return null;
|
|
122
|
+
return {
|
|
123
|
+
label: entry.label,
|
|
124
|
+
platform: entry.platform
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
;
|
|
128
|
+
return null;
|
|
129
|
+
};
|
|
130
|
+
export {
|
|
131
|
+
matchSocial,
|
|
132
|
+
platforms
|
|
133
|
+
};
|
package/package.json
CHANGED
|
@@ -132,6 +132,11 @@
|
|
|
132
132
|
"import": "./dist/slugify.js",
|
|
133
133
|
"require": "./dist/slugify.cjs"
|
|
134
134
|
},
|
|
135
|
+
"./social": {
|
|
136
|
+
"types": "./dist/social.d.ts",
|
|
137
|
+
"import": "./dist/social.js",
|
|
138
|
+
"require": "./dist/social.cjs"
|
|
139
|
+
},
|
|
135
140
|
"./token": {
|
|
136
141
|
"types": "./dist/token.d.ts",
|
|
137
142
|
"import": "./dist/token.js",
|
|
@@ -164,5 +169,5 @@
|
|
|
164
169
|
"test": ". \"$HOME/.nvm/nvm.sh\" && nvm use && node --test"
|
|
165
170
|
},
|
|
166
171
|
"types": "dist/index.d.ts",
|
|
167
|
-
"version": "0.0.
|
|
172
|
+
"version": "0.0.93"
|
|
168
173
|
}
|