@stacksjs/registry 0.10.48 → 0.10.50
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/index.js +2080 -126
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -42257,11 +42257,24 @@ class BuildStatusStore {
|
|
|
42257
42257
|
if (changed)
|
|
42258
42258
|
this.snapshot.scheduleSave();
|
|
42259
42259
|
}
|
|
42260
|
-
requestRebuild(domain) {
|
|
42260
|
+
requestRebuild(domain, priority = false) {
|
|
42261
42261
|
const d = String(domain || "").trim();
|
|
42262
|
-
if (!d
|
|
42262
|
+
if (!d)
|
|
42263
42263
|
return false;
|
|
42264
|
-
this.queue.
|
|
42264
|
+
const queuedAt = this.queue.indexOf(d);
|
|
42265
|
+
if (queuedAt !== -1) {
|
|
42266
|
+
if (!priority || queuedAt === 0)
|
|
42267
|
+
return false;
|
|
42268
|
+
this.queue.splice(queuedAt, 1);
|
|
42269
|
+
this.queue.unshift(d);
|
|
42270
|
+
this.snapshot.scheduleSave();
|
|
42271
|
+
this.emit();
|
|
42272
|
+
return true;
|
|
42273
|
+
}
|
|
42274
|
+
if (priority)
|
|
42275
|
+
this.queue.unshift(d);
|
|
42276
|
+
else
|
|
42277
|
+
this.queue.push(d);
|
|
42265
42278
|
this.snapshot.scheduleSave();
|
|
42266
42279
|
this.emit();
|
|
42267
42280
|
return true;
|
|
@@ -42452,24 +42465,145 @@ function compareVersionLoose(a, b) {
|
|
|
42452
42465
|
return 0;
|
|
42453
42466
|
}
|
|
42454
42467
|
|
|
42468
|
+
// src/subscriptions.ts
|
|
42469
|
+
var TIERS = {
|
|
42470
|
+
free: {
|
|
42471
|
+
id: "free",
|
|
42472
|
+
name: "Free",
|
|
42473
|
+
price: 0,
|
|
42474
|
+
commissionBps: 1000,
|
|
42475
|
+
analyticsRetentionDays: 30,
|
|
42476
|
+
maxArtifactBytes: 50 * 1024 * 1024,
|
|
42477
|
+
privatePackages: false,
|
|
42478
|
+
priorityBuilds: false,
|
|
42479
|
+
seats: 1,
|
|
42480
|
+
buildInsurance: false,
|
|
42481
|
+
securityAlerts: false,
|
|
42482
|
+
sbomExport: false,
|
|
42483
|
+
teamEntitlements: false
|
|
42484
|
+
},
|
|
42485
|
+
pro: {
|
|
42486
|
+
id: "pro",
|
|
42487
|
+
name: "Pro",
|
|
42488
|
+
price: 900,
|
|
42489
|
+
commissionBps: 500,
|
|
42490
|
+
analyticsRetentionDays: 3650,
|
|
42491
|
+
maxArtifactBytes: 250 * 1024 * 1024,
|
|
42492
|
+
privatePackages: true,
|
|
42493
|
+
priorityBuilds: true,
|
|
42494
|
+
seats: 1,
|
|
42495
|
+
buildInsurance: true,
|
|
42496
|
+
securityAlerts: true,
|
|
42497
|
+
sbomExport: true,
|
|
42498
|
+
teamEntitlements: false,
|
|
42499
|
+
stripeLookupKey: "pantry_pro_monthly"
|
|
42500
|
+
},
|
|
42501
|
+
team: {
|
|
42502
|
+
id: "team",
|
|
42503
|
+
name: "Team",
|
|
42504
|
+
price: 2900,
|
|
42505
|
+
commissionBps: 500,
|
|
42506
|
+
analyticsRetentionDays: 3650,
|
|
42507
|
+
maxArtifactBytes: 1024 * 1024 * 1024,
|
|
42508
|
+
privatePackages: true,
|
|
42509
|
+
priorityBuilds: true,
|
|
42510
|
+
seats: 10,
|
|
42511
|
+
buildInsurance: true,
|
|
42512
|
+
securityAlerts: true,
|
|
42513
|
+
sbomExport: true,
|
|
42514
|
+
teamEntitlements: true,
|
|
42515
|
+
stripeLookupKey: "pantry_team_monthly"
|
|
42516
|
+
}
|
|
42517
|
+
};
|
|
42518
|
+
var DISCOVERY_FEE_BPS = 300;
|
|
42519
|
+
var MAX_TOTAL_FEE_BPS = 3000;
|
|
42520
|
+
function tierOf(value) {
|
|
42521
|
+
if (value === "pro" || value === "team")
|
|
42522
|
+
return value;
|
|
42523
|
+
return "free";
|
|
42524
|
+
}
|
|
42525
|
+
function tierDefinition(tier) {
|
|
42526
|
+
return TIERS[tier];
|
|
42527
|
+
}
|
|
42528
|
+
function effectiveTier(sub, now = new Date) {
|
|
42529
|
+
if (!sub || sub.tier === "free")
|
|
42530
|
+
return "free";
|
|
42531
|
+
switch (sub.status) {
|
|
42532
|
+
case "active":
|
|
42533
|
+
case "trialing":
|
|
42534
|
+
case "past_due":
|
|
42535
|
+
return sub.tier;
|
|
42536
|
+
case "canceled":
|
|
42537
|
+
return sub.currentPeriodEnd && new Date(sub.currentPeriodEnd) > now ? sub.tier : "free";
|
|
42538
|
+
default:
|
|
42539
|
+
return "free";
|
|
42540
|
+
}
|
|
42541
|
+
}
|
|
42542
|
+
function calculateFee(input) {
|
|
42543
|
+
const commissionBps = tierDefinition(input.sellerTier).commissionBps;
|
|
42544
|
+
const discoveryBps = input.discoveredOnSite ? DISCOVERY_FEE_BPS : 0;
|
|
42545
|
+
const totalBps = Math.min(commissionBps + discoveryBps, MAX_TOTAL_FEE_BPS);
|
|
42546
|
+
const applicationFee = Math.max(0, Math.min(Math.floor(input.amount * totalBps / 1e4), input.amount));
|
|
42547
|
+
return {
|
|
42548
|
+
amount: input.amount,
|
|
42549
|
+
commissionBps,
|
|
42550
|
+
discoveryBps,
|
|
42551
|
+
totalBps,
|
|
42552
|
+
applicationFee,
|
|
42553
|
+
sellerNet: input.amount - applicationFee
|
|
42554
|
+
};
|
|
42555
|
+
}
|
|
42556
|
+
function formatBps(bps) {
|
|
42557
|
+
const percent = bps / 100;
|
|
42558
|
+
return `${Number.isInteger(percent) ? percent : percent.toFixed(2)}%`;
|
|
42559
|
+
}
|
|
42560
|
+
function isDiscovery(origin) {
|
|
42561
|
+
return origin === "site";
|
|
42562
|
+
}
|
|
42563
|
+
|
|
42455
42564
|
// src/paywall.ts
|
|
42456
|
-
|
|
42457
|
-
|
|
42565
|
+
function stripeSecretKey() {
|
|
42566
|
+
return process.env.STRIPE_SECRET_KEY || "";
|
|
42567
|
+
}
|
|
42568
|
+
function stripeWebhookSecret() {
|
|
42569
|
+
return process.env.STRIPE_WEBHOOK_SECRET || "";
|
|
42570
|
+
}
|
|
42571
|
+
function paymentsEnabled() {
|
|
42572
|
+
return stripeSecretKey().length > 0;
|
|
42573
|
+
}
|
|
42574
|
+
function encodeStripeParams(body, prefix = "") {
|
|
42575
|
+
const out = [];
|
|
42576
|
+
for (const [key, value] of Object.entries(body)) {
|
|
42577
|
+
if (value === undefined || value === null)
|
|
42578
|
+
continue;
|
|
42579
|
+
const name = prefix ? `${prefix}[${key}]` : key;
|
|
42580
|
+
if (Array.isArray(value)) {
|
|
42581
|
+
value.forEach((item, i) => {
|
|
42582
|
+
if (item !== null && typeof item === "object")
|
|
42583
|
+
out.push(...encodeStripeParams(item, `${name}[${i}]`));
|
|
42584
|
+
else
|
|
42585
|
+
out.push([`${name}[${i}]`, String(item)]);
|
|
42586
|
+
});
|
|
42587
|
+
} else if (typeof value === "object") {
|
|
42588
|
+
out.push(...encodeStripeParams(value, name));
|
|
42589
|
+
} else {
|
|
42590
|
+
out.push([name, String(value)]);
|
|
42591
|
+
}
|
|
42592
|
+
}
|
|
42593
|
+
return out;
|
|
42594
|
+
}
|
|
42458
42595
|
async function stripeRequest(method, path, body) {
|
|
42459
|
-
|
|
42596
|
+
const key = stripeSecretKey();
|
|
42597
|
+
if (!key)
|
|
42460
42598
|
throw new Error("STRIPE_SECRET_KEY not configured");
|
|
42461
42599
|
const url = `https://api.stripe.com/v1${path}`;
|
|
42462
42600
|
const headers = {
|
|
42463
|
-
Authorization: `Bearer ${
|
|
42601
|
+
Authorization: `Bearer ${key}`
|
|
42464
42602
|
};
|
|
42465
42603
|
let fetchBody;
|
|
42466
42604
|
if (body) {
|
|
42467
42605
|
headers["Content-Type"] = "application/x-www-form-urlencoded";
|
|
42468
|
-
fetchBody = new URLSearchParams(
|
|
42469
|
-
if (Array.isArray(v))
|
|
42470
|
-
return v.map((item, i) => [`${k}[${i}]`, String(item)]);
|
|
42471
|
-
return [[k, String(v)]];
|
|
42472
|
-
})).toString();
|
|
42606
|
+
fetchBody = new URLSearchParams(encodeStripeParams(body)).toString();
|
|
42473
42607
|
}
|
|
42474
42608
|
const res = await fetch(url, { method, headers, body: fetchBody });
|
|
42475
42609
|
if (!res.ok) {
|
|
@@ -42479,93 +42613,247 @@ async function stripeRequest(method, path, body) {
|
|
|
42479
42613
|
}
|
|
42480
42614
|
return res.json();
|
|
42481
42615
|
}
|
|
42482
|
-
|
|
42616
|
+
function accountSubject(email) {
|
|
42617
|
+
return `user:${email.toLowerCase().trim()}`;
|
|
42618
|
+
}
|
|
42619
|
+
async function resolveAccess(storage, packageName, version, buyer, ownerEmail) {
|
|
42483
42620
|
const paywall = await storage.getPaywall(packageName);
|
|
42484
|
-
if (!paywall || !paywall.enabled)
|
|
42485
|
-
return { allowed: true };
|
|
42486
|
-
|
|
42487
|
-
|
|
42488
|
-
|
|
42621
|
+
if (!paywall || !paywall.enabled)
|
|
42622
|
+
return { allowed: true, reason: "no-paywall" };
|
|
42623
|
+
if (paywall.freeVersions?.includes(version))
|
|
42624
|
+
return { allowed: true, paywall, reason: "free-version" };
|
|
42625
|
+
if (buyer.admin)
|
|
42626
|
+
return { allowed: true, paywall, reason: "admin" };
|
|
42627
|
+
const email = buyer.userId?.toLowerCase().trim();
|
|
42628
|
+
const owner = ownerEmail?.toLowerCase().trim();
|
|
42629
|
+
if (owner && (email === owner || buyer.org?.toLowerCase().trim() === owner))
|
|
42630
|
+
return { allowed: true, paywall, reason: "owner" };
|
|
42631
|
+
for (const subject of [email, buyer.org?.toLowerCase().trim()]) {
|
|
42632
|
+
if (!subject)
|
|
42633
|
+
continue;
|
|
42634
|
+
const grant = await storage.getAccessGrant(packageName, accountSubject(subject));
|
|
42635
|
+
if (grant) {
|
|
42636
|
+
if (grant.expiresAt && new Date(grant.expiresAt) < new Date)
|
|
42637
|
+
return { allowed: false, paywall, reason: "expired" };
|
|
42638
|
+
return { allowed: true, paywall, reason: subject === email ? "entitled" : "team-entitled" };
|
|
42639
|
+
}
|
|
42489
42640
|
}
|
|
42490
|
-
if (
|
|
42491
|
-
|
|
42641
|
+
if (buyer.token) {
|
|
42642
|
+
const legacy = await storage.getAccessGrant(packageName, buyer.token);
|
|
42643
|
+
if (legacy)
|
|
42644
|
+
return { allowed: true, paywall, reason: "entitled" };
|
|
42492
42645
|
}
|
|
42493
|
-
|
|
42494
|
-
|
|
42495
|
-
|
|
42646
|
+
return {
|
|
42647
|
+
allowed: false,
|
|
42648
|
+
paywall,
|
|
42649
|
+
reason: email || buyer.token ? "payment-required" : "unauthenticated"
|
|
42650
|
+
};
|
|
42651
|
+
}
|
|
42652
|
+
async function checkPaywallAccess(storage, packageName, version, authToken) {
|
|
42653
|
+
const result = await resolveAccess(storage, packageName, version, { token: authToken });
|
|
42654
|
+
return {
|
|
42655
|
+
allowed: result.allowed,
|
|
42656
|
+
paywall: result.paywall,
|
|
42657
|
+
reason: result.allowed ? undefined : result.reason === "unauthenticated" ? "Authentication required for paid package" : "Payment required"
|
|
42658
|
+
};
|
|
42659
|
+
}
|
|
42660
|
+
async function isEntitled(storage, packageName, email, org) {
|
|
42661
|
+
for (const subject of [email, org]) {
|
|
42662
|
+
if (!subject)
|
|
42663
|
+
continue;
|
|
42664
|
+
const grant = await storage.getAccessGrant(packageName, accountSubject(subject));
|
|
42665
|
+
if (!grant)
|
|
42666
|
+
continue;
|
|
42667
|
+
if (grant.expiresAt && new Date(grant.expiresAt) < new Date)
|
|
42668
|
+
continue;
|
|
42669
|
+
return true;
|
|
42496
42670
|
}
|
|
42497
|
-
return
|
|
42671
|
+
return false;
|
|
42672
|
+
}
|
|
42673
|
+
var SUPPORTED_CURRENCIES = new Set(["usd", "eur", "gbp", "cad", "aud", "chf", "jpy", "sek", "nok", "dkk"]);
|
|
42674
|
+
var MIN_PRICE_CENTS = 100;
|
|
42675
|
+
var MAX_PRICE_CENTS = 1e7;
|
|
42676
|
+
function validatePriceConfig(config) {
|
|
42677
|
+
if (typeof config.price !== "number" || !Number.isInteger(config.price))
|
|
42678
|
+
return "Price must be a whole number of cents (e.g. 900 for $9.00)";
|
|
42679
|
+
if (config.price < MIN_PRICE_CENTS)
|
|
42680
|
+
return `Price must be at least ${MIN_PRICE_CENTS} cents ($${(MIN_PRICE_CENTS / 100).toFixed(2)})`;
|
|
42681
|
+
if (config.price > MAX_PRICE_CENTS)
|
|
42682
|
+
return `Price must be at most ${MAX_PRICE_CENTS} cents ($${(MAX_PRICE_CENTS / 100).toFixed(2)})`;
|
|
42683
|
+
const currency = (config.currency || "usd").toLowerCase();
|
|
42684
|
+
if (!SUPPORTED_CURRENCIES.has(currency))
|
|
42685
|
+
return `Unsupported currency "${currency}" \u2014 one of: ${[...SUPPORTED_CURRENCIES].join(", ")}`;
|
|
42686
|
+
if (config.freeVersions && !Array.isArray(config.freeVersions))
|
|
42687
|
+
return "freeVersions must be an array of version strings";
|
|
42688
|
+
if (config.freeVersions?.some((v) => typeof v !== "string" || v.length > 64))
|
|
42689
|
+
return "freeVersions must be version strings";
|
|
42690
|
+
if (config.payoutAccountId && !/^acct_[A-Za-z0-9]+$/.test(config.payoutAccountId))
|
|
42691
|
+
return "payoutAccountId must be a Stripe Connect account id (acct_\u2026)";
|
|
42692
|
+
if (config.trialDays !== undefined && (!Number.isInteger(config.trialDays) || config.trialDays < 0 || config.trialDays > 365))
|
|
42693
|
+
return "trialDays must be a whole number of days between 0 and 365";
|
|
42694
|
+
return null;
|
|
42498
42695
|
}
|
|
42499
42696
|
async function configurePaywall(storage, packageName, config) {
|
|
42697
|
+
const invalid = validatePriceConfig(config);
|
|
42698
|
+
if (invalid)
|
|
42699
|
+
throw new Error(invalid);
|
|
42500
42700
|
const now = new Date().toISOString();
|
|
42501
42701
|
const existing = await storage.getPaywall(packageName);
|
|
42702
|
+
const currency = (config.currency || existing?.currency || "usd").toLowerCase();
|
|
42502
42703
|
let stripeProductId = existing?.stripeProductId;
|
|
42503
42704
|
let stripePriceId = existing?.stripePriceId;
|
|
42504
|
-
if (
|
|
42705
|
+
if (paymentsEnabled()) {
|
|
42505
42706
|
if (!stripeProductId) {
|
|
42506
42707
|
const product = await stripeRequest("POST", "/products", {
|
|
42507
42708
|
name: packageName,
|
|
42508
|
-
description: `Access to ${packageName}
|
|
42709
|
+
description: `Access to ${packageName} on the pantry registry`,
|
|
42509
42710
|
metadata: { pantry_package: packageName }
|
|
42510
42711
|
});
|
|
42511
42712
|
stripeProductId = product.id;
|
|
42512
42713
|
}
|
|
42513
|
-
const
|
|
42514
|
-
|
|
42515
|
-
|
|
42516
|
-
|
|
42517
|
-
|
|
42518
|
-
|
|
42714
|
+
const priceChanged = !stripePriceId || existing?.price !== config.price || existing?.currency?.toLowerCase() !== currency;
|
|
42715
|
+
if (priceChanged) {
|
|
42716
|
+
const price = await stripeRequest("POST", "/prices", {
|
|
42717
|
+
product: stripeProductId,
|
|
42718
|
+
unit_amount: String(config.price),
|
|
42719
|
+
currency
|
|
42720
|
+
});
|
|
42721
|
+
stripePriceId = price.id;
|
|
42722
|
+
}
|
|
42519
42723
|
}
|
|
42520
42724
|
const paywall = {
|
|
42521
42725
|
name: packageName,
|
|
42522
42726
|
enabled: true,
|
|
42523
42727
|
price: config.price,
|
|
42524
|
-
currency
|
|
42728
|
+
currency,
|
|
42525
42729
|
stripeProductId,
|
|
42526
42730
|
stripePriceId,
|
|
42527
|
-
|
|
42528
|
-
|
|
42731
|
+
stripeAccountId: config.payoutAccountId ?? existing?.stripeAccountId,
|
|
42732
|
+
freeVersions: config.freeVersions ?? existing?.freeVersions,
|
|
42733
|
+
trialDays: config.trialDays ?? existing?.trialDays,
|
|
42529
42734
|
createdAt: existing?.createdAt || now,
|
|
42530
42735
|
updatedAt: now
|
|
42531
42736
|
};
|
|
42532
42737
|
await storage.putPaywall(paywall);
|
|
42533
42738
|
return paywall;
|
|
42534
42739
|
}
|
|
42535
|
-
async function createCheckoutSession(storage,
|
|
42740
|
+
async function createCheckoutSession(storage, request) {
|
|
42741
|
+
const { packageName, email, baseUrl } = request;
|
|
42536
42742
|
const paywall = await storage.getPaywall(packageName);
|
|
42537
|
-
if (!paywall || !paywall.enabled)
|
|
42538
|
-
throw new Error("
|
|
42539
|
-
|
|
42540
|
-
|
|
42541
|
-
|
|
42542
|
-
|
|
42543
|
-
|
|
42544
|
-
|
|
42545
|
-
|
|
42743
|
+
if (!paywall || !paywall.enabled)
|
|
42744
|
+
throw new Error("This package is not for sale");
|
|
42745
|
+
if (!paymentsEnabled())
|
|
42746
|
+
throw new Error("Payments are not configured on this registry");
|
|
42747
|
+
if (!paywall.stripePriceId)
|
|
42748
|
+
throw new Error("This package has a price but no Stripe price \u2014 the publisher should save it again");
|
|
42749
|
+
const encoded = encodeURIComponent(packageName);
|
|
42750
|
+
const origin = request.origin || "cli";
|
|
42751
|
+
const fee = calculateFee({
|
|
42752
|
+
amount: paywall.price,
|
|
42753
|
+
sellerTier: request.sellerTier || "free",
|
|
42754
|
+
discoveredOnSite: isDiscovery(origin)
|
|
42755
|
+
});
|
|
42546
42756
|
const session = await stripeRequest("POST", "/checkout/sessions", {
|
|
42547
42757
|
mode: "payment",
|
|
42548
42758
|
"line_items[0][price]": paywall.stripePriceId,
|
|
42549
42759
|
"line_items[0][quantity]": "1",
|
|
42550
|
-
|
|
42551
|
-
|
|
42760
|
+
customer_email: email,
|
|
42761
|
+
client_reference_id: `${packageName}:${email}`,
|
|
42762
|
+
success_url: `${baseUrl}/packages/${encoded}/checkout/success`,
|
|
42763
|
+
cancel_url: `${baseUrl}/pkg/${encoded}`,
|
|
42552
42764
|
"metadata[package_name]": packageName,
|
|
42553
|
-
"metadata[
|
|
42554
|
-
|
|
42765
|
+
"metadata[buyer_email]": (request.org || email).toLowerCase().trim(),
|
|
42766
|
+
"metadata[purchased_by]": email,
|
|
42767
|
+
"metadata[sale_origin]": origin,
|
|
42768
|
+
"metadata[fee_bps]": String(fee.totalBps),
|
|
42769
|
+
"metadata[seller_tier]": request.sellerTier || "free",
|
|
42770
|
+
...paywall.stripeAccountId ? {
|
|
42771
|
+
"payment_intent_data[transfer_data][destination]": paywall.stripeAccountId,
|
|
42772
|
+
"payment_intent_data[on_behalf_of]": paywall.stripeAccountId,
|
|
42773
|
+
...fee.applicationFee > 0 ? { "payment_intent_data[application_fee_amount]": String(fee.applicationFee) } : {}
|
|
42774
|
+
} : {}
|
|
42555
42775
|
});
|
|
42556
42776
|
return { url: session.url };
|
|
42557
42777
|
}
|
|
42778
|
+
var _tierPriceCache = new Map;
|
|
42779
|
+
async function ensureTierPrice(tier) {
|
|
42780
|
+
if (!tier.stripeLookupKey)
|
|
42781
|
+
throw new Error(`${tier.name} is not a paid tier`);
|
|
42782
|
+
const cached = _tierPriceCache.get(tier.stripeLookupKey);
|
|
42783
|
+
if (cached)
|
|
42784
|
+
return cached;
|
|
42785
|
+
const existing = await stripeRequest("GET", `/prices?lookup_keys[]=${encodeURIComponent(tier.stripeLookupKey)}&active=true&limit=1`);
|
|
42786
|
+
if (existing?.data?.length > 0) {
|
|
42787
|
+
_tierPriceCache.set(tier.stripeLookupKey, existing.data[0].id);
|
|
42788
|
+
return existing.data[0].id;
|
|
42789
|
+
}
|
|
42790
|
+
const product = await stripeRequest("POST", "/products", {
|
|
42791
|
+
name: `pantry ${tier.name}`,
|
|
42792
|
+
description: `pantry registry ${tier.name} plan`,
|
|
42793
|
+
metadata: { pantry_tier: tier.id }
|
|
42794
|
+
});
|
|
42795
|
+
const price = await stripeRequest("POST", "/prices", {
|
|
42796
|
+
product: product.id,
|
|
42797
|
+
unit_amount: String(tier.price),
|
|
42798
|
+
currency: "usd",
|
|
42799
|
+
recurring: { interval: "month" },
|
|
42800
|
+
lookup_key: tier.stripeLookupKey
|
|
42801
|
+
});
|
|
42802
|
+
_tierPriceCache.set(tier.stripeLookupKey, price.id);
|
|
42803
|
+
return price.id;
|
|
42804
|
+
}
|
|
42805
|
+
async function createSubscriptionCheckout(options) {
|
|
42806
|
+
if (!paymentsEnabled())
|
|
42807
|
+
throw new Error("Payments are not configured on this registry");
|
|
42808
|
+
if (!options.tier.stripeLookupKey)
|
|
42809
|
+
throw new Error("The free plan does not need a subscription");
|
|
42810
|
+
const priceId = await ensureTierPrice(options.tier);
|
|
42811
|
+
const session = await stripeRequest("POST", "/checkout/sessions", {
|
|
42812
|
+
mode: "subscription",
|
|
42813
|
+
"line_items[0][price]": priceId,
|
|
42814
|
+
"line_items[0][quantity]": "1",
|
|
42815
|
+
...options.stripeCustomerId ? { customer: options.stripeCustomerId } : { customer_email: options.email },
|
|
42816
|
+
success_url: `${options.baseUrl}/account?subscribed=${options.tier.id}`,
|
|
42817
|
+
cancel_url: `${options.baseUrl}/pricing`,
|
|
42818
|
+
"metadata[pantry_tier]": options.tier.id,
|
|
42819
|
+
"metadata[account_email]": options.email,
|
|
42820
|
+
"subscription_data[metadata][pantry_tier]": options.tier.id,
|
|
42821
|
+
"subscription_data[metadata][account_email]": options.email
|
|
42822
|
+
});
|
|
42823
|
+
return { url: session.url };
|
|
42824
|
+
}
|
|
42825
|
+
async function createBillingPortalSession(customerId, returnUrl) {
|
|
42826
|
+
if (!paymentsEnabled())
|
|
42827
|
+
throw new Error("Payments are not configured on this registry");
|
|
42828
|
+
const session = await stripeRequest("POST", "/billing_portal/sessions", {
|
|
42829
|
+
customer: customerId,
|
|
42830
|
+
return_url: returnUrl
|
|
42831
|
+
});
|
|
42832
|
+
return { url: session.url };
|
|
42833
|
+
}
|
|
42834
|
+
function subscriptionChangeFrom(object) {
|
|
42835
|
+
const email = object?.metadata?.account_email;
|
|
42836
|
+
const tier = object?.metadata?.pantry_tier;
|
|
42837
|
+
if (!email || !tier)
|
|
42838
|
+
return null;
|
|
42839
|
+
return {
|
|
42840
|
+
email,
|
|
42841
|
+
tier,
|
|
42842
|
+
status: object.cancel_at_period_end && object.status === "active" ? "canceled" : object.status || "none",
|
|
42843
|
+
stripeCustomerId: typeof object.customer === "string" ? object.customer : object.customer?.id,
|
|
42844
|
+
stripeSubscriptionId: object.id,
|
|
42845
|
+
currentPeriodEnd: object.current_period_end ? new Date(object.current_period_end * 1000).toISOString() : undefined
|
|
42846
|
+
};
|
|
42847
|
+
}
|
|
42558
42848
|
var processedWebhookEvents = new Map;
|
|
42559
42849
|
var WEBHOOK_DEDUP_TTL = 10 * 60 * 1000;
|
|
42560
|
-
async function handleStripeWebhook(storage, rawBody, signature) {
|
|
42561
|
-
if (!
|
|
42850
|
+
async function handleStripeWebhook(storage, rawBody, signature, onSubscription) {
|
|
42851
|
+
if (!stripeWebhookSecret())
|
|
42562
42852
|
throw new Error("STRIPE_WEBHOOK_SECRET not configured");
|
|
42563
|
-
}
|
|
42564
42853
|
const event = await verifyStripeWebhook(rawBody, signature);
|
|
42565
42854
|
if (event.id) {
|
|
42566
|
-
if (processedWebhookEvents.has(event.id))
|
|
42855
|
+
if (processedWebhookEvents.has(event.id))
|
|
42567
42856
|
return { processed: true };
|
|
42568
|
-
}
|
|
42569
42857
|
processedWebhookEvents.set(event.id, Date.now());
|
|
42570
42858
|
if (processedWebhookEvents.size > 100) {
|
|
42571
42859
|
const now = Date.now();
|
|
@@ -42575,18 +42863,53 @@ async function handleStripeWebhook(storage, rawBody, signature) {
|
|
|
42575
42863
|
}
|
|
42576
42864
|
}
|
|
42577
42865
|
}
|
|
42866
|
+
if (event.type?.startsWith("customer.subscription.") && onSubscription) {
|
|
42867
|
+
const change = subscriptionChangeFrom(event.data.object);
|
|
42868
|
+
if (change) {
|
|
42869
|
+
const applied = event.type === "customer.subscription.deleted" ? { ...change, status: "canceled" } : change;
|
|
42870
|
+
await onSubscription(applied);
|
|
42871
|
+
return { processed: true, subscription: `${applied.email}:${applied.tier}:${applied.status}` };
|
|
42872
|
+
}
|
|
42873
|
+
return { processed: false };
|
|
42874
|
+
}
|
|
42578
42875
|
if (event.type === "checkout.session.completed") {
|
|
42579
42876
|
const session = event.data.object;
|
|
42877
|
+
if (session.mode === "subscription" && onSubscription) {
|
|
42878
|
+
const email2 = session.metadata?.account_email;
|
|
42879
|
+
const tier = session.metadata?.pantry_tier;
|
|
42880
|
+
if (email2 && tier) {
|
|
42881
|
+
await onSubscription({
|
|
42882
|
+
email: email2,
|
|
42883
|
+
tier,
|
|
42884
|
+
status: "active",
|
|
42885
|
+
stripeCustomerId: typeof session.customer === "string" ? session.customer : session.customer?.id,
|
|
42886
|
+
stripeSubscriptionId: typeof session.subscription === "string" ? session.subscription : session.subscription?.id
|
|
42887
|
+
});
|
|
42888
|
+
return { processed: true, subscription: `${email2}:${tier}:active` };
|
|
42889
|
+
}
|
|
42890
|
+
return { processed: false };
|
|
42891
|
+
}
|
|
42580
42892
|
const packageName = session.metadata?.package_name;
|
|
42581
|
-
const
|
|
42582
|
-
|
|
42893
|
+
const email = session.metadata?.buyer_email;
|
|
42894
|
+
const paid = !session.payment_status || session.payment_status === "paid" || session.payment_status === "no_payment_required";
|
|
42895
|
+
if (packageName && email && paid) {
|
|
42583
42896
|
const grant = {
|
|
42584
42897
|
packageName,
|
|
42585
|
-
token:
|
|
42898
|
+
token: accountSubject(email),
|
|
42586
42899
|
stripePaymentId: session.payment_intent || session.id,
|
|
42587
42900
|
grantedAt: new Date().toISOString()
|
|
42588
42901
|
};
|
|
42589
42902
|
await storage.putAccessGrant(grant);
|
|
42903
|
+
return { processed: true, granted: email };
|
|
42904
|
+
}
|
|
42905
|
+
const legacyToken = session.metadata?.access_token;
|
|
42906
|
+
if (packageName && legacyToken && paid) {
|
|
42907
|
+
await storage.putAccessGrant({
|
|
42908
|
+
packageName,
|
|
42909
|
+
token: legacyToken,
|
|
42910
|
+
stripePaymentId: session.payment_intent || session.id,
|
|
42911
|
+
grantedAt: new Date().toISOString()
|
|
42912
|
+
});
|
|
42590
42913
|
return { processed: true };
|
|
42591
42914
|
}
|
|
42592
42915
|
}
|
|
@@ -42600,32 +42923,40 @@ async function verifyStripeWebhook(rawBody, signature) {
|
|
|
42600
42923
|
}, {});
|
|
42601
42924
|
const timestamp = parts.t;
|
|
42602
42925
|
const expectedSig = parts.v1;
|
|
42603
|
-
if (!timestamp || !expectedSig)
|
|
42926
|
+
if (!timestamp || !expectedSig)
|
|
42604
42927
|
throw new Error("Invalid Stripe signature format");
|
|
42605
|
-
}
|
|
42606
42928
|
const payload = `${timestamp}.${rawBody}`;
|
|
42607
|
-
const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(
|
|
42929
|
+
const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(stripeWebhookSecret()), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
42608
42930
|
const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(payload));
|
|
42609
42931
|
const computedSig = Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
42610
42932
|
const computedBuf = Buffer.from(computedSig);
|
|
42611
42933
|
const expectedBuf = Buffer.from(expectedSig);
|
|
42612
|
-
if (computedBuf.length !== expectedBuf.length || !crypto.timingSafeEqual(computedBuf, expectedBuf))
|
|
42934
|
+
if (computedBuf.length !== expectedBuf.length || !crypto.timingSafeEqual(computedBuf, expectedBuf))
|
|
42613
42935
|
throw new Error("Stripe webhook signature verification failed");
|
|
42614
|
-
}
|
|
42615
42936
|
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
|
|
42616
|
-
if (age > 300)
|
|
42937
|
+
if (age > 300)
|
|
42617
42938
|
throw new Error("Stripe webhook timestamp too old");
|
|
42618
|
-
}
|
|
42619
42939
|
try {
|
|
42620
42940
|
return JSON.parse(rawBody);
|
|
42621
42941
|
} catch {
|
|
42622
42942
|
throw new Error("Invalid webhook body JSON");
|
|
42623
42943
|
}
|
|
42624
42944
|
}
|
|
42945
|
+
var CURRENCY_SYMBOLS = {
|
|
42946
|
+
usd: "$",
|
|
42947
|
+
eur: "\u20AC",
|
|
42948
|
+
gbp: "\xA3",
|
|
42949
|
+
cad: "CA$",
|
|
42950
|
+
aud: "A$",
|
|
42951
|
+
jpy: "\xA5"
|
|
42952
|
+
};
|
|
42953
|
+
var ZERO_DECIMAL = new Set(["jpy"]);
|
|
42625
42954
|
function formatPrice(price, currency) {
|
|
42626
|
-
const
|
|
42627
|
-
const symbol =
|
|
42628
|
-
|
|
42955
|
+
const code = (currency || "usd").toLowerCase();
|
|
42956
|
+
const symbol = CURRENCY_SYMBOLS[code] || `${code.toUpperCase()} `;
|
|
42957
|
+
if (ZERO_DECIMAL.has(code))
|
|
42958
|
+
return `${symbol}${price}`;
|
|
42959
|
+
return `${symbol}${(price / 100).toFixed(2)}`;
|
|
42629
42960
|
}
|
|
42630
42961
|
|
|
42631
42962
|
// ../../node_modules/@stacksjs/stx/dist/index.js
|
|
@@ -100217,6 +100548,9 @@ class AuthService {
|
|
|
100217
100548
|
return { email: user.email, name: user.name, role: user.role || "user", createdAt: user.createdAt, updatedAt: user.updatedAt };
|
|
100218
100549
|
}
|
|
100219
100550
|
async upsertAdminUser(email, name, password) {
|
|
100551
|
+
return this.upsertUserAccount(email, name, password, "admin");
|
|
100552
|
+
}
|
|
100553
|
+
async upsertUserAccount(email, name, password, role = "user") {
|
|
100220
100554
|
const normalizedEmail = email.toLowerCase().trim();
|
|
100221
100555
|
if (!normalizedEmail || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalizedEmail)) {
|
|
100222
100556
|
throw new AuthError("Invalid email address", 400);
|
|
@@ -100229,14 +100563,14 @@ class AuthService {
|
|
|
100229
100563
|
const passwordHash = await hashPassword(password);
|
|
100230
100564
|
const user = {
|
|
100231
100565
|
email: normalizedEmail,
|
|
100232
|
-
name: (name || existing?.name || "Admin").trim(),
|
|
100566
|
+
name: (name || existing?.name || (role === "admin" ? "Admin" : normalizedEmail.split("@")[0])).trim(),
|
|
100233
100567
|
passwordHash,
|
|
100234
|
-
role
|
|
100568
|
+
role,
|
|
100235
100569
|
createdAt: existing?.createdAt || now,
|
|
100236
100570
|
updatedAt: now
|
|
100237
100571
|
};
|
|
100238
100572
|
await this.storage.upsertUser(user);
|
|
100239
|
-
return { email: user.email, name: user.name, role
|
|
100573
|
+
return { email: user.email, name: user.name, role, createdAt: user.createdAt, updatedAt: user.updatedAt };
|
|
100240
100574
|
}
|
|
100241
100575
|
async login(email, password) {
|
|
100242
100576
|
const normalizedEmail = email.toLowerCase().trim();
|
|
@@ -100324,6 +100658,9 @@ class AuthService {
|
|
|
100324
100658
|
await this.storage.deleteApiToken(userId, tokenId);
|
|
100325
100659
|
}
|
|
100326
100660
|
async validatePublishToken(token, legacyToken) {
|
|
100661
|
+
return this.validateAccessToken(token, legacyToken, "publish");
|
|
100662
|
+
}
|
|
100663
|
+
async validateAccessToken(token, legacyToken, permission) {
|
|
100327
100664
|
if (legacyToken) {
|
|
100328
100665
|
const maxLen = Math.max(token.length, legacyToken.length);
|
|
100329
100666
|
const tokenBuf = Buffer.alloc(maxLen);
|
|
@@ -100342,14 +100679,150 @@ class AuthService {
|
|
|
100342
100679
|
if (tokenRecord.expiresAt && new Date(tokenRecord.expiresAt) < new Date) {
|
|
100343
100680
|
return { valid: false, error: "Token has expired" };
|
|
100344
100681
|
}
|
|
100345
|
-
|
|
100346
|
-
|
|
100682
|
+
const permitted = permission === "read" ? tokenRecord.permissions.includes("read") || tokenRecord.permissions.includes("publish") : tokenRecord.permissions.includes("publish");
|
|
100683
|
+
if (!permitted) {
|
|
100684
|
+
return { valid: false, error: `Token does not have ${permission} permission` };
|
|
100347
100685
|
}
|
|
100348
100686
|
this.storage.updateTokenLastUsed(tokenRecord.tokenHash).catch((err) => console.warn("Failed to update token last-used:", err));
|
|
100349
100687
|
return { valid: true, userId: tokenRecord.userId, tokenId: tokenRecord.id };
|
|
100350
100688
|
}
|
|
100351
100689
|
return { valid: false, error: "Invalid token" };
|
|
100352
100690
|
}
|
|
100691
|
+
async findUser(email) {
|
|
100692
|
+
const user = await this.storage.getUser(email.toLowerCase().trim());
|
|
100693
|
+
if (!user)
|
|
100694
|
+
return null;
|
|
100695
|
+
return {
|
|
100696
|
+
email: user.email,
|
|
100697
|
+
name: user.name,
|
|
100698
|
+
role: user.role || "user",
|
|
100699
|
+
subscription: user.subscription,
|
|
100700
|
+
createdAt: user.createdAt,
|
|
100701
|
+
updatedAt: user.updatedAt
|
|
100702
|
+
};
|
|
100703
|
+
}
|
|
100704
|
+
async getSubscription(email) {
|
|
100705
|
+
const user = await this.storage.getUser(email.toLowerCase().trim());
|
|
100706
|
+
if (!user?.subscription)
|
|
100707
|
+
return null;
|
|
100708
|
+
return {
|
|
100709
|
+
tier: tierOf(user.subscription.tier),
|
|
100710
|
+
status: user.subscription.status || "none",
|
|
100711
|
+
stripeCustomerId: user.subscription.stripeCustomerId,
|
|
100712
|
+
stripeSubscriptionId: user.subscription.stripeSubscriptionId,
|
|
100713
|
+
currentPeriodEnd: user.subscription.currentPeriodEnd,
|
|
100714
|
+
updatedAt: user.subscription.updatedAt
|
|
100715
|
+
};
|
|
100716
|
+
}
|
|
100717
|
+
async getTier(email) {
|
|
100718
|
+
if (!email || email === "_admin")
|
|
100719
|
+
return "free";
|
|
100720
|
+
return effectiveTier(await this.getSubscription(email));
|
|
100721
|
+
}
|
|
100722
|
+
async setSubscription(email, sub) {
|
|
100723
|
+
const normalized = email.toLowerCase().trim();
|
|
100724
|
+
const user = await this.storage.getUser(normalized);
|
|
100725
|
+
if (!user)
|
|
100726
|
+
throw new AuthError(`No such account: ${email}`, 404);
|
|
100727
|
+
const next = {
|
|
100728
|
+
...user,
|
|
100729
|
+
updatedAt: new Date().toISOString()
|
|
100730
|
+
};
|
|
100731
|
+
if (!sub || sub.tier === "free") {
|
|
100732
|
+
delete next.subscription;
|
|
100733
|
+
} else {
|
|
100734
|
+
next.subscription = {
|
|
100735
|
+
tier: sub.tier,
|
|
100736
|
+
status: sub.status,
|
|
100737
|
+
stripeCustomerId: sub.stripeCustomerId,
|
|
100738
|
+
stripeSubscriptionId: sub.stripeSubscriptionId,
|
|
100739
|
+
currentPeriodEnd: sub.currentPeriodEnd,
|
|
100740
|
+
updatedAt: new Date().toISOString()
|
|
100741
|
+
};
|
|
100742
|
+
}
|
|
100743
|
+
await this.storage.upsertUser(next);
|
|
100744
|
+
}
|
|
100745
|
+
async getTeamMembers(owner) {
|
|
100746
|
+
const user = await this.storage.getUser(owner.toLowerCase().trim());
|
|
100747
|
+
return user?.team?.members ?? [];
|
|
100748
|
+
}
|
|
100749
|
+
async getTeamOwner(email) {
|
|
100750
|
+
const user = await this.storage.getUser(email.toLowerCase().trim());
|
|
100751
|
+
return user?.teamOwner ?? null;
|
|
100752
|
+
}
|
|
100753
|
+
async canActFor(actor, owner) {
|
|
100754
|
+
if (!actor || !owner)
|
|
100755
|
+
return false;
|
|
100756
|
+
const a11 = actor.toLowerCase().trim();
|
|
100757
|
+
const o10 = owner.toLowerCase().trim();
|
|
100758
|
+
if (a11 === o10)
|
|
100759
|
+
return true;
|
|
100760
|
+
return await this.getTeamOwner(a11) === o10;
|
|
100761
|
+
}
|
|
100762
|
+
async addTeamMember(owner, memberEmail, seatLimit) {
|
|
100763
|
+
const ownerEmail = owner.toLowerCase().trim();
|
|
100764
|
+
const member = memberEmail.toLowerCase().trim();
|
|
100765
|
+
if (member === ownerEmail)
|
|
100766
|
+
throw new AuthError("You are already on your own team", 400);
|
|
100767
|
+
const ownerUser = await this.storage.getUser(ownerEmail);
|
|
100768
|
+
if (!ownerUser)
|
|
100769
|
+
throw new AuthError("No such account", 404);
|
|
100770
|
+
const memberUser = await this.storage.getUser(member);
|
|
100771
|
+
if (!memberUser)
|
|
100772
|
+
throw new AuthError(`${memberEmail} does not have an account yet \u2014 they need to sign up first`, 404);
|
|
100773
|
+
if (memberUser.teamOwner && memberUser.teamOwner !== ownerEmail)
|
|
100774
|
+
throw new AuthError(`${memberEmail} is already on another team`, 409);
|
|
100775
|
+
if (memberUser.team?.members?.length)
|
|
100776
|
+
throw new AuthError(`${memberEmail} runs their own team`, 409);
|
|
100777
|
+
const members = ownerUser.team?.members ?? [];
|
|
100778
|
+
if (members.includes(member))
|
|
100779
|
+
return members;
|
|
100780
|
+
if (members.length + 1 >= seatLimit)
|
|
100781
|
+
throw new AuthError(`That plan includes ${seatLimit} seats, and they are all taken`, 402);
|
|
100782
|
+
const next = [...members, member];
|
|
100783
|
+
await this.storage.upsertUser({
|
|
100784
|
+
...ownerUser,
|
|
100785
|
+
team: { members: next, updatedAt: new Date().toISOString() },
|
|
100786
|
+
updatedAt: new Date().toISOString()
|
|
100787
|
+
});
|
|
100788
|
+
await this.storage.upsertUser({
|
|
100789
|
+
...memberUser,
|
|
100790
|
+
teamOwner: ownerEmail,
|
|
100791
|
+
updatedAt: new Date().toISOString()
|
|
100792
|
+
});
|
|
100793
|
+
return next;
|
|
100794
|
+
}
|
|
100795
|
+
async removeTeamMember(owner, memberEmail) {
|
|
100796
|
+
const ownerEmail = owner.toLowerCase().trim();
|
|
100797
|
+
const member = memberEmail.toLowerCase().trim();
|
|
100798
|
+
const ownerUser = await this.storage.getUser(ownerEmail);
|
|
100799
|
+
if (!ownerUser)
|
|
100800
|
+
throw new AuthError("No such account", 404);
|
|
100801
|
+
const next = (ownerUser.team?.members ?? []).filter((m7) => m7 !== member);
|
|
100802
|
+
await this.storage.upsertUser({
|
|
100803
|
+
...ownerUser,
|
|
100804
|
+
team: { members: next, updatedAt: new Date().toISOString() },
|
|
100805
|
+
updatedAt: new Date().toISOString()
|
|
100806
|
+
});
|
|
100807
|
+
const memberUser = await this.storage.getUser(member);
|
|
100808
|
+
if (memberUser?.teamOwner === ownerEmail) {
|
|
100809
|
+
const cleaned = { ...memberUser, updatedAt: new Date().toISOString() };
|
|
100810
|
+
delete cleaned.teamOwner;
|
|
100811
|
+
await this.storage.upsertUser(cleaned);
|
|
100812
|
+
}
|
|
100813
|
+
return next;
|
|
100814
|
+
}
|
|
100815
|
+
async findByStripeCustomer(customerId, candidateEmail) {
|
|
100816
|
+
if (candidateEmail) {
|
|
100817
|
+
const sub = await this.getSubscription(candidateEmail);
|
|
100818
|
+
if (sub?.stripeCustomerId === customerId || !sub?.stripeCustomerId) {
|
|
100819
|
+
const user = await this.storage.getUser(candidateEmail.toLowerCase().trim());
|
|
100820
|
+
if (user)
|
|
100821
|
+
return user.email;
|
|
100822
|
+
}
|
|
100823
|
+
}
|
|
100824
|
+
return null;
|
|
100825
|
+
}
|
|
100353
100826
|
}
|
|
100354
100827
|
|
|
100355
100828
|
class AuthError extends Error {
|
|
@@ -100469,6 +100942,9 @@ class DynamoDBAuthStorage {
|
|
|
100469
100942
|
name: data.name,
|
|
100470
100943
|
passwordHash: data.passwordHash,
|
|
100471
100944
|
role: data.role === "admin" ? "admin" : "user",
|
|
100945
|
+
...data.subscription ? { subscription: JSON.parse(data.subscription) } : {},
|
|
100946
|
+
...data.team ? { team: JSON.parse(data.team) } : {},
|
|
100947
|
+
...data.teamOwner ? { teamOwner: data.teamOwner } : {},
|
|
100472
100948
|
createdAt: data.createdAt,
|
|
100473
100949
|
updatedAt: data.updatedAt
|
|
100474
100950
|
};
|
|
@@ -100483,6 +100959,9 @@ class DynamoDBAuthStorage {
|
|
|
100483
100959
|
name: user.name,
|
|
100484
100960
|
passwordHash: user.passwordHash,
|
|
100485
100961
|
role: user.role || "user",
|
|
100962
|
+
...user.subscription ? { subscription: JSON.stringify(user.subscription) } : {},
|
|
100963
|
+
...user.team ? { team: JSON.stringify(user.team) } : {},
|
|
100964
|
+
...user.teamOwner ? { teamOwner: user.teamOwner } : {},
|
|
100486
100965
|
createdAt: user.createdAt,
|
|
100487
100966
|
updatedAt: user.updatedAt
|
|
100488
100967
|
}),
|
|
@@ -100499,6 +100978,9 @@ class DynamoDBAuthStorage {
|
|
|
100499
100978
|
name: user.name,
|
|
100500
100979
|
passwordHash: user.passwordHash,
|
|
100501
100980
|
role: user.role || "user",
|
|
100981
|
+
...user.subscription ? { subscription: JSON.stringify(user.subscription) } : {},
|
|
100982
|
+
...user.team ? { team: JSON.stringify(user.team) } : {},
|
|
100983
|
+
...user.teamOwner ? { teamOwner: user.teamOwner } : {},
|
|
100502
100984
|
createdAt: user.createdAt,
|
|
100503
100985
|
updatedAt: user.updatedAt
|
|
100504
100986
|
})
|
|
@@ -100704,6 +101186,783 @@ function createAuthStorage(tableName, region) {
|
|
|
100704
101186
|
return new InMemoryAuthStorage;
|
|
100705
101187
|
}
|
|
100706
101188
|
|
|
101189
|
+
// src/plugins.ts
|
|
101190
|
+
function pluginSpecifiers(env2 = process.env) {
|
|
101191
|
+
return (env2.REGISTRY_PLUGINS || "").split(",").map((s11) => s11.trim()).filter(Boolean);
|
|
101192
|
+
}
|
|
101193
|
+
function assertPlugin(value, specifier) {
|
|
101194
|
+
if (!value || typeof value !== "object" || typeof value.name !== "string") {
|
|
101195
|
+
throw new TypeError(`Registry plugin "${specifier}" must default-export an object with a \`name\` (or a function returning one)`);
|
|
101196
|
+
}
|
|
101197
|
+
return value;
|
|
101198
|
+
}
|
|
101199
|
+
async function loadPlugin(specifier) {
|
|
101200
|
+
const target = specifier.startsWith(".") ? new URL(specifier, `file://${process.cwd()}/`).href : specifier;
|
|
101201
|
+
let mod;
|
|
101202
|
+
try {
|
|
101203
|
+
mod = await import(target);
|
|
101204
|
+
} catch (err) {
|
|
101205
|
+
throw new Error(`Failed to load registry plugin "${specifier}": ${err.message}`);
|
|
101206
|
+
}
|
|
101207
|
+
const exported = mod.default;
|
|
101208
|
+
const resolved = typeof exported === "function" ? await exported() : exported;
|
|
101209
|
+
return assertPlugin(resolved, specifier);
|
|
101210
|
+
}
|
|
101211
|
+
var _plugins = null;
|
|
101212
|
+
function loadPlugins(env2 = process.env) {
|
|
101213
|
+
if (_plugins)
|
|
101214
|
+
return _plugins;
|
|
101215
|
+
const specifiers = pluginSpecifiers(env2);
|
|
101216
|
+
_plugins = specifiers.length === 0 ? Promise.resolve([]) : Promise.all(specifiers.map(loadPlugin)).then((plugins) => {
|
|
101217
|
+
console.log(`Loaded ${plugins.length} registry plugin(s): ${plugins.map((p11) => p11.name).join(", ")}`);
|
|
101218
|
+
return plugins;
|
|
101219
|
+
});
|
|
101220
|
+
return _plugins;
|
|
101221
|
+
}
|
|
101222
|
+
function setPlugins(plugins) {
|
|
101223
|
+
_plugins = Promise.resolve(plugins);
|
|
101224
|
+
}
|
|
101225
|
+
function resetPlugins() {
|
|
101226
|
+
_plugins = null;
|
|
101227
|
+
}
|
|
101228
|
+
async function pluginsAuthorize() {
|
|
101229
|
+
const plugins = await loadPlugins();
|
|
101230
|
+
return plugins.some((p11) => typeof p11.authorizeRead === "function");
|
|
101231
|
+
}
|
|
101232
|
+
async function pluginAccessVerdict(ctx) {
|
|
101233
|
+
const plugins = await loadPlugins();
|
|
101234
|
+
let verdict;
|
|
101235
|
+
for (const plugin of plugins) {
|
|
101236
|
+
if (!plugin.authorizeRead)
|
|
101237
|
+
continue;
|
|
101238
|
+
let result;
|
|
101239
|
+
try {
|
|
101240
|
+
result = await plugin.authorizeRead(ctx);
|
|
101241
|
+
} catch (err) {
|
|
101242
|
+
console.error(`Registry plugin "${plugin.name}" authorizeRead threw:`, err);
|
|
101243
|
+
return "deny";
|
|
101244
|
+
}
|
|
101245
|
+
if (result === "deny")
|
|
101246
|
+
return "deny";
|
|
101247
|
+
if (result === "allow")
|
|
101248
|
+
verdict = "allow";
|
|
101249
|
+
}
|
|
101250
|
+
return verdict;
|
|
101251
|
+
}
|
|
101252
|
+
async function pluginResponse(req, ctx) {
|
|
101253
|
+
const plugins = await loadPlugins();
|
|
101254
|
+
for (const plugin of plugins) {
|
|
101255
|
+
if (!plugin.handleRequest)
|
|
101256
|
+
continue;
|
|
101257
|
+
try {
|
|
101258
|
+
const res = await plugin.handleRequest(req, ctx);
|
|
101259
|
+
if (res)
|
|
101260
|
+
return res;
|
|
101261
|
+
} catch (err) {
|
|
101262
|
+
console.error(`Registry plugin "${plugin.name}" handleRequest threw:`, err);
|
|
101263
|
+
return Response.json({ error: "Plugin error" }, { status: 500 });
|
|
101264
|
+
}
|
|
101265
|
+
}
|
|
101266
|
+
return null;
|
|
101267
|
+
}
|
|
101268
|
+
function emitPluginEvent(event) {
|
|
101269
|
+
loadPlugins().then((plugins) => {
|
|
101270
|
+
for (const plugin of plugins) {
|
|
101271
|
+
if (!plugin.onEvent)
|
|
101272
|
+
continue;
|
|
101273
|
+
try {
|
|
101274
|
+
const result = plugin.onEvent(event);
|
|
101275
|
+
if (result && typeof result.catch === "function")
|
|
101276
|
+
result.catch((err) => console.error(`Registry plugin "${plugin.name}" onEvent rejected:`, err));
|
|
101277
|
+
} catch (err) {
|
|
101278
|
+
console.error(`Registry plugin "${plugin.name}" onEvent threw:`, err);
|
|
101279
|
+
}
|
|
101280
|
+
}
|
|
101281
|
+
}).catch(() => {});
|
|
101282
|
+
}
|
|
101283
|
+
|
|
101284
|
+
// src/access.ts
|
|
101285
|
+
function isTruthy(value) {
|
|
101286
|
+
if (!value)
|
|
101287
|
+
return false;
|
|
101288
|
+
const v11 = value.trim().toLowerCase();
|
|
101289
|
+
return v11 === "1" || v11 === "true" || v11 === "yes" || v11 === "on";
|
|
101290
|
+
}
|
|
101291
|
+
function isFalsy(value) {
|
|
101292
|
+
if (value == null)
|
|
101293
|
+
return false;
|
|
101294
|
+
const v11 = value.trim().toLowerCase();
|
|
101295
|
+
return v11 === "0" || v11 === "false" || v11 === "no" || v11 === "off";
|
|
101296
|
+
}
|
|
101297
|
+
function resolveVisibility(env2 = process.env) {
|
|
101298
|
+
const raw = (env2.REGISTRY_VISIBILITY ?? env2.PANTRY_REGISTRY_VISIBILITY ?? "").trim().toLowerCase();
|
|
101299
|
+
if (raw === "private")
|
|
101300
|
+
return "private";
|
|
101301
|
+
if (raw === "public")
|
|
101302
|
+
return "public";
|
|
101303
|
+
if (isTruthy(env2.REGISTRY_PRIVATE))
|
|
101304
|
+
return "private";
|
|
101305
|
+
return "public";
|
|
101306
|
+
}
|
|
101307
|
+
function signupsEnabled(env2 = process.env, visibility = resolveVisibility(env2)) {
|
|
101308
|
+
const raw = env2.REGISTRY_ALLOW_SIGNUP;
|
|
101309
|
+
if (isTruthy(raw))
|
|
101310
|
+
return true;
|
|
101311
|
+
if (isFalsy(raw))
|
|
101312
|
+
return false;
|
|
101313
|
+
return visibility === "public";
|
|
101314
|
+
}
|
|
101315
|
+
function allowedSignupDomains(env2 = process.env) {
|
|
101316
|
+
return (env2.REGISTRY_SIGNUP_DOMAINS || "").split(",").map((d10) => d10.trim().toLowerCase().replace(/^@/, "")).filter(Boolean);
|
|
101317
|
+
}
|
|
101318
|
+
function isSignupEmailAllowed(email, env2 = process.env) {
|
|
101319
|
+
const domains = allowedSignupDomains(env2);
|
|
101320
|
+
if (domains.length === 0)
|
|
101321
|
+
return true;
|
|
101322
|
+
const domain = email.toLowerCase().trim().split("@")[1] || "";
|
|
101323
|
+
return domains.includes(domain);
|
|
101324
|
+
}
|
|
101325
|
+
var ALWAYS_PUBLIC_PATHS = new Set([
|
|
101326
|
+
"/health",
|
|
101327
|
+
"/api/registry-info",
|
|
101328
|
+
"/login",
|
|
101329
|
+
"/signup",
|
|
101330
|
+
"/auth/login",
|
|
101331
|
+
"/auth/logout",
|
|
101332
|
+
"/auth/signup",
|
|
101333
|
+
"/auth/me",
|
|
101334
|
+
"/favicon.ico",
|
|
101335
|
+
"/robots.txt",
|
|
101336
|
+
"/webhooks/stripe"
|
|
101337
|
+
]);
|
|
101338
|
+
function extraPublicPaths(env2 = process.env) {
|
|
101339
|
+
return (env2.REGISTRY_PUBLIC_PATHS || "").split(",").map((p11) => p11.trim()).filter(Boolean);
|
|
101340
|
+
}
|
|
101341
|
+
function isPublicPath(path6, env2 = process.env) {
|
|
101342
|
+
if (ALWAYS_PUBLIC_PATHS.has(path6))
|
|
101343
|
+
return true;
|
|
101344
|
+
for (const prefix of extraPublicPaths(env2)) {
|
|
101345
|
+
if (prefix === "/") {
|
|
101346
|
+
if (path6 === "/")
|
|
101347
|
+
return true;
|
|
101348
|
+
continue;
|
|
101349
|
+
}
|
|
101350
|
+
if (path6 === prefix || path6.startsWith(prefix.endsWith("/") ? prefix : `${prefix}/`))
|
|
101351
|
+
return true;
|
|
101352
|
+
}
|
|
101353
|
+
return false;
|
|
101354
|
+
}
|
|
101355
|
+
function wantsHtml(req) {
|
|
101356
|
+
if (req.method !== "GET")
|
|
101357
|
+
return false;
|
|
101358
|
+
const accept = req.headers.get("accept") || "";
|
|
101359
|
+
return accept.includes("text/html");
|
|
101360
|
+
}
|
|
101361
|
+
async function enforceReadAccess(req, url, options) {
|
|
101362
|
+
const env2 = options.env ?? process.env;
|
|
101363
|
+
const visibility = options.visibility ?? resolveVisibility(env2);
|
|
101364
|
+
const path6 = url.pathname;
|
|
101365
|
+
const cors = options.corsHeaders ?? {};
|
|
101366
|
+
const publicPath = visibility === "public" || isPublicPath(path6, env2);
|
|
101367
|
+
const anonymous = { authenticated: false, userId: null };
|
|
101368
|
+
const identity = publicPath && !await pluginsAuthorize() ? anonymous : await options.identify(req);
|
|
101369
|
+
const verdict = await pluginAccessVerdict({
|
|
101370
|
+
req,
|
|
101371
|
+
url,
|
|
101372
|
+
path: path6,
|
|
101373
|
+
method: req.method,
|
|
101374
|
+
visibility,
|
|
101375
|
+
userId: identity.userId
|
|
101376
|
+
});
|
|
101377
|
+
if (verdict === "allow")
|
|
101378
|
+
return null;
|
|
101379
|
+
if (verdict !== "deny") {
|
|
101380
|
+
if (publicPath)
|
|
101381
|
+
return null;
|
|
101382
|
+
if (identity.authenticated) {
|
|
101383
|
+
emitPluginEvent({ type: "access-granted", path: path6, method: req.method, userId: identity.userId });
|
|
101384
|
+
return null;
|
|
101385
|
+
}
|
|
101386
|
+
}
|
|
101387
|
+
const reason = verdict === "deny" ? "Denied by access policy" : "Authentication required";
|
|
101388
|
+
emitPluginEvent({ type: "access-denied", path: path6, method: req.method, reason });
|
|
101389
|
+
if (wantsHtml(req) && verdict !== "deny") {
|
|
101390
|
+
return new Response(null, {
|
|
101391
|
+
status: 302,
|
|
101392
|
+
headers: { ...cors, Location: `/login?next=${encodeURIComponent(path6 + url.search)}` }
|
|
101393
|
+
});
|
|
101394
|
+
}
|
|
101395
|
+
const registryUrl = env2.BASE_URL || url.origin;
|
|
101396
|
+
return Response.json({
|
|
101397
|
+
error: reason,
|
|
101398
|
+
...verdict === "deny" ? {} : {
|
|
101399
|
+
hint: `This registry is private. Store a token with: pantry token set --registry ${registryUrl}`,
|
|
101400
|
+
docs: "https://pantry.dev/self-hosting"
|
|
101401
|
+
}
|
|
101402
|
+
}, {
|
|
101403
|
+
status: verdict === "deny" ? 403 : 401,
|
|
101404
|
+
headers: {
|
|
101405
|
+
...cors,
|
|
101406
|
+
...verdict === "deny" ? {} : { "WWW-Authenticate": 'Bearer realm="pantry-registry"' }
|
|
101407
|
+
}
|
|
101408
|
+
});
|
|
101409
|
+
}
|
|
101410
|
+
function registryInfo(env2 = process.env, baseUrl) {
|
|
101411
|
+
const visibility = resolveVisibility(env2);
|
|
101412
|
+
const base = (baseUrl || env2.BASE_URL || "").replace(/\/$/, "");
|
|
101413
|
+
return {
|
|
101414
|
+
visibility,
|
|
101415
|
+
requiresAuth: visibility === "private",
|
|
101416
|
+
signupsEnabled: signupsEnabled(env2, visibility),
|
|
101417
|
+
...base ? { loginUrl: `${base}/login` } : {},
|
|
101418
|
+
docs: "https://pantry.dev/self-hosting"
|
|
101419
|
+
};
|
|
101420
|
+
}
|
|
101421
|
+
|
|
101422
|
+
// src/mirror.ts
|
|
101423
|
+
function orgKey(org) {
|
|
101424
|
+
return org.toLowerCase().trim().replace(/[^a-z0-9._@-]/g, "_").replace(/@/g, "_at_");
|
|
101425
|
+
}
|
|
101426
|
+
function safeSegment(value) {
|
|
101427
|
+
const trimmed = String(value || "").trim();
|
|
101428
|
+
if (!trimmed || trimmed.length > 200)
|
|
101429
|
+
return null;
|
|
101430
|
+
if (trimmed.includes("..") || trimmed.startsWith("/"))
|
|
101431
|
+
return null;
|
|
101432
|
+
if (!/^[\w@./+-]+$/.test(trimmed))
|
|
101433
|
+
return null;
|
|
101434
|
+
return trimmed.replace(/\//g, "_");
|
|
101435
|
+
}
|
|
101436
|
+
async function sha256Hex(bytes) {
|
|
101437
|
+
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
101438
|
+
return Array.from(new Uint8Array(digest)).map((b11) => b11.toString(16).padStart(2, "0")).join("");
|
|
101439
|
+
}
|
|
101440
|
+
function integrityMatches(integrity, hex) {
|
|
101441
|
+
if (!integrity)
|
|
101442
|
+
return true;
|
|
101443
|
+
const [algorithm, encoded] = integrity.split("-");
|
|
101444
|
+
if (algorithm !== "sha256" || !encoded)
|
|
101445
|
+
return true;
|
|
101446
|
+
try {
|
|
101447
|
+
const expected = Array.from(Buffer.from(encoded, "base64")).map((b11) => b11.toString(16).padStart(2, "0")).join("");
|
|
101448
|
+
return expected === hex;
|
|
101449
|
+
} catch {
|
|
101450
|
+
return true;
|
|
101451
|
+
}
|
|
101452
|
+
}
|
|
101453
|
+
var defaultFetcher = async (url) => {
|
|
101454
|
+
const res = await fetch(url, { redirect: "follow" });
|
|
101455
|
+
if (!res.ok)
|
|
101456
|
+
throw new Error(`HTTP ${res.status}`);
|
|
101457
|
+
return res.arrayBuffer();
|
|
101458
|
+
};
|
|
101459
|
+
|
|
101460
|
+
class MirrorStore {
|
|
101461
|
+
storage;
|
|
101462
|
+
fetcher;
|
|
101463
|
+
maxArtifactBytes;
|
|
101464
|
+
maxEntries;
|
|
101465
|
+
constructor(storage, options = {}) {
|
|
101466
|
+
this.storage = storage;
|
|
101467
|
+
this.fetcher = options.fetcher || defaultFetcher;
|
|
101468
|
+
this.maxArtifactBytes = options.maxArtifactBytes ?? 200 * 1024 * 1024;
|
|
101469
|
+
this.maxEntries = options.maxEntries ?? 2000;
|
|
101470
|
+
}
|
|
101471
|
+
indexKey(org) {
|
|
101472
|
+
return `mirror/${orgKey(org)}/index.json`;
|
|
101473
|
+
}
|
|
101474
|
+
async getIndex(org) {
|
|
101475
|
+
try {
|
|
101476
|
+
const bytes = await this.storage.download(this.indexKey(org));
|
|
101477
|
+
const parsed = JSON.parse(new TextDecoder().decode(bytes));
|
|
101478
|
+
if (parsed && Array.isArray(parsed.entries))
|
|
101479
|
+
return parsed;
|
|
101480
|
+
} catch {}
|
|
101481
|
+
return { org, updatedAt: new Date().toISOString(), entries: [] };
|
|
101482
|
+
}
|
|
101483
|
+
async putIndex(index) {
|
|
101484
|
+
const body = new TextEncoder().encode(JSON.stringify(index)).buffer;
|
|
101485
|
+
await this.storage.upload(this.indexKey(index.org), body);
|
|
101486
|
+
}
|
|
101487
|
+
async snapshot(org, entries) {
|
|
101488
|
+
const index = await this.getIndex(org);
|
|
101489
|
+
const existing = new Map(index.entries.map((e11) => [`${e11.name}@${e11.version}:${e11.integrity || ""}`, e11]));
|
|
101490
|
+
let mirrored = 0;
|
|
101491
|
+
let skipped = 0;
|
|
101492
|
+
let failed = 0;
|
|
101493
|
+
const touched = [];
|
|
101494
|
+
for (const entry of entries.slice(0, this.maxEntries)) {
|
|
101495
|
+
const name = safeSegment(entry.name);
|
|
101496
|
+
const version3 = safeSegment(entry.version);
|
|
101497
|
+
if (!name || !version3) {
|
|
101498
|
+
failed++;
|
|
101499
|
+
continue;
|
|
101500
|
+
}
|
|
101501
|
+
const identity = `${entry.name}@${entry.version}:${entry.integrity || ""}`;
|
|
101502
|
+
const already = existing.get(identity);
|
|
101503
|
+
if (already?.key) {
|
|
101504
|
+
skipped++;
|
|
101505
|
+
touched.push(already);
|
|
101506
|
+
continue;
|
|
101507
|
+
}
|
|
101508
|
+
const record = {
|
|
101509
|
+
...entry,
|
|
101510
|
+
mirroredAt: new Date().toISOString()
|
|
101511
|
+
};
|
|
101512
|
+
if (!entry.resolved) {
|
|
101513
|
+
record.error = "no download URL";
|
|
101514
|
+
failed++;
|
|
101515
|
+
existing.set(identity, record);
|
|
101516
|
+
touched.push(record);
|
|
101517
|
+
continue;
|
|
101518
|
+
}
|
|
101519
|
+
try {
|
|
101520
|
+
const bytes = await this.fetcher(entry.resolved);
|
|
101521
|
+
if (bytes.byteLength > this.maxArtifactBytes) {
|
|
101522
|
+
record.error = `artifact is larger than the ${Math.round(this.maxArtifactBytes / (1024 * 1024))}MB mirror limit`;
|
|
101523
|
+
failed++;
|
|
101524
|
+
} else {
|
|
101525
|
+
const hex = await sha256Hex(bytes);
|
|
101526
|
+
if (!integrityMatches(entry.integrity, hex)) {
|
|
101527
|
+
record.error = "integrity mismatch \u2014 upstream bytes differ from the lockfile";
|
|
101528
|
+
failed++;
|
|
101529
|
+
} else {
|
|
101530
|
+
const key = `mirror/${orgKey(org)}/${name}/${version3}/${hex.slice(0, 32)}.tgz`;
|
|
101531
|
+
await this.storage.upload(key, bytes);
|
|
101532
|
+
record.key = key;
|
|
101533
|
+
record.size = bytes.byteLength;
|
|
101534
|
+
record.sha256 = hex;
|
|
101535
|
+
mirrored++;
|
|
101536
|
+
}
|
|
101537
|
+
}
|
|
101538
|
+
} catch (err) {
|
|
101539
|
+
record.error = err.message;
|
|
101540
|
+
failed++;
|
|
101541
|
+
}
|
|
101542
|
+
existing.set(identity, record);
|
|
101543
|
+
touched.push(record);
|
|
101544
|
+
}
|
|
101545
|
+
const next = {
|
|
101546
|
+
org,
|
|
101547
|
+
updatedAt: new Date().toISOString(),
|
|
101548
|
+
entries: [...existing.values()]
|
|
101549
|
+
};
|
|
101550
|
+
await this.putIndex(next);
|
|
101551
|
+
return { mirrored, skipped, failed, entries: touched };
|
|
101552
|
+
}
|
|
101553
|
+
async fetchArtifact(org, name, version3) {
|
|
101554
|
+
const index = await this.getIndex(org);
|
|
101555
|
+
const matches = index.entries.filter((e11) => e11.name === name && e11.version === version3 && e11.key).sort((a11, b11) => b11.mirroredAt.localeCompare(a11.mirroredAt));
|
|
101556
|
+
for (const record of matches) {
|
|
101557
|
+
try {
|
|
101558
|
+
return { bytes: await this.storage.download(record.key), record };
|
|
101559
|
+
} catch {}
|
|
101560
|
+
}
|
|
101561
|
+
return null;
|
|
101562
|
+
}
|
|
101563
|
+
async list(org) {
|
|
101564
|
+
const index = await this.getIndex(org);
|
|
101565
|
+
return [...index.entries].sort((a11, b11) => b11.mirroredAt.localeCompare(a11.mirroredAt));
|
|
101566
|
+
}
|
|
101567
|
+
async stats(org) {
|
|
101568
|
+
const entries = await this.list(org);
|
|
101569
|
+
return {
|
|
101570
|
+
artifacts: entries.length,
|
|
101571
|
+
stored: entries.filter((e11) => e11.key).length,
|
|
101572
|
+
bytes: entries.reduce((sum, e11) => sum + (e11.size || 0), 0),
|
|
101573
|
+
failed: entries.filter((e11) => e11.error).length
|
|
101574
|
+
};
|
|
101575
|
+
}
|
|
101576
|
+
}
|
|
101577
|
+
function normalizeEntries(value) {
|
|
101578
|
+
if (!Array.isArray(value))
|
|
101579
|
+
return [];
|
|
101580
|
+
const out = [];
|
|
101581
|
+
for (const item of value) {
|
|
101582
|
+
if (!item || typeof item !== "object")
|
|
101583
|
+
continue;
|
|
101584
|
+
const entry = item;
|
|
101585
|
+
if (typeof entry.name !== "string" || typeof entry.version !== "string")
|
|
101586
|
+
continue;
|
|
101587
|
+
out.push({
|
|
101588
|
+
name: entry.name,
|
|
101589
|
+
version: entry.version,
|
|
101590
|
+
resolved: typeof entry.resolved === "string" ? entry.resolved : undefined,
|
|
101591
|
+
integrity: typeof entry.integrity === "string" ? entry.integrity : undefined,
|
|
101592
|
+
ecosystem: typeof entry.ecosystem === "string" ? entry.ecosystem : undefined,
|
|
101593
|
+
license: typeof entry.license === "string" ? entry.license : undefined
|
|
101594
|
+
});
|
|
101595
|
+
}
|
|
101596
|
+
return out;
|
|
101597
|
+
}
|
|
101598
|
+
|
|
101599
|
+
// src/security.ts
|
|
101600
|
+
var SEVERITY_ORDER = {
|
|
101601
|
+
critical: 0,
|
|
101602
|
+
high: 1,
|
|
101603
|
+
moderate: 2,
|
|
101604
|
+
low: 3,
|
|
101605
|
+
unknown: 4
|
|
101606
|
+
};
|
|
101607
|
+
function osvEcosystem(ecosystem) {
|
|
101608
|
+
switch ((ecosystem || "npm").toLowerCase()) {
|
|
101609
|
+
case "npm":
|
|
101610
|
+
return "npm";
|
|
101611
|
+
case "pypi":
|
|
101612
|
+
case "python":
|
|
101613
|
+
return "PyPI";
|
|
101614
|
+
case "cargo":
|
|
101615
|
+
case "crates":
|
|
101616
|
+
return "crates.io";
|
|
101617
|
+
case "go":
|
|
101618
|
+
return "Go";
|
|
101619
|
+
case "composer":
|
|
101620
|
+
case "packagist":
|
|
101621
|
+
return "Packagist";
|
|
101622
|
+
case "rubygems":
|
|
101623
|
+
case "gem":
|
|
101624
|
+
return "RubyGems";
|
|
101625
|
+
case "maven":
|
|
101626
|
+
return "Maven";
|
|
101627
|
+
case "nuget":
|
|
101628
|
+
return "NuGet";
|
|
101629
|
+
default:
|
|
101630
|
+
return "";
|
|
101631
|
+
}
|
|
101632
|
+
}
|
|
101633
|
+
function severityOf(vuln) {
|
|
101634
|
+
const explicit = vuln?.database_specific?.severity || vuln?.affected?.[0]?.database_specific?.severity;
|
|
101635
|
+
if (typeof explicit === "string") {
|
|
101636
|
+
const normalized = explicit.toLowerCase();
|
|
101637
|
+
if (normalized === "critical" || normalized === "high" || normalized === "moderate" || normalized === "low")
|
|
101638
|
+
return normalized;
|
|
101639
|
+
if (normalized === "medium")
|
|
101640
|
+
return "moderate";
|
|
101641
|
+
}
|
|
101642
|
+
const score = vuln?.severity?.find((s11) => s11.type?.startsWith("CVSS"))?.score;
|
|
101643
|
+
if (typeof score === "string") {
|
|
101644
|
+
const match = score.match(/\/(?:AV|A):/) ? null : Number.parseFloat(score);
|
|
101645
|
+
if (match !== null && Number.isFinite(match)) {
|
|
101646
|
+
if (match >= 9)
|
|
101647
|
+
return "critical";
|
|
101648
|
+
if (match >= 7)
|
|
101649
|
+
return "high";
|
|
101650
|
+
if (match >= 4)
|
|
101651
|
+
return "moderate";
|
|
101652
|
+
if (match > 0)
|
|
101653
|
+
return "low";
|
|
101654
|
+
}
|
|
101655
|
+
}
|
|
101656
|
+
return "unknown";
|
|
101657
|
+
}
|
|
101658
|
+
function fixedVersionOf(vuln) {
|
|
101659
|
+
for (const affected of vuln?.affected || []) {
|
|
101660
|
+
for (const range of affected?.ranges || []) {
|
|
101661
|
+
for (const event of range?.events || []) {
|
|
101662
|
+
if (event?.fixed)
|
|
101663
|
+
return String(event.fixed);
|
|
101664
|
+
}
|
|
101665
|
+
}
|
|
101666
|
+
}
|
|
101667
|
+
return;
|
|
101668
|
+
}
|
|
101669
|
+
var osvFetcher = async (entries) => {
|
|
101670
|
+
const queries = entries.map((e11) => ({ entry: e11, ecosystem: osvEcosystem(e11.ecosystem) })).filter((q11) => q11.ecosystem);
|
|
101671
|
+
const found = new Map;
|
|
101672
|
+
if (queries.length === 0)
|
|
101673
|
+
return found;
|
|
101674
|
+
const CHUNK = 500;
|
|
101675
|
+
for (let i9 = 0;i9 < queries.length; i9 += CHUNK) {
|
|
101676
|
+
const chunk = queries.slice(i9, i9 + CHUNK);
|
|
101677
|
+
const res = await fetch("https://api.osv.dev/v1/querybatch", {
|
|
101678
|
+
method: "POST",
|
|
101679
|
+
headers: { "Content-Type": "application/json" },
|
|
101680
|
+
body: JSON.stringify({
|
|
101681
|
+
queries: chunk.map((q11) => ({
|
|
101682
|
+
package: { name: q11.entry.name, ecosystem: q11.ecosystem },
|
|
101683
|
+
version: q11.entry.version
|
|
101684
|
+
}))
|
|
101685
|
+
})
|
|
101686
|
+
});
|
|
101687
|
+
if (!res.ok)
|
|
101688
|
+
throw new Error(`OSV responded ${res.status}`);
|
|
101689
|
+
const body = await res.json();
|
|
101690
|
+
const results = body.results || [];
|
|
101691
|
+
for (let j10 = 0;j10 < chunk.length; j10++) {
|
|
101692
|
+
const ids = (results[j10]?.vulns || []).map((v11) => v11.id).slice(0, 25);
|
|
101693
|
+
if (ids.length === 0)
|
|
101694
|
+
continue;
|
|
101695
|
+
const alerts = [];
|
|
101696
|
+
for (const id2 of ids) {
|
|
101697
|
+
try {
|
|
101698
|
+
const detail = await fetch(`https://api.osv.dev/v1/vulns/${encodeURIComponent(id2)}`);
|
|
101699
|
+
const vuln = detail.ok ? await detail.json() : null;
|
|
101700
|
+
alerts.push({
|
|
101701
|
+
type: "vulnerability",
|
|
101702
|
+
package: chunk[j10].entry.name,
|
|
101703
|
+
version: chunk[j10].entry.version,
|
|
101704
|
+
ecosystem: chunk[j10].ecosystem,
|
|
101705
|
+
id: id2,
|
|
101706
|
+
severity: vuln ? severityOf(vuln) : "unknown",
|
|
101707
|
+
summary: vuln?.summary || vuln?.details?.slice(0, 200) || id2,
|
|
101708
|
+
url: `https://osv.dev/vulnerability/${id2}`,
|
|
101709
|
+
fixedIn: vuln ? fixedVersionOf(vuln) : undefined
|
|
101710
|
+
});
|
|
101711
|
+
} catch {
|
|
101712
|
+
alerts.push({
|
|
101713
|
+
type: "vulnerability",
|
|
101714
|
+
package: chunk[j10].entry.name,
|
|
101715
|
+
version: chunk[j10].entry.version,
|
|
101716
|
+
ecosystem: chunk[j10].ecosystem,
|
|
101717
|
+
id: id2,
|
|
101718
|
+
severity: "unknown",
|
|
101719
|
+
summary: id2,
|
|
101720
|
+
url: `https://osv.dev/vulnerability/${id2}`
|
|
101721
|
+
});
|
|
101722
|
+
}
|
|
101723
|
+
}
|
|
101724
|
+
found.set(`${chunk[j10].entry.name}@${chunk[j10].entry.version}`, alerts);
|
|
101725
|
+
}
|
|
101726
|
+
}
|
|
101727
|
+
return found;
|
|
101728
|
+
};
|
|
101729
|
+
function normalizeLicense(license) {
|
|
101730
|
+
return license.trim().toUpperCase();
|
|
101731
|
+
}
|
|
101732
|
+
function checkLicense(entry, policy) {
|
|
101733
|
+
if (!policy || !policy.allow?.length && !policy.deny?.length)
|
|
101734
|
+
return null;
|
|
101735
|
+
const license = entry.license?.trim();
|
|
101736
|
+
if (!license) {
|
|
101737
|
+
if (!policy.allow?.length)
|
|
101738
|
+
return null;
|
|
101739
|
+
return {
|
|
101740
|
+
type: "license",
|
|
101741
|
+
package: entry.name,
|
|
101742
|
+
version: entry.version,
|
|
101743
|
+
license: "unknown",
|
|
101744
|
+
reason: "unknown",
|
|
101745
|
+
summary: `${entry.name}@${entry.version} does not declare a licence, and your policy allows only: ${policy.allow.join(", ")}`
|
|
101746
|
+
};
|
|
101747
|
+
}
|
|
101748
|
+
const normalized = normalizeLicense(license);
|
|
101749
|
+
if (policy.deny?.some((d10) => normalizeLicense(d10) === normalized)) {
|
|
101750
|
+
return {
|
|
101751
|
+
type: "license",
|
|
101752
|
+
package: entry.name,
|
|
101753
|
+
version: entry.version,
|
|
101754
|
+
license,
|
|
101755
|
+
reason: "denied",
|
|
101756
|
+
summary: `${entry.name}@${entry.version} is ${license}, which your policy denies`
|
|
101757
|
+
};
|
|
101758
|
+
}
|
|
101759
|
+
if (policy.allow?.length && !policy.allow.some((a11) => normalizeLicense(a11) === normalized)) {
|
|
101760
|
+
return {
|
|
101761
|
+
type: "license",
|
|
101762
|
+
package: entry.name,
|
|
101763
|
+
version: entry.version,
|
|
101764
|
+
license,
|
|
101765
|
+
reason: "not-allowed",
|
|
101766
|
+
summary: `${entry.name}@${entry.version} is ${license}, which is not in your allowed list`
|
|
101767
|
+
};
|
|
101768
|
+
}
|
|
101769
|
+
return null;
|
|
101770
|
+
}
|
|
101771
|
+
|
|
101772
|
+
class SecurityStore {
|
|
101773
|
+
storage;
|
|
101774
|
+
fetcher;
|
|
101775
|
+
constructor(storage, fetcher = osvFetcher) {
|
|
101776
|
+
this.storage = storage;
|
|
101777
|
+
this.fetcher = fetcher;
|
|
101778
|
+
}
|
|
101779
|
+
key(org) {
|
|
101780
|
+
return `security/${orgKey(org)}/watch.json`;
|
|
101781
|
+
}
|
|
101782
|
+
async getWatchList(org) {
|
|
101783
|
+
try {
|
|
101784
|
+
const bytes = await this.storage.download(this.key(org));
|
|
101785
|
+
const parsed = JSON.parse(new TextDecoder().decode(bytes));
|
|
101786
|
+
if (parsed && Array.isArray(parsed.entries))
|
|
101787
|
+
return parsed;
|
|
101788
|
+
} catch {}
|
|
101789
|
+
return { org, entries: [], updatedAt: new Date().toISOString() };
|
|
101790
|
+
}
|
|
101791
|
+
async setWatchList(org, entries, policy) {
|
|
101792
|
+
const existing = await this.getWatchList(org);
|
|
101793
|
+
const list = {
|
|
101794
|
+
org,
|
|
101795
|
+
entries,
|
|
101796
|
+
policy: policy ?? existing.policy,
|
|
101797
|
+
updatedAt: new Date().toISOString()
|
|
101798
|
+
};
|
|
101799
|
+
const body = new TextEncoder().encode(JSON.stringify(list)).buffer;
|
|
101800
|
+
await this.storage.upload(this.key(org), body);
|
|
101801
|
+
return list;
|
|
101802
|
+
}
|
|
101803
|
+
async setPolicy(org, policy) {
|
|
101804
|
+
const existing = await this.getWatchList(org);
|
|
101805
|
+
return this.setWatchList(org, existing.entries, policy);
|
|
101806
|
+
}
|
|
101807
|
+
async report(org) {
|
|
101808
|
+
const list = await this.getWatchList(org);
|
|
101809
|
+
const alerts = [];
|
|
101810
|
+
let degraded;
|
|
101811
|
+
try {
|
|
101812
|
+
const vulns = await this.fetcher(list.entries);
|
|
101813
|
+
for (const found of vulns.values())
|
|
101814
|
+
alerts.push(...found);
|
|
101815
|
+
} catch (err) {
|
|
101816
|
+
degraded = `advisory lookup failed: ${err.message}`;
|
|
101817
|
+
}
|
|
101818
|
+
for (const entry of list.entries) {
|
|
101819
|
+
const licenseAlert = checkLicense(entry, list.policy);
|
|
101820
|
+
if (licenseAlert)
|
|
101821
|
+
alerts.push(licenseAlert);
|
|
101822
|
+
}
|
|
101823
|
+
alerts.sort((a11, b11) => {
|
|
101824
|
+
const sa3 = a11.type === "vulnerability" ? SEVERITY_ORDER[a11.severity] : 5;
|
|
101825
|
+
const sb3 = b11.type === "vulnerability" ? SEVERITY_ORDER[b11.severity] : 5;
|
|
101826
|
+
return sa3 - sb3 || a11.package.localeCompare(b11.package);
|
|
101827
|
+
});
|
|
101828
|
+
const counts = { critical: 0, high: 0, moderate: 0, low: 0, license: 0 };
|
|
101829
|
+
for (const alert3 of alerts) {
|
|
101830
|
+
if (alert3.type === "license")
|
|
101831
|
+
counts.license++;
|
|
101832
|
+
else if (alert3.severity !== "unknown")
|
|
101833
|
+
counts[alert3.severity]++;
|
|
101834
|
+
}
|
|
101835
|
+
return {
|
|
101836
|
+
org,
|
|
101837
|
+
generatedAt: new Date().toISOString(),
|
|
101838
|
+
watched: list.entries.length,
|
|
101839
|
+
alerts,
|
|
101840
|
+
counts,
|
|
101841
|
+
...degraded ? { degraded } : {}
|
|
101842
|
+
};
|
|
101843
|
+
}
|
|
101844
|
+
}
|
|
101845
|
+
function normalizePolicy(value) {
|
|
101846
|
+
if (!value || typeof value !== "object")
|
|
101847
|
+
return;
|
|
101848
|
+
const raw = value;
|
|
101849
|
+
const strings = (v11) => Array.isArray(v11) ? v11.filter((x11) => typeof x11 === "string" && x11.length < 100).slice(0, 200) : undefined;
|
|
101850
|
+
const allow = strings(raw.allow);
|
|
101851
|
+
const deny = strings(raw.deny);
|
|
101852
|
+
if (!allow && !deny)
|
|
101853
|
+
return;
|
|
101854
|
+
return { ...allow ? { allow } : {}, ...deny ? { deny } : {} };
|
|
101855
|
+
}
|
|
101856
|
+
|
|
101857
|
+
// src/sbom.ts
|
|
101858
|
+
function purlFor(entry) {
|
|
101859
|
+
const type = (entry.ecosystem || "npm").toLowerCase();
|
|
101860
|
+
const [scope, bare] = entry.name.startsWith("@") ? [entry.name.slice(0, entry.name.indexOf("/")), entry.name.slice(entry.name.indexOf("/") + 1)] : [null, entry.name];
|
|
101861
|
+
const namespace = scope ? `${encodeURIComponent(scope)}/` : "";
|
|
101862
|
+
return `pkg:${type}/${namespace}${encodeURIComponent(bare)}@${encodeURIComponent(entry.version)}`;
|
|
101863
|
+
}
|
|
101864
|
+
function hashesFor(entry) {
|
|
101865
|
+
const hashes = [];
|
|
101866
|
+
if (entry.sha256)
|
|
101867
|
+
hashes.push({ alg: "SHA-256", content: entry.sha256 });
|
|
101868
|
+
if (entry.integrity?.startsWith("sha512-")) {
|
|
101869
|
+
try {
|
|
101870
|
+
const hex = Array.from(Buffer.from(entry.integrity.slice(7), "base64")).map((b11) => b11.toString(16).padStart(2, "0")).join("");
|
|
101871
|
+
hashes.push({ alg: "SHA-512", content: hex });
|
|
101872
|
+
} catch {}
|
|
101873
|
+
}
|
|
101874
|
+
return hashes;
|
|
101875
|
+
}
|
|
101876
|
+
function toCycloneDx(entries, options) {
|
|
101877
|
+
const timestamp = options.timestamp || new Date().toISOString();
|
|
101878
|
+
return {
|
|
101879
|
+
bomFormat: "CycloneDX",
|
|
101880
|
+
specVersion: "1.5",
|
|
101881
|
+
serialNumber: `urn:uuid:${options.documentId || deterministicUuid(options.org, timestamp)}`,
|
|
101882
|
+
version: 1,
|
|
101883
|
+
metadata: {
|
|
101884
|
+
timestamp,
|
|
101885
|
+
tools: [{ vendor: "pantry", name: "pantry-registry", version: "1" }],
|
|
101886
|
+
component: {
|
|
101887
|
+
type: "application",
|
|
101888
|
+
name: options.subject || options.org,
|
|
101889
|
+
version: timestamp.slice(0, 10)
|
|
101890
|
+
}
|
|
101891
|
+
},
|
|
101892
|
+
components: entries.map((entry) => ({
|
|
101893
|
+
type: "library",
|
|
101894
|
+
name: entry.name,
|
|
101895
|
+
version: entry.version,
|
|
101896
|
+
purl: purlFor(entry),
|
|
101897
|
+
...entry.license ? { licenses: [{ license: { id: entry.license } }] } : {},
|
|
101898
|
+
...hashesFor(entry).length ? { hashes: hashesFor(entry) } : {},
|
|
101899
|
+
...entry.resolved ? { externalReferences: [{ type: "distribution", url: entry.resolved }] } : {},
|
|
101900
|
+
properties: [
|
|
101901
|
+
{ name: "pantry:mirrored", value: entry.key ? "true" : "false" },
|
|
101902
|
+
...entry.mirroredAt ? [{ name: "pantry:mirroredAt", value: entry.mirroredAt }] : []
|
|
101903
|
+
]
|
|
101904
|
+
}))
|
|
101905
|
+
};
|
|
101906
|
+
}
|
|
101907
|
+
function toSpdx(entries, options) {
|
|
101908
|
+
const timestamp = options.timestamp || new Date().toISOString();
|
|
101909
|
+
const name = options.subject || options.org;
|
|
101910
|
+
const packages = entries.map((entry, i9) => ({
|
|
101911
|
+
SPDXID: `SPDXRef-Package-${i9}`,
|
|
101912
|
+
name: entry.name,
|
|
101913
|
+
versionInfo: entry.version,
|
|
101914
|
+
downloadLocation: entry.resolved || "NOASSERTION",
|
|
101915
|
+
filesAnalyzed: false,
|
|
101916
|
+
licenseConcluded: entry.license || "NOASSERTION",
|
|
101917
|
+
licenseDeclared: entry.license || "NOASSERTION",
|
|
101918
|
+
copyrightText: "NOASSERTION",
|
|
101919
|
+
externalRefs: [{
|
|
101920
|
+
referenceCategory: "PACKAGE-MANAGER",
|
|
101921
|
+
referenceType: "purl",
|
|
101922
|
+
referenceLocator: purlFor(entry)
|
|
101923
|
+
}],
|
|
101924
|
+
...entry.sha256 ? { checksums: [{ algorithm: "SHA256", checksumValue: entry.sha256 }] } : {}
|
|
101925
|
+
}));
|
|
101926
|
+
return {
|
|
101927
|
+
spdxVersion: "SPDX-2.3",
|
|
101928
|
+
dataLicense: "CC0-1.0",
|
|
101929
|
+
SPDXID: "SPDXRef-DOCUMENT",
|
|
101930
|
+
name,
|
|
101931
|
+
documentNamespace: `https://pantry.dev/sbom/${encodeURIComponent(options.org)}/${options.documentId || deterministicUuid(options.org, timestamp)}`,
|
|
101932
|
+
creationInfo: {
|
|
101933
|
+
created: timestamp,
|
|
101934
|
+
creators: ["Tool: pantry-registry"]
|
|
101935
|
+
},
|
|
101936
|
+
packages,
|
|
101937
|
+
relationships: packages.map((pkg) => ({
|
|
101938
|
+
spdxElementId: "SPDXRef-DOCUMENT",
|
|
101939
|
+
relatedSpdxElement: pkg.SPDXID,
|
|
101940
|
+
relationshipType: "DESCRIBES"
|
|
101941
|
+
}))
|
|
101942
|
+
};
|
|
101943
|
+
}
|
|
101944
|
+
function buildSbom(entries, format, options) {
|
|
101945
|
+
return format === "spdx" ? toSpdx(entries, options) : toCycloneDx(entries, options);
|
|
101946
|
+
}
|
|
101947
|
+
function parseFormat(value) {
|
|
101948
|
+
return String(value || "").toLowerCase() === "spdx" ? "spdx" : "cyclonedx";
|
|
101949
|
+
}
|
|
101950
|
+
function deterministicUuid(org, timestamp) {
|
|
101951
|
+
const seed = `${org}:${timestamp}`;
|
|
101952
|
+
let h15 = 2166136261;
|
|
101953
|
+
let h23 = 16777619;
|
|
101954
|
+
for (let i9 = 0;i9 < seed.length; i9++) {
|
|
101955
|
+
h15 = Math.imul(h15 ^ seed.charCodeAt(i9), 16777619) >>> 0;
|
|
101956
|
+
h23 = Math.imul(h23 + seed.charCodeAt(i9) + i9, 2246822507) >>> 0;
|
|
101957
|
+
}
|
|
101958
|
+
const hex = (n10) => n10.toString(16).padStart(8, "0");
|
|
101959
|
+
const a11 = hex(h15);
|
|
101960
|
+
const b11 = hex(h23);
|
|
101961
|
+
const c10 = hex((h15 ^ h23) >>> 0);
|
|
101962
|
+
const d10 = hex(h15 + h23 >>> 0);
|
|
101963
|
+
return `${a11}-${b11.slice(0, 4)}-4${b11.slice(5, 8)}-a${c10.slice(1, 4)}-${c10.slice(4)}${d10}`;
|
|
101964
|
+
}
|
|
101965
|
+
|
|
100707
101966
|
// src/server.ts
|
|
100708
101967
|
var _aliases = new Map;
|
|
100709
101968
|
function loadAliases() {
|
|
@@ -101132,6 +102391,17 @@ function createHandler(registry, analyticsStorage, zigPackageStorage, baseUrl, b
|
|
|
101132
102391
|
return new Response(null, { headers: corsHeaders });
|
|
101133
102392
|
}
|
|
101134
102393
|
try {
|
|
102394
|
+
if (path6 === "/api/registry-info" && req.method === "GET") {
|
|
102395
|
+
return Response.json(registryInfo(process.env, baseUrl), {
|
|
102396
|
+
headers: { ...corsHeaders, "Cache-Control": "no-store" }
|
|
102397
|
+
});
|
|
102398
|
+
}
|
|
102399
|
+
const denied = await enforceReadAccess(req, url, { identify: identifyReader, corsHeaders });
|
|
102400
|
+
if (denied)
|
|
102401
|
+
return denied;
|
|
102402
|
+
const fromPlugin = await pluginResponse(req, { url, path: path6, method: req.method, visibility: resolveVisibility() });
|
|
102403
|
+
if (fromPlugin)
|
|
102404
|
+
return fromPlugin;
|
|
101135
102405
|
const ua3 = req.headers.get("user-agent") || "";
|
|
101136
102406
|
const isCLI = /^(curl|wget|httpie|fetch|libfetch|powershell)/i.test(ua3) || !ua3;
|
|
101137
102407
|
if (isCLI && (path6 === "/" || path6 === "")) {
|
|
@@ -101241,8 +102511,11 @@ function createHandler(registry, analyticsStorage, zigPackageStorage, baseUrl, b
|
|
|
101241
102511
|
const domain = typeof body?.domain === "string" ? body.domain : "";
|
|
101242
102512
|
if (!/^[a-zA-Z0-9._/-]{1,128}$/.test(domain))
|
|
101243
102513
|
return Response.json({ error: "valid domain required" }, { status: 400, headers: corsHeaders });
|
|
101244
|
-
const
|
|
101245
|
-
|
|
102514
|
+
const identity = await identifyReader(req);
|
|
102515
|
+
const tier = await tierForUser(identity.userId);
|
|
102516
|
+
const priority = identity.userId === "_admin" || tierDefinition(tier).priorityBuilds;
|
|
102517
|
+
const queued = getBuildStatus().requestRebuild(domain, priority);
|
|
102518
|
+
return Response.json({ queued, domain, priority }, { headers: corsHeaders });
|
|
101246
102519
|
}
|
|
101247
102520
|
if (path6 === "/api/rebuild-queue" && req.method === "GET") {
|
|
101248
102521
|
return Response.json({ queue: getBuildStatus().getQueue() }, { headers: corsHeaders });
|
|
@@ -101273,6 +102546,52 @@ function createHandler(registry, analyticsStorage, zigPackageStorage, baseUrl, b
|
|
|
101273
102546
|
if (authResponse)
|
|
101274
102547
|
return authResponse;
|
|
101275
102548
|
}
|
|
102549
|
+
if (authService) {
|
|
102550
|
+
const enterprise = await handleEnterpriseRoutes(path6, req, registry, corsHeaders);
|
|
102551
|
+
if (enterprise)
|
|
102552
|
+
return enterprise;
|
|
102553
|
+
}
|
|
102554
|
+
if (authService && (path6 === "/api/plans" || path6.startsWith("/account/"))) {
|
|
102555
|
+
const subscriptionResponse = await handleSubscriptionRoutes(path6, req, authService, baseUrl, corsHeaders);
|
|
102556
|
+
if (subscriptionResponse)
|
|
102557
|
+
return subscriptionResponse;
|
|
102558
|
+
}
|
|
102559
|
+
if (path6.startsWith("/admin/") && authService) {
|
|
102560
|
+
const adminResponse = await handleAdminRoutes(path6, req, authService, corsHeaders);
|
|
102561
|
+
if (adminResponse)
|
|
102562
|
+
return adminResponse;
|
|
102563
|
+
}
|
|
102564
|
+
if (path6 === "/pricing" && req.method === "GET") {
|
|
102565
|
+
const html = await renderSitePage("pricing.stx", {
|
|
102566
|
+
title: "Plans",
|
|
102567
|
+
metaDescription: "Publishing and installing on pantry is free. Selling a package costs a fee per sale \u2014 10% on Free, 5% on a plan \u2014 and a plan also insures your builds, watches your lockfile and unlocks private packages.",
|
|
102568
|
+
canonicalUrl: "https://pantry.dev/pricing",
|
|
102569
|
+
plans: Object.values(TIERS).map((t11) => ({
|
|
102570
|
+
id: t11.id,
|
|
102571
|
+
name: t11.name,
|
|
102572
|
+
featured: t11.id === "pro",
|
|
102573
|
+
formattedPrice: t11.price === 0 ? "Free" : `$${(t11.price / 100).toFixed(0)}/mo`,
|
|
102574
|
+
sellingFee: formatBps(t11.commissionBps),
|
|
102575
|
+
tagline: t11.id === "free" ? "Publish and sell, no cost" : t11.id === "pro" ? "For people who ship" : "For teams who depend on it",
|
|
102576
|
+
publishing: [
|
|
102577
|
+
t11.privatePackages ? "Private & unlisted packages" : "Public packages",
|
|
102578
|
+
t11.analyticsRetentionDays >= 3650 ? "Lifetime full analytics" : "30 days of full analytics",
|
|
102579
|
+
`${Math.round(t11.maxArtifactBytes / 1048576)}MB artifacts`,
|
|
102580
|
+
t11.priorityBuilds ? "Priority builds" : "Standard build queue",
|
|
102581
|
+
t11.seats > 1 ? `${t11.seats} seats, shared packages` : "1 seat"
|
|
102582
|
+
],
|
|
102583
|
+
consuming: [
|
|
102584
|
+
t11.buildInsurance ? "Build insurance \u2014 every artifact mirrored" : "Standard downloads",
|
|
102585
|
+
t11.securityAlerts ? "Continuous CVE & licence alerts" : "Point-in-time `pantry audit`",
|
|
102586
|
+
t11.sbomExport ? "SBOM export (CycloneDX, SPDX)" : "No SBOM export",
|
|
102587
|
+
t11.teamEntitlements ? "Paid packages bought once for the whole team" : "Purchases are per account"
|
|
102588
|
+
]
|
|
102589
|
+
})),
|
|
102590
|
+
discoveryFee: formatBps(DISCOVERY_FEE_BPS),
|
|
102591
|
+
paymentsEnabled: paymentsEnabled()
|
|
102592
|
+
});
|
|
102593
|
+
return htmlResponse(html);
|
|
102594
|
+
}
|
|
101276
102595
|
if (authService && (path6 === "/login" || path6 === "/signup" || path6 === "/account")) {
|
|
101277
102596
|
return handleSiteAuth(path6, req, authService, corsHeaders);
|
|
101278
102597
|
}
|
|
@@ -101366,7 +102685,15 @@ function createHandler(registry, analyticsStorage, zigPackageStorage, baseUrl, b
|
|
|
101366
102685
|
}
|
|
101367
102686
|
try {
|
|
101368
102687
|
const rawBody = await req.text();
|
|
101369
|
-
const result = await handleStripeWebhook(registry.metadata, rawBody, signature)
|
|
102688
|
+
const result = await handleStripeWebhook(registry.metadata, rawBody, signature, authService ? async (change) => {
|
|
102689
|
+
await authService.setSubscription(change.email, {
|
|
102690
|
+
tier: tierOf(change.tier),
|
|
102691
|
+
status: change.status,
|
|
102692
|
+
stripeCustomerId: change.stripeCustomerId,
|
|
102693
|
+
stripeSubscriptionId: change.stripeSubscriptionId,
|
|
102694
|
+
currentPeriodEnd: change.currentPeriodEnd
|
|
102695
|
+
});
|
|
102696
|
+
} : undefined);
|
|
101370
102697
|
return Response.json(result, { headers: corsHeaders });
|
|
101371
102698
|
} catch (err) {
|
|
101372
102699
|
console.error("Stripe webhook error:", err);
|
|
@@ -101542,61 +102869,111 @@ function createHandler(registry, analyticsStorage, zigPackageStorage, baseUrl, b
|
|
|
101542
102869
|
if (!paywall || !paywall.enabled) {
|
|
101543
102870
|
return Response.json({ enabled: false }, { headers: corsHeaders });
|
|
101544
102871
|
}
|
|
102872
|
+
const identity = await identifyReader(req);
|
|
102873
|
+
const owned = identity.userId && identity.userId !== "_admin" ? await isEntitled(registry.metadata, packageName, identity.userId) : false;
|
|
101545
102874
|
return Response.json({
|
|
101546
102875
|
enabled: true,
|
|
101547
102876
|
price: paywall.price,
|
|
101548
102877
|
currency: paywall.currency,
|
|
101549
102878
|
formattedPrice: formatPrice(paywall.price, paywall.currency),
|
|
101550
|
-
freeVersions: paywall.freeVersions || []
|
|
101551
|
-
|
|
102879
|
+
freeVersions: paywall.freeVersions || [],
|
|
102880
|
+
owned,
|
|
102881
|
+
paymentsEnabled: paymentsEnabled(),
|
|
102882
|
+
buyUrl: `${baseUrl}/packages/${encodeURIComponent(packageName)}/buy`
|
|
102883
|
+
}, { headers: { ...corsHeaders, "Cache-Control": "no-store" } });
|
|
101552
102884
|
}
|
|
101553
102885
|
if (rest === "paywall" && req.method === "POST") {
|
|
101554
|
-
const
|
|
101555
|
-
if (
|
|
101556
|
-
return
|
|
101557
|
-
}
|
|
102886
|
+
const denied2 = await requirePackageOwner(req, registry, packageName, corsHeaders);
|
|
102887
|
+
if (denied2)
|
|
102888
|
+
return denied2;
|
|
101558
102889
|
let body;
|
|
101559
102890
|
try {
|
|
101560
102891
|
body = await req.json();
|
|
101561
102892
|
} catch {
|
|
101562
102893
|
return Response.json({ error: "Invalid JSON body" }, { status: 400, headers: corsHeaders });
|
|
101563
102894
|
}
|
|
101564
|
-
|
|
101565
|
-
|
|
102895
|
+
const invalid = validatePriceConfig(body);
|
|
102896
|
+
if (invalid) {
|
|
102897
|
+
return Response.json({ error: invalid }, { status: 400, headers: corsHeaders });
|
|
102898
|
+
}
|
|
102899
|
+
try {
|
|
102900
|
+
const paywall = await configurePaywall(registry.metadata, packageName, body);
|
|
102901
|
+
return Response.json({
|
|
102902
|
+
success: true,
|
|
102903
|
+
paywall: {
|
|
102904
|
+
enabled: paywall.enabled,
|
|
102905
|
+
price: paywall.price,
|
|
102906
|
+
currency: paywall.currency,
|
|
102907
|
+
formattedPrice: formatPrice(paywall.price, paywall.currency),
|
|
102908
|
+
freeVersions: paywall.freeVersions || [],
|
|
102909
|
+
payoutAccountId: paywall.stripeAccountId,
|
|
102910
|
+
paymentsEnabled: paymentsEnabled()
|
|
102911
|
+
}
|
|
102912
|
+
}, { status: 200, headers: corsHeaders });
|
|
102913
|
+
} catch (err) {
|
|
102914
|
+
return Response.json({ error: err.message || "Could not set the price" }, { status: 400, headers: corsHeaders });
|
|
101566
102915
|
}
|
|
101567
|
-
const paywall = await configurePaywall(registry.metadata, packageName, body);
|
|
101568
|
-
return Response.json({
|
|
101569
|
-
success: true,
|
|
101570
|
-
paywall: {
|
|
101571
|
-
enabled: paywall.enabled,
|
|
101572
|
-
price: paywall.price,
|
|
101573
|
-
currency: paywall.currency,
|
|
101574
|
-
formattedPrice: formatPrice(paywall.price, paywall.currency)
|
|
101575
|
-
}
|
|
101576
|
-
}, { status: 200, headers: corsHeaders });
|
|
101577
102916
|
}
|
|
101578
102917
|
if (rest === "paywall" && req.method === "DELETE") {
|
|
101579
|
-
const
|
|
101580
|
-
if (
|
|
101581
|
-
return
|
|
101582
|
-
}
|
|
102918
|
+
const denied2 = await requirePackageOwner(req, registry, packageName, corsHeaders);
|
|
102919
|
+
if (denied2)
|
|
102920
|
+
return denied2;
|
|
101583
102921
|
await registry.metadata.deletePaywall(packageName);
|
|
101584
102922
|
return Response.json({ success: true }, { headers: corsHeaders });
|
|
101585
102923
|
}
|
|
101586
|
-
if (rest === "checkout" && req.method === "
|
|
101587
|
-
const
|
|
101588
|
-
if (!
|
|
101589
|
-
return Response.json({
|
|
102924
|
+
if (rest === "checkout" && req.method === "POST") {
|
|
102925
|
+
const identity = await identifyReader(req);
|
|
102926
|
+
if (!identity.authenticated || !identity.userId || identity.userId === "_admin") {
|
|
102927
|
+
return Response.json({
|
|
102928
|
+
error: "Sign in to buy a package",
|
|
102929
|
+
hint: "Create an account at /signup, then: pantry token set"
|
|
102930
|
+
}, { status: 401, headers: corsHeaders });
|
|
102931
|
+
}
|
|
102932
|
+
if (await isEntitled(registry.metadata, packageName, identity.userId)) {
|
|
102933
|
+
return Response.json({ owned: true, message: "You already own this package" }, { headers: corsHeaders });
|
|
101590
102934
|
}
|
|
101591
102935
|
try {
|
|
101592
|
-
const session = await createCheckoutSession(registry.metadata,
|
|
102936
|
+
const session = await createCheckoutSession(registry.metadata, {
|
|
102937
|
+
packageName,
|
|
102938
|
+
email: identity.userId,
|
|
102939
|
+
baseUrl,
|
|
102940
|
+
sellerTier: await sellerTierFor(registry, authService, packageName),
|
|
102941
|
+
origin: url.searchParams.get("origin") === "site" ? "site" : "cli"
|
|
102942
|
+
});
|
|
102943
|
+
return Response.json({ url: session.url }, { headers: corsHeaders });
|
|
102944
|
+
} catch (err) {
|
|
102945
|
+
console.error("Checkout session error:", err);
|
|
102946
|
+
return Response.json({ error: err.message || "Could not start checkout" }, { status: 400, headers: corsHeaders });
|
|
102947
|
+
}
|
|
102948
|
+
}
|
|
102949
|
+
if ((rest === "buy" || rest === "checkout") && req.method === "GET") {
|
|
102950
|
+
const sessionToken = extractSessionToken(req);
|
|
102951
|
+
const user = sessionToken && authService ? await authService.validateSession(sessionToken) : null;
|
|
102952
|
+
if (!user) {
|
|
102953
|
+
const next = `/packages/${encodeURIComponent(packageName)}/buy`;
|
|
102954
|
+
return new Response(null, {
|
|
102955
|
+
status: 302,
|
|
102956
|
+
headers: { ...corsHeaders, Location: `/login?next=${encodeURIComponent(next)}` }
|
|
102957
|
+
});
|
|
102958
|
+
}
|
|
102959
|
+
if (await isEntitled(registry.metadata, packageName, user.email)) {
|
|
101593
102960
|
return new Response(null, {
|
|
101594
102961
|
status: 302,
|
|
101595
|
-
headers: { ...corsHeaders, Location:
|
|
102962
|
+
headers: { ...corsHeaders, Location: `/packages/${encodeURIComponent(packageName)}/checkout/success` }
|
|
101596
102963
|
});
|
|
102964
|
+
}
|
|
102965
|
+
try {
|
|
102966
|
+
const session = await createCheckoutSession(registry.metadata, {
|
|
102967
|
+
packageName,
|
|
102968
|
+
email: user.email,
|
|
102969
|
+
baseUrl,
|
|
102970
|
+
sellerTier: await sellerTierFor(registry, authService, packageName),
|
|
102971
|
+
origin: "site"
|
|
102972
|
+
});
|
|
102973
|
+
return new Response(null, { status: 302, headers: { ...corsHeaders, Location: session.url } });
|
|
101597
102974
|
} catch (err) {
|
|
101598
102975
|
console.error("Checkout session error:", err);
|
|
101599
|
-
return Response.json({ error: "
|
|
102976
|
+
return Response.json({ error: err.message || "Could not start checkout" }, { status: 400, headers: corsHeaders });
|
|
101600
102977
|
}
|
|
101601
102978
|
}
|
|
101602
102979
|
if (rest === "checkout/success" && req.method === "GET") {
|
|
@@ -101615,19 +102992,20 @@ function createHandler(registry, analyticsStorage, zigPackageStorage, baseUrl, b
|
|
|
101615
102992
|
if (!version3 || !/^[a-zA-Z0-9._+-]+$/.test(version3) || version3.length > 64) {
|
|
101616
102993
|
return Response.json({ error: "Invalid version" }, { status: 400, headers: corsHeaders });
|
|
101617
102994
|
}
|
|
101618
|
-
const
|
|
101619
|
-
const access4 = await checkPaywallAccess(registry.metadata, packageName, version3, authToken);
|
|
102995
|
+
const access4 = await resolvePackageAccess(req, registry, packageName, version3);
|
|
101620
102996
|
if (!access4.allowed && access4.paywall) {
|
|
101621
|
-
const
|
|
102997
|
+
const buyUrl = `${baseUrl}/packages/${encodeURIComponent(packageName)}/buy`;
|
|
102998
|
+
const priceText = formatPrice(access4.paywall.price, access4.paywall.currency);
|
|
101622
102999
|
return Response.json({
|
|
101623
103000
|
error: "Payment required",
|
|
101624
103001
|
package: packageName,
|
|
101625
103002
|
price: access4.paywall.price,
|
|
101626
103003
|
currency: access4.paywall.currency,
|
|
101627
|
-
formattedPrice:
|
|
101628
|
-
|
|
101629
|
-
|
|
101630
|
-
|
|
103004
|
+
formattedPrice: priceText,
|
|
103005
|
+
buyUrl,
|
|
103006
|
+
checkoutUrl: buyUrl,
|
|
103007
|
+
message: access4.reason === "unauthenticated" ? `${packageName} costs ${priceText}. Sign in and buy it with: pantry buy ${packageName}` : `${packageName} costs ${priceText}. Buy it with: pantry buy ${packageName} (or open ${buyUrl})`
|
|
103008
|
+
}, { status: 402, headers: { ...corsHeaders, "Cache-Control": "no-store" } });
|
|
101631
103009
|
}
|
|
101632
103010
|
const tarball = await registry.downloadTarball(packageName, version3);
|
|
101633
103011
|
if (!tarball) {
|
|
@@ -101767,9 +103145,14 @@ function createServer(registry, port = 3000, analytics, zigStorage, binaryStorag
|
|
|
101767
103145
|
const start = () => {
|
|
101768
103146
|
server = Bun.serve({
|
|
101769
103147
|
port,
|
|
101770
|
-
fetch: handler
|
|
103148
|
+
fetch: handler,
|
|
103149
|
+
maxRequestBodySize: Math.max(...Object.values(TIERS).map((t11) => t11.maxArtifactBytes)) + 33554432
|
|
101771
103150
|
});
|
|
103151
|
+
const visibility = resolveVisibility();
|
|
101772
103152
|
console.log(`Pantry Registry running at http://localhost:${port}`);
|
|
103153
|
+
console.log(visibility === "private" ? " Visibility: PRIVATE \u2014 every read requires a token or a logged-in session" : " Visibility: public \u2014 reads are unauthenticated (set REGISTRY_VISIBILITY=private to close it)");
|
|
103154
|
+
if (visibility === "private" && signupsEnabled())
|
|
103155
|
+
console.warn(" WARNING: REGISTRY_ALLOW_SIGNUP is on \u2014 anyone can create an account and read every package");
|
|
101773
103156
|
console.log("Endpoints:");
|
|
101774
103157
|
console.log(" GET /packages/{name} - Get package metadata");
|
|
101775
103158
|
console.log(" GET /packages/{name}/{version} - Get specific version");
|
|
@@ -101954,6 +103337,440 @@ async function isAuthorizedRequest(req) {
|
|
|
101954
103337
|
} catch {}
|
|
101955
103338
|
return false;
|
|
101956
103339
|
}
|
|
103340
|
+
async function identifyReader(req) {
|
|
103341
|
+
try {
|
|
103342
|
+
const authHeader = req.headers.get("authorization");
|
|
103343
|
+
const token = extractBearerToken(authHeader);
|
|
103344
|
+
if (token && _authService) {
|
|
103345
|
+
const result = await _authService.validateAccessToken(token, getRegistryToken3() ?? "", "read");
|
|
103346
|
+
if (result.valid)
|
|
103347
|
+
return { authenticated: true, userId: result.userId ?? null };
|
|
103348
|
+
} else if (token) {
|
|
103349
|
+
const result = await validateToken3(authHeader);
|
|
103350
|
+
if (result.valid)
|
|
103351
|
+
return { authenticated: true, userId: result.userId ?? null };
|
|
103352
|
+
}
|
|
103353
|
+
if (_authService) {
|
|
103354
|
+
const sessionToken = extractSessionToken(req);
|
|
103355
|
+
if (sessionToken) {
|
|
103356
|
+
const user = await _authService.validateSession(sessionToken);
|
|
103357
|
+
if (user)
|
|
103358
|
+
return { authenticated: true, userId: user.email };
|
|
103359
|
+
}
|
|
103360
|
+
}
|
|
103361
|
+
} catch {}
|
|
103362
|
+
return { authenticated: false, userId: null };
|
|
103363
|
+
}
|
|
103364
|
+
async function isAdminRequest(req) {
|
|
103365
|
+
try {
|
|
103366
|
+
const authHeader = req.headers.get("authorization");
|
|
103367
|
+
const token = extractBearerToken(authHeader);
|
|
103368
|
+
if (token) {
|
|
103369
|
+
const registryToken = getRegistryToken3();
|
|
103370
|
+
if (registryToken && token.length === registryToken.length) {
|
|
103371
|
+
const maxLen = Math.max(token.length, registryToken.length);
|
|
103372
|
+
const a11 = Buffer.alloc(maxLen);
|
|
103373
|
+
const b11 = Buffer.alloc(maxLen);
|
|
103374
|
+
Buffer.from(token).copy(a11);
|
|
103375
|
+
Buffer.from(registryToken).copy(b11);
|
|
103376
|
+
if (__require("crypto").timingSafeEqual(a11, b11))
|
|
103377
|
+
return true;
|
|
103378
|
+
}
|
|
103379
|
+
}
|
|
103380
|
+
if (_authService) {
|
|
103381
|
+
const sessionToken = extractSessionToken(req);
|
|
103382
|
+
if (sessionToken) {
|
|
103383
|
+
const user = await _authService.validateSession(sessionToken);
|
|
103384
|
+
if (user?.role === "admin")
|
|
103385
|
+
return true;
|
|
103386
|
+
}
|
|
103387
|
+
}
|
|
103388
|
+
} catch {}
|
|
103389
|
+
return false;
|
|
103390
|
+
}
|
|
103391
|
+
async function requirePackageOwner(req, registry, packageName, corsHeaders) {
|
|
103392
|
+
const identity = await identifyReader(req);
|
|
103393
|
+
if (!identity.authenticated) {
|
|
103394
|
+
return Response.json({ error: "Authentication required", hint: "pantry token set --registry <registry-url>" }, { status: 401, headers: corsHeaders });
|
|
103395
|
+
}
|
|
103396
|
+
if (identity.userId === "_admin")
|
|
103397
|
+
return null;
|
|
103398
|
+
const record = await registry.getPublisherPackageRecord(packageName);
|
|
103399
|
+
if (!record) {
|
|
103400
|
+
return Response.json({ error: "Package not found" }, { status: 404, headers: corsHeaders });
|
|
103401
|
+
}
|
|
103402
|
+
if (record.publishedBy && record.publishedBy !== identity.userId) {
|
|
103403
|
+
if (_authService && await _authService.canActFor(identity.userId, record.publishedBy))
|
|
103404
|
+
return null;
|
|
103405
|
+
return Response.json({ error: "Only the publisher of this package can change its price" }, { status: 403, headers: corsHeaders });
|
|
103406
|
+
}
|
|
103407
|
+
return null;
|
|
103408
|
+
}
|
|
103409
|
+
async function requirePublishRights(registry, userId, packageName, corsHeaders) {
|
|
103410
|
+
if (!userId || userId === "_admin")
|
|
103411
|
+
return null;
|
|
103412
|
+
const record = await registry.getPublisherPackageRecord(packageName).catch(() => null);
|
|
103413
|
+
const owner = record?.publishedBy;
|
|
103414
|
+
if (!owner)
|
|
103415
|
+
return null;
|
|
103416
|
+
if (owner === userId)
|
|
103417
|
+
return null;
|
|
103418
|
+
if (_authService && await _authService.canActFor(userId, owner))
|
|
103419
|
+
return null;
|
|
103420
|
+
return Response.json({
|
|
103421
|
+
error: `${packageName} belongs to another account`,
|
|
103422
|
+
hint: "Ask its owner to add you to their team, or publish under a name you own"
|
|
103423
|
+
}, { status: 403, headers: corsHeaders });
|
|
103424
|
+
}
|
|
103425
|
+
async function tierForPackage(registry, packageName, fallbackUserId) {
|
|
103426
|
+
const record = await registry.getPublisherPackageRecord(packageName).catch(() => null);
|
|
103427
|
+
if (record?.publishedBy)
|
|
103428
|
+
return tierForUser(record.publishedBy);
|
|
103429
|
+
return tierForUser(fallbackUserId);
|
|
103430
|
+
}
|
|
103431
|
+
async function tierForUser(userId) {
|
|
103432
|
+
if (!userId || userId === "_admin" || !_authService)
|
|
103433
|
+
return "free";
|
|
103434
|
+
try {
|
|
103435
|
+
return await _authService.getTier(userId);
|
|
103436
|
+
} catch {
|
|
103437
|
+
return "free";
|
|
103438
|
+
}
|
|
103439
|
+
}
|
|
103440
|
+
async function sellerTierFor(registry, auth, packageName) {
|
|
103441
|
+
if (!auth)
|
|
103442
|
+
return "free";
|
|
103443
|
+
try {
|
|
103444
|
+
const record = await registry.getPublisherPackageRecord(packageName);
|
|
103445
|
+
if (!record?.publishedBy)
|
|
103446
|
+
return "free";
|
|
103447
|
+
return await auth.getTier(record.publishedBy);
|
|
103448
|
+
} catch {
|
|
103449
|
+
return "free";
|
|
103450
|
+
}
|
|
103451
|
+
}
|
|
103452
|
+
async function handleSubscriptionRoutes(path6, req, auth, baseUrl, corsHeaders) {
|
|
103453
|
+
if (path6 === "/api/plans" && req.method === "GET") {
|
|
103454
|
+
return Response.json({
|
|
103455
|
+
plans: Object.values(TIERS).map((t11) => ({
|
|
103456
|
+
id: t11.id,
|
|
103457
|
+
name: t11.name,
|
|
103458
|
+
price: t11.price,
|
|
103459
|
+
formattedPrice: t11.price === 0 ? "Free" : `$${(t11.price / 100).toFixed(0)}/mo`,
|
|
103460
|
+
sellingFee: formatBps(t11.commissionBps),
|
|
103461
|
+
sellingFeeBps: t11.commissionBps,
|
|
103462
|
+
commission: formatBps(t11.commissionBps),
|
|
103463
|
+
commissionBps: t11.commissionBps,
|
|
103464
|
+
privatePackages: t11.privatePackages,
|
|
103465
|
+
priorityBuilds: t11.priorityBuilds,
|
|
103466
|
+
analyticsRetentionDays: t11.analyticsRetentionDays,
|
|
103467
|
+
maxArtifactMB: Math.round(t11.maxArtifactBytes / 1048576),
|
|
103468
|
+
seats: t11.seats,
|
|
103469
|
+
buildInsurance: t11.buildInsurance,
|
|
103470
|
+
securityAlerts: t11.securityAlerts,
|
|
103471
|
+
sbomExport: t11.sbomExport,
|
|
103472
|
+
teamEntitlements: t11.teamEntitlements
|
|
103473
|
+
})),
|
|
103474
|
+
discoveryFee: formatBps(DISCOVERY_FEE_BPS),
|
|
103475
|
+
discoveryFeeBps: DISCOVERY_FEE_BPS,
|
|
103476
|
+
paymentsEnabled: paymentsEnabled()
|
|
103477
|
+
}, { headers: { ...corsHeaders, "Cache-Control": "public, max-age=300" } });
|
|
103478
|
+
}
|
|
103479
|
+
const isTeamPath = path6 === "/account/team" || path6.startsWith("/account/team/");
|
|
103480
|
+
if (path6 !== "/account/subscription" && path6 !== "/account/billing-portal" && !isTeamPath)
|
|
103481
|
+
return null;
|
|
103482
|
+
const sessionToken = extractSessionToken(req);
|
|
103483
|
+
const user = sessionToken ? await auth.validateSession(sessionToken) : null;
|
|
103484
|
+
if (!user) {
|
|
103485
|
+
return Response.json({ error: "Sign in to manage your plan" }, { status: 401, headers: corsHeaders });
|
|
103486
|
+
}
|
|
103487
|
+
const current = await auth.getSubscription(user.email);
|
|
103488
|
+
const tier = await auth.getTier(user.email);
|
|
103489
|
+
if (path6 === "/account/subscription" && req.method === "GET") {
|
|
103490
|
+
const def = tierDefinition(tier);
|
|
103491
|
+
return Response.json({
|
|
103492
|
+
tier,
|
|
103493
|
+
name: def.name,
|
|
103494
|
+
status: current?.status || "none",
|
|
103495
|
+
sellingFee: formatBps(def.commissionBps),
|
|
103496
|
+
commission: formatBps(def.commissionBps),
|
|
103497
|
+
currentPeriodEnd: current?.currentPeriodEnd,
|
|
103498
|
+
manageable: Boolean(current?.stripeCustomerId)
|
|
103499
|
+
}, { headers: { ...corsHeaders, "Cache-Control": "no-store" } });
|
|
103500
|
+
}
|
|
103501
|
+
if (path6 === "/account/subscription" && req.method === "POST") {
|
|
103502
|
+
const body = await req.json().catch(() => null);
|
|
103503
|
+
const requested = tierOf(body?.tier);
|
|
103504
|
+
if (requested === "free") {
|
|
103505
|
+
return Response.json({ error: "Cancel from the billing portal rather than downgrading here", hint: "POST /account/billing-portal" }, { status: 400, headers: corsHeaders });
|
|
103506
|
+
}
|
|
103507
|
+
if (tier === requested) {
|
|
103508
|
+
return Response.json({ alreadySubscribed: true, tier }, { headers: corsHeaders });
|
|
103509
|
+
}
|
|
103510
|
+
try {
|
|
103511
|
+
const session = await createSubscriptionCheckout({
|
|
103512
|
+
tier: tierDefinition(requested),
|
|
103513
|
+
email: user.email,
|
|
103514
|
+
baseUrl,
|
|
103515
|
+
stripeCustomerId: current?.stripeCustomerId
|
|
103516
|
+
});
|
|
103517
|
+
return Response.json({ url: session.url }, { headers: corsHeaders });
|
|
103518
|
+
} catch (err) {
|
|
103519
|
+
console.error("Subscription checkout error:", err);
|
|
103520
|
+
return Response.json({ error: err.message || "Could not start checkout" }, { status: 400, headers: corsHeaders });
|
|
103521
|
+
}
|
|
103522
|
+
}
|
|
103523
|
+
if (isTeamPath) {
|
|
103524
|
+
const def = tierDefinition(tier);
|
|
103525
|
+
if (path6 === "/account/team" && req.method === "GET") {
|
|
103526
|
+
const members = await auth.getTeamMembers(user.email);
|
|
103527
|
+
const belongsTo = await auth.getTeamOwner(user.email);
|
|
103528
|
+
return Response.json({
|
|
103529
|
+
tier,
|
|
103530
|
+
seats: def.seats,
|
|
103531
|
+
seatsUsed: members.length + 1,
|
|
103532
|
+
members,
|
|
103533
|
+
memberOf: belongsTo,
|
|
103534
|
+
canInvite: def.seats > 1
|
|
103535
|
+
}, { headers: { ...corsHeaders, "Cache-Control": "no-store" } });
|
|
103536
|
+
}
|
|
103537
|
+
if (path6 === "/account/team/members" && req.method === "POST") {
|
|
103538
|
+
if (def.seats <= 1) {
|
|
103539
|
+
return Response.json({
|
|
103540
|
+
error: "Seats are a Team feature",
|
|
103541
|
+
hint: "pantry subscribe team",
|
|
103542
|
+
tier
|
|
103543
|
+
}, { status: 402, headers: corsHeaders });
|
|
103544
|
+
}
|
|
103545
|
+
const body = await req.json().catch(() => null);
|
|
103546
|
+
const invitee = typeof body?.email === "string" ? body.email.trim() : "";
|
|
103547
|
+
if (!invitee) {
|
|
103548
|
+
return Response.json({ error: "An email address is required" }, { status: 400, headers: corsHeaders });
|
|
103549
|
+
}
|
|
103550
|
+
try {
|
|
103551
|
+
const members = await auth.addTeamMember(user.email, invitee, def.seats);
|
|
103552
|
+
return Response.json({ members, seats: def.seats, seatsUsed: members.length + 1 }, { headers: corsHeaders });
|
|
103553
|
+
} catch (err) {
|
|
103554
|
+
const status = err instanceof AuthError ? err.status : 400;
|
|
103555
|
+
return Response.json({ error: err.message }, { status, headers: corsHeaders });
|
|
103556
|
+
}
|
|
103557
|
+
}
|
|
103558
|
+
const removeMatch = path6.match(/^\/account\/team\/members\/(.+)$/);
|
|
103559
|
+
if (removeMatch && req.method === "DELETE") {
|
|
103560
|
+
const target = decodeURIComponent(removeMatch[1]);
|
|
103561
|
+
const members = await auth.removeTeamMember(user.email, target);
|
|
103562
|
+
return Response.json({ members, seats: def.seats, seatsUsed: members.length + 1 }, { headers: corsHeaders });
|
|
103563
|
+
}
|
|
103564
|
+
return Response.json({ error: "Not found" }, { status: 404, headers: corsHeaders });
|
|
103565
|
+
}
|
|
103566
|
+
if (path6 === "/account/billing-portal" && req.method === "POST") {
|
|
103567
|
+
if (!current?.stripeCustomerId) {
|
|
103568
|
+
return Response.json({ error: "This account has no billing history" }, { status: 400, headers: corsHeaders });
|
|
103569
|
+
}
|
|
103570
|
+
try {
|
|
103571
|
+
const session = await createBillingPortalSession(current.stripeCustomerId, `${baseUrl}/account`);
|
|
103572
|
+
return Response.json({ url: session.url }, { headers: corsHeaders });
|
|
103573
|
+
} catch (err) {
|
|
103574
|
+
console.error("Billing portal error:", err);
|
|
103575
|
+
return Response.json({ error: err.message || "Could not open the billing portal" }, { status: 400, headers: corsHeaders });
|
|
103576
|
+
}
|
|
103577
|
+
}
|
|
103578
|
+
return null;
|
|
103579
|
+
}
|
|
103580
|
+
var _mirrorStore = null;
|
|
103581
|
+
var _securityStore = null;
|
|
103582
|
+
function mirrorStore(registry) {
|
|
103583
|
+
if (!_mirrorStore)
|
|
103584
|
+
_mirrorStore = new MirrorStore(registry.tarball);
|
|
103585
|
+
return _mirrorStore;
|
|
103586
|
+
}
|
|
103587
|
+
function securityStore(registry) {
|
|
103588
|
+
if (!_securityStore)
|
|
103589
|
+
_securityStore = new SecurityStore(registry.tarball);
|
|
103590
|
+
return _securityStore;
|
|
103591
|
+
}
|
|
103592
|
+
async function orgFor(email) {
|
|
103593
|
+
if (!_authService)
|
|
103594
|
+
return email;
|
|
103595
|
+
return await _authService.getTeamOwner(email) || email;
|
|
103596
|
+
}
|
|
103597
|
+
async function requirePaidOrg(req, feature, corsHeaders, entitled = (t11) => t11.buildInsurance) {
|
|
103598
|
+
const identity = await identifyReader(req);
|
|
103599
|
+
if (!identity.authenticated || !identity.userId || identity.userId === "_admin") {
|
|
103600
|
+
return Response.json({ error: "Sign in to use this", hint: "pantry token set" }, { status: 401, headers: corsHeaders });
|
|
103601
|
+
}
|
|
103602
|
+
const org = await orgFor(identity.userId);
|
|
103603
|
+
const tier = await tierForUser(org);
|
|
103604
|
+
if (!entitled(tierDefinition(tier))) {
|
|
103605
|
+
return Response.json({
|
|
103606
|
+
error: `${feature} is a paid feature`,
|
|
103607
|
+
hint: "pantry subscribe pro",
|
|
103608
|
+
tier
|
|
103609
|
+
}, { status: 402, headers: corsHeaders });
|
|
103610
|
+
}
|
|
103611
|
+
return { org, email: identity.userId, tier };
|
|
103612
|
+
}
|
|
103613
|
+
async function handleEnterpriseRoutes(path6, req, registry, corsHeaders) {
|
|
103614
|
+
if (!path6.startsWith("/mirror") && !path6.startsWith("/security") && path6 !== "/sbom")
|
|
103615
|
+
return null;
|
|
103616
|
+
if (path6 === "/mirror/snapshot" && req.method === "POST") {
|
|
103617
|
+
const gate = await requirePaidOrg(req, "Build insurance", corsHeaders);
|
|
103618
|
+
if (gate instanceof Response)
|
|
103619
|
+
return gate;
|
|
103620
|
+
const body = await req.json().catch(() => null);
|
|
103621
|
+
const entries = normalizeEntries(body?.entries);
|
|
103622
|
+
if (entries.length === 0) {
|
|
103623
|
+
return Response.json({ error: "No usable entries \u2014 send {entries: [{name, version, resolved, integrity}]}" }, { status: 400, headers: corsHeaders });
|
|
103624
|
+
}
|
|
103625
|
+
const result = await mirrorStore(registry).snapshot(gate.org, entries);
|
|
103626
|
+
return Response.json({
|
|
103627
|
+
org: gate.org,
|
|
103628
|
+
mirrored: result.mirrored,
|
|
103629
|
+
skipped: result.skipped,
|
|
103630
|
+
failed: result.failed,
|
|
103631
|
+
failures: result.entries.filter((e11) => e11.error).map((e11) => ({ name: e11.name, version: e11.version, error: e11.error }))
|
|
103632
|
+
}, { headers: corsHeaders });
|
|
103633
|
+
}
|
|
103634
|
+
if (path6 === "/mirror" && req.method === "GET") {
|
|
103635
|
+
const gate = await requirePaidOrg(req, "Build insurance", corsHeaders);
|
|
103636
|
+
if (gate instanceof Response)
|
|
103637
|
+
return gate;
|
|
103638
|
+
const store = mirrorStore(registry);
|
|
103639
|
+
return Response.json({
|
|
103640
|
+
org: gate.org,
|
|
103641
|
+
stats: await store.stats(gate.org),
|
|
103642
|
+
entries: (await store.list(gate.org)).slice(0, 500)
|
|
103643
|
+
}, { headers: { ...corsHeaders, "Cache-Control": "no-store" } });
|
|
103644
|
+
}
|
|
103645
|
+
const mirrorTarball = path6.match(/^\/mirror\/(.+)\/([^/]+)\/tarball$/);
|
|
103646
|
+
if (mirrorTarball && req.method === "GET") {
|
|
103647
|
+
const gate = await requirePaidOrg(req, "Build insurance", corsHeaders);
|
|
103648
|
+
if (gate instanceof Response)
|
|
103649
|
+
return gate;
|
|
103650
|
+
const found = await mirrorStore(registry).fetchArtifact(gate.org, decodeURIComponent(mirrorTarball[1]), decodeURIComponent(mirrorTarball[2]));
|
|
103651
|
+
if (!found) {
|
|
103652
|
+
return Response.json({ error: "Not in your mirror" }, { status: 404, headers: corsHeaders });
|
|
103653
|
+
}
|
|
103654
|
+
return new Response(found.bytes, {
|
|
103655
|
+
headers: {
|
|
103656
|
+
...corsHeaders,
|
|
103657
|
+
"Content-Type": "application/gzip",
|
|
103658
|
+
"Content-Length": String(found.bytes.byteLength),
|
|
103659
|
+
"X-Pantry-Mirrored-At": found.record.mirroredAt,
|
|
103660
|
+
...found.record.sha256 ? { "X-Pantry-SHA256": found.record.sha256 } : {},
|
|
103661
|
+
"Cache-Control": "private, max-age=31536000, immutable"
|
|
103662
|
+
}
|
|
103663
|
+
});
|
|
103664
|
+
}
|
|
103665
|
+
if (path6 === "/security/watch" && (req.method === "PUT" || req.method === "POST")) {
|
|
103666
|
+
const gate = await requirePaidOrg(req, "Security alerts", corsHeaders, (t11) => t11.securityAlerts);
|
|
103667
|
+
if (gate instanceof Response)
|
|
103668
|
+
return gate;
|
|
103669
|
+
const body = await req.json().catch(() => null);
|
|
103670
|
+
const entries = normalizeEntries(body?.entries);
|
|
103671
|
+
if (entries.length === 0) {
|
|
103672
|
+
return Response.json({ error: "No usable entries \u2014 send {entries: [{name, version, ecosystem, license}]}" }, { status: 400, headers: corsHeaders });
|
|
103673
|
+
}
|
|
103674
|
+
const list = await securityStore(registry).setWatchList(gate.org, entries, normalizePolicy(body?.policy));
|
|
103675
|
+
return Response.json({ org: gate.org, watched: list.entries.length, policy: list.policy }, { headers: corsHeaders });
|
|
103676
|
+
}
|
|
103677
|
+
if (path6 === "/security/policy" && (req.method === "PUT" || req.method === "POST")) {
|
|
103678
|
+
const gate = await requirePaidOrg(req, "Security alerts", corsHeaders, (t11) => t11.securityAlerts);
|
|
103679
|
+
if (gate instanceof Response)
|
|
103680
|
+
return gate;
|
|
103681
|
+
const policy = normalizePolicy(await req.json().catch(() => null));
|
|
103682
|
+
if (!policy) {
|
|
103683
|
+
return Response.json({ error: "Send {allow: [...]} and/or {deny: [...]}" }, { status: 400, headers: corsHeaders });
|
|
103684
|
+
}
|
|
103685
|
+
const list = await securityStore(registry).setPolicy(gate.org, policy);
|
|
103686
|
+
return Response.json({ org: gate.org, policy: list.policy }, { headers: corsHeaders });
|
|
103687
|
+
}
|
|
103688
|
+
if (path6 === "/security/alerts" && req.method === "GET") {
|
|
103689
|
+
const gate = await requirePaidOrg(req, "Security alerts", corsHeaders, (t11) => t11.securityAlerts);
|
|
103690
|
+
if (gate instanceof Response)
|
|
103691
|
+
return gate;
|
|
103692
|
+
const report = await securityStore(registry).report(gate.org);
|
|
103693
|
+
return Response.json(report, { headers: { ...corsHeaders, "Cache-Control": "no-store" } });
|
|
103694
|
+
}
|
|
103695
|
+
if (path6 === "/sbom" && req.method === "GET") {
|
|
103696
|
+
const gate = await requirePaidOrg(req, "SBOM export", corsHeaders, (t11) => t11.sbomExport);
|
|
103697
|
+
if (gate instanceof Response)
|
|
103698
|
+
return gate;
|
|
103699
|
+
const url = new URL(req.url);
|
|
103700
|
+
const format = parseFormat(url.searchParams.get("format"));
|
|
103701
|
+
let entries = await mirrorStore(registry).list(gate.org);
|
|
103702
|
+
if (entries.length === 0) {
|
|
103703
|
+
const watched = await securityStore(registry).getWatchList(gate.org);
|
|
103704
|
+
entries = watched.entries.map((e11) => ({ ...e11, mirroredAt: watched.updatedAt }));
|
|
103705
|
+
}
|
|
103706
|
+
const document2 = buildSbom(entries, format, {
|
|
103707
|
+
org: gate.org,
|
|
103708
|
+
subject: url.searchParams.get("name") || undefined
|
|
103709
|
+
});
|
|
103710
|
+
return new Response(JSON.stringify(document2, null, 2), {
|
|
103711
|
+
headers: {
|
|
103712
|
+
...corsHeaders,
|
|
103713
|
+
"Content-Type": "application/json",
|
|
103714
|
+
"Content-Disposition": `attachment; filename="${format === "spdx" ? "sbom.spdx.json" : "sbom.cdx.json"}"`,
|
|
103715
|
+
"Cache-Control": "no-store"
|
|
103716
|
+
}
|
|
103717
|
+
});
|
|
103718
|
+
}
|
|
103719
|
+
return null;
|
|
103720
|
+
}
|
|
103721
|
+
async function resolvePackageAccess(req, registry, packageName, version3) {
|
|
103722
|
+
const paywall = await registry.metadata.getPaywall(packageName);
|
|
103723
|
+
if (!paywall || !paywall.enabled)
|
|
103724
|
+
return { allowed: true, reason: "no-paywall" };
|
|
103725
|
+
const identity = await identifyReader(req);
|
|
103726
|
+
const record = await registry.getPublisherPackageRecord(packageName);
|
|
103727
|
+
return resolveAccess(registry.metadata, packageName, version3, {
|
|
103728
|
+
userId: identity.userId,
|
|
103729
|
+
token: extractBearerToken(req.headers.get("authorization")),
|
|
103730
|
+
admin: identity.userId === "_admin",
|
|
103731
|
+
org: identity.userId && identity.userId !== "_admin" ? await orgFor(identity.userId) : null
|
|
103732
|
+
}, record?.publishedBy);
|
|
103733
|
+
}
|
|
103734
|
+
async function handleAdminRoutes(path6, req, auth, corsHeaders) {
|
|
103735
|
+
if (!path6.startsWith("/admin/"))
|
|
103736
|
+
return null;
|
|
103737
|
+
const known = path6 === "/admin/users" || path6 === "/admin/tokens" || path6 === "/admin/tokens/revoke";
|
|
103738
|
+
if (!known || req.method !== "POST")
|
|
103739
|
+
return null;
|
|
103740
|
+
if (!await isAdminRequest(req)) {
|
|
103741
|
+
return Response.json({ error: "Admin authentication required", hint: "Send the registry token: Authorization: Bearer $PANTRY_REGISTRY_TOKEN" }, { status: 401, headers: corsHeaders });
|
|
103742
|
+
}
|
|
103743
|
+
const body = await req.json().catch(() => null);
|
|
103744
|
+
if (!body)
|
|
103745
|
+
return Response.json({ error: "Invalid JSON body" }, { status: 400, headers: corsHeaders });
|
|
103746
|
+
const email = typeof body.email === "string" ? body.email : "";
|
|
103747
|
+
try {
|
|
103748
|
+
if (path6 === "/admin/users") {
|
|
103749
|
+
const user = await auth.upsertUserAccount(email, typeof body.name === "string" ? body.name : "", typeof body.password === "string" ? body.password : "", body.role === "admin" ? "admin" : "user");
|
|
103750
|
+
return Response.json({ success: true, user }, { status: 201, headers: corsHeaders });
|
|
103751
|
+
}
|
|
103752
|
+
if (path6 === "/admin/tokens") {
|
|
103753
|
+
const user = await auth.findUser(email);
|
|
103754
|
+
if (!user)
|
|
103755
|
+
return Response.json({ error: `No such user: ${email}` }, { status: 404, headers: corsHeaders });
|
|
103756
|
+
const valid = ["publish", "read"];
|
|
103757
|
+
const permissions2 = Array.isArray(body.permissions) ? body.permissions.filter((p11) => valid.includes(p11)) : ["read"];
|
|
103758
|
+
const result = await auth.createApiToken(user.email, typeof body.name === "string" ? body.name : "admin-issued", {
|
|
103759
|
+
permissions: permissions2.length > 0 ? permissions2 : ["read"],
|
|
103760
|
+
expiresInDays: typeof body.expiresInDays === "number" ? body.expiresInDays : undefined
|
|
103761
|
+
});
|
|
103762
|
+
return Response.json({ success: true, ...result }, { status: 201, headers: corsHeaders });
|
|
103763
|
+
}
|
|
103764
|
+
const id2 = typeof body.id === "string" ? body.id : "";
|
|
103765
|
+
if (!email || !id2)
|
|
103766
|
+
return Response.json({ error: "email and id are required" }, { status: 400, headers: corsHeaders });
|
|
103767
|
+
await auth.deleteApiToken(email.toLowerCase().trim(), id2);
|
|
103768
|
+
return Response.json({ success: true }, { headers: corsHeaders });
|
|
103769
|
+
} catch (err) {
|
|
103770
|
+
const status = err instanceof AuthError ? err.status : 400;
|
|
103771
|
+
return Response.json({ error: err.message }, { status, headers: corsHeaders });
|
|
103772
|
+
}
|
|
103773
|
+
}
|
|
101957
103774
|
async function handlePublish(req, registry, corsHeaders) {
|
|
101958
103775
|
const contentType = req.headers.get("content-type") || "";
|
|
101959
103776
|
const authHeader = req.headers.get("authorization");
|
|
@@ -101986,8 +103803,17 @@ async function handlePublish(req, registry, corsHeaders) {
|
|
|
101986
103803
|
const metaErr = validateMetadataLimits(metadata);
|
|
101987
103804
|
if (metaErr)
|
|
101988
103805
|
return Response.json({ error: metaErr }, { status: 400, headers: corsHeaders });
|
|
101989
|
-
|
|
101990
|
-
|
|
103806
|
+
const notYours = await requirePublishRights(registry, authResult.userId, metadata.name, corsHeaders);
|
|
103807
|
+
if (notYours)
|
|
103808
|
+
return notYours;
|
|
103809
|
+
const publisherTier = await tierForPackage(registry, metadata.name, authResult.userId);
|
|
103810
|
+
const maxBytes = tierDefinition(publisherTier).maxArtifactBytes;
|
|
103811
|
+
if (tarballFile.size > maxBytes) {
|
|
103812
|
+
const mb3 = (bytes) => `${Math.round(bytes / 1048576)}MB`;
|
|
103813
|
+
return Response.json({
|
|
103814
|
+
error: `Tarball is ${mb3(tarballFile.size)}, over the ${mb3(maxBytes)} limit for the ${tierDefinition(publisherTier).name} plan`,
|
|
103815
|
+
...publisherTier === "free" ? { hint: "Pro raises this to 250MB, Team to 1GB: pantry subscribe pro" } : {}
|
|
103816
|
+
}, { status: 413, headers: corsHeaders });
|
|
101991
103817
|
}
|
|
101992
103818
|
const exists = await registry.exists(metadata.name, metadata.version);
|
|
101993
103819
|
if (exists) {
|
|
@@ -102015,6 +103841,9 @@ async function handlePublish(req, registry, corsHeaders) {
|
|
|
102015
103841
|
const jsonVersionErr = validatePublishVersion(metadata.version);
|
|
102016
103842
|
if (jsonVersionErr)
|
|
102017
103843
|
return Response.json({ error: jsonVersionErr }, { status: 400, headers: corsHeaders });
|
|
103844
|
+
const notYoursJson = await requirePublishRights(registry, authResult.userId, metadata.name, corsHeaders);
|
|
103845
|
+
if (notYoursJson)
|
|
103846
|
+
return notYoursJson;
|
|
102018
103847
|
const jsonMetaErr = validateMetadataLimits(metadata);
|
|
102019
103848
|
if (jsonMetaErr)
|
|
102020
103849
|
return Response.json({ error: jsonMetaErr }, { status: 400, headers: corsHeaders });
|
|
@@ -102142,10 +103971,25 @@ function extractSessionToken(req) {
|
|
|
102142
103971
|
const match = cookie.match(/pantry_session=([^;]+)/);
|
|
102143
103972
|
return match ? match[1] : null;
|
|
102144
103973
|
}
|
|
103974
|
+
function safeRedirectTarget(value) {
|
|
103975
|
+
if (!value || !value.startsWith("/") || value.startsWith("//"))
|
|
103976
|
+
return null;
|
|
103977
|
+
return value;
|
|
103978
|
+
}
|
|
103979
|
+
function signupRejection(email) {
|
|
103980
|
+
if (!signupsEnabled())
|
|
103981
|
+
return "Signups are closed on this registry \u2014 ask an operator for an account";
|
|
103982
|
+
if (!isSignupEmailAllowed(email))
|
|
103983
|
+
return "That email domain is not allowed to sign up on this registry";
|
|
103984
|
+
return null;
|
|
103985
|
+
}
|
|
102145
103986
|
async function handleAuthRoutes(path6, req, auth, corsHeaders) {
|
|
102146
103987
|
if (path6 === "/auth/signup" && req.method === "POST") {
|
|
102147
103988
|
try {
|
|
102148
103989
|
const body = await req.json();
|
|
103990
|
+
const rejection = signupRejection(body.email || "");
|
|
103991
|
+
if (rejection)
|
|
103992
|
+
return Response.json({ error: rejection }, { status: 403, headers: corsHeaders });
|
|
102149
103993
|
await auth.signup(body.email || "", body.name || "", body.password || "");
|
|
102150
103994
|
const { sessionToken, user: loggedInUser } = await auth.login(body.email || "", body.password || "");
|
|
102151
103995
|
return new Response(JSON.stringify({ success: true, user: loggedInUser, sessionToken }), {
|
|
@@ -102276,11 +104120,12 @@ async function handleSiteAuth(path6, req, auth, corsHeaders) {
|
|
|
102276
104120
|
const email = formData.get("email") || "";
|
|
102277
104121
|
const password = formData.get("password") || "";
|
|
102278
104122
|
const { sessionToken: sessionToken2 } = await auth.login(email, password);
|
|
104123
|
+
const next = safeRedirectTarget(formData.get("next") || new URL(req.url).searchParams.get("next"));
|
|
102279
104124
|
return new Response(null, {
|
|
102280
104125
|
status: 302,
|
|
102281
104126
|
headers: {
|
|
102282
104127
|
...htmlHeaders,
|
|
102283
|
-
Location: "/account",
|
|
104128
|
+
Location: next || "/account",
|
|
102284
104129
|
"Set-Cookie": `pantry_session=${sessionToken2}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=2592000`
|
|
102285
104130
|
}
|
|
102286
104131
|
});
|
|
@@ -102300,12 +104145,20 @@ async function handleSiteAuth(path6, req, auth, corsHeaders) {
|
|
|
102300
104145
|
return new Response(html, { headers: htmlHeaders });
|
|
102301
104146
|
}
|
|
102302
104147
|
if (path6 === "/signup") {
|
|
104148
|
+
if (!signupsEnabled()) {
|
|
104149
|
+
return new Response(null, { status: 302, headers: { ...htmlHeaders, Location: "/login" } });
|
|
104150
|
+
}
|
|
102303
104151
|
if (req.method === "POST") {
|
|
102304
104152
|
try {
|
|
102305
104153
|
const formData = await req.formData();
|
|
102306
104154
|
const email = formData.get("email") || "";
|
|
102307
104155
|
const name = formData.get("name") || "";
|
|
102308
104156
|
const password = formData.get("password") || "";
|
|
104157
|
+
const rejection = signupRejection(email);
|
|
104158
|
+
if (rejection) {
|
|
104159
|
+
const html2 = await renderSitePage("signup.stx", { error: escapeHtml4(rejection), title: "Sign Up" });
|
|
104160
|
+
return new Response(html2, { status: 403, headers: htmlHeaders });
|
|
104161
|
+
}
|
|
102309
104162
|
await auth.signup(email, name, password);
|
|
102310
104163
|
const { sessionToken: sessionToken2 } = await auth.login(email, password);
|
|
102311
104164
|
return new Response(null, {
|
|
@@ -102371,14 +104224,16 @@ async function requireSessionUser(req, auth) {
|
|
|
102371
104224
|
function isSiteAdmin(user) {
|
|
102372
104225
|
return user.role === "admin";
|
|
102373
104226
|
}
|
|
102374
|
-
function canManagePackage(pkg, user) {
|
|
104227
|
+
async function canManagePackage(pkg, user) {
|
|
102375
104228
|
if (!pkg)
|
|
102376
104229
|
return false;
|
|
102377
104230
|
if (isSiteAdmin(user))
|
|
102378
104231
|
return true;
|
|
102379
104232
|
if (!pkg.publishedBy)
|
|
102380
104233
|
return true;
|
|
102381
|
-
|
|
104234
|
+
if (pkg.publishedBy === user.email)
|
|
104235
|
+
return true;
|
|
104236
|
+
return _authService ? _authService.canActFor(user.email, pkg.publishedBy) : false;
|
|
102382
104237
|
}
|
|
102383
104238
|
async function handlePublisherApi(path6, req, registry, analyticsStorage, auth, corsHeaders) {
|
|
102384
104239
|
if (!path6.startsWith("/publisher/api/"))
|
|
@@ -102397,16 +104252,75 @@ async function handlePublisherApi(path6, req, registry, analyticsStorage, auth,
|
|
|
102397
104252
|
const packages = await registry.listPublisherPackages(user.email, admin ? 200 : 50, admin);
|
|
102398
104253
|
return Response.json({ packages, admin }, { headers: corsHeaders });
|
|
102399
104254
|
}
|
|
104255
|
+
const paywallMatch = path6.match(/^\/publisher\/api\/packages\/(.+)\/paywall$/);
|
|
104256
|
+
if (paywallMatch) {
|
|
104257
|
+
const name = decodeURIComponent(paywallMatch[1]);
|
|
104258
|
+
const record = await registry.getPublisherPackageRecord(name);
|
|
104259
|
+
if (!record || !await canManagePackage(record, user)) {
|
|
104260
|
+
return Response.json({ error: "Package not found or access denied" }, { status: 403, headers: corsHeaders });
|
|
104261
|
+
}
|
|
104262
|
+
if (req.method === "GET") {
|
|
104263
|
+
const paywall = await registry.metadata.getPaywall(name);
|
|
104264
|
+
return Response.json({
|
|
104265
|
+
paywall: paywall && paywall.enabled ? {
|
|
104266
|
+
enabled: true,
|
|
104267
|
+
price: paywall.price,
|
|
104268
|
+
currency: paywall.currency,
|
|
104269
|
+
formattedPrice: formatPrice(paywall.price, paywall.currency),
|
|
104270
|
+
freeVersions: paywall.freeVersions || [],
|
|
104271
|
+
payoutAccountId: paywall.stripeAccountId
|
|
104272
|
+
} : { enabled: false },
|
|
104273
|
+
paymentsEnabled: paymentsEnabled()
|
|
104274
|
+
}, { headers: { ...corsHeaders, "Cache-Control": "no-store" } });
|
|
104275
|
+
}
|
|
104276
|
+
if (req.method === "PUT" || req.method === "POST") {
|
|
104277
|
+
let body;
|
|
104278
|
+
try {
|
|
104279
|
+
body = await req.json();
|
|
104280
|
+
} catch {
|
|
104281
|
+
return Response.json({ error: "Invalid JSON" }, { status: 400, headers: corsHeaders });
|
|
104282
|
+
}
|
|
104283
|
+
const invalid = validatePriceConfig(body);
|
|
104284
|
+
if (invalid)
|
|
104285
|
+
return Response.json({ error: invalid }, { status: 400, headers: corsHeaders });
|
|
104286
|
+
if (!record.publishedBy && !admin) {
|
|
104287
|
+
await registry.claimPublisherPackage(name, user.email);
|
|
104288
|
+
}
|
|
104289
|
+
try {
|
|
104290
|
+
const paywall = await configurePaywall(registry.metadata, name, body);
|
|
104291
|
+
return Response.json({
|
|
104292
|
+
paywall: {
|
|
104293
|
+
enabled: paywall.enabled,
|
|
104294
|
+
price: paywall.price,
|
|
104295
|
+
currency: paywall.currency,
|
|
104296
|
+
formattedPrice: formatPrice(paywall.price, paywall.currency),
|
|
104297
|
+
freeVersions: paywall.freeVersions || [],
|
|
104298
|
+
payoutAccountId: paywall.stripeAccountId
|
|
104299
|
+
},
|
|
104300
|
+
paymentsEnabled: paymentsEnabled()
|
|
104301
|
+
}, { headers: corsHeaders });
|
|
104302
|
+
} catch (err) {
|
|
104303
|
+
return Response.json({ error: err.message || "Could not set the price" }, { status: 400, headers: corsHeaders });
|
|
104304
|
+
}
|
|
104305
|
+
}
|
|
104306
|
+
if (req.method === "DELETE") {
|
|
104307
|
+
await registry.metadata.deletePaywall(name);
|
|
104308
|
+
return Response.json({ paywall: { enabled: false } }, { headers: corsHeaders });
|
|
104309
|
+
}
|
|
104310
|
+
}
|
|
102400
104311
|
const pkgMatch = path6.match(/^\/publisher\/api\/packages\/(.+)$/);
|
|
102401
104312
|
if (pkgMatch) {
|
|
102402
104313
|
const name = decodeURIComponent(pkgMatch[1]);
|
|
102403
104314
|
if (req.method === "GET") {
|
|
102404
104315
|
const record = await registry.getPublisherPackageRecord(name);
|
|
102405
|
-
if (!record || !canManagePackage(record, user)) {
|
|
104316
|
+
if (!record || !await canManagePackage(record, user)) {
|
|
102406
104317
|
return Response.json({ error: "Package not found or access denied" }, { status: 403, headers: corsHeaders });
|
|
102407
104318
|
}
|
|
102408
104319
|
const stats = await analyticsStorage.getPackageStats(name);
|
|
102409
104320
|
const commits = await registry.getPackageCommits(name, 15);
|
|
104321
|
+
const tier = await auth.getTier(user.email);
|
|
104322
|
+
const retentionDays = tierDefinition(tier).analyticsRetentionDays;
|
|
104323
|
+
const timeline = await analyticsStorage.getDownloadTimeline(name, retentionDays);
|
|
102410
104324
|
return Response.json({
|
|
102411
104325
|
package: record,
|
|
102412
104326
|
stats: {
|
|
@@ -102414,12 +104328,14 @@ async function handlePublisherApi(path6, req, registry, analyticsStorage, auth,
|
|
|
102414
104328
|
downloads30d: stats?.monthlyDownloads ?? 0,
|
|
102415
104329
|
weeklyDownloads: stats?.weeklyDownloads ?? 0
|
|
102416
104330
|
},
|
|
104331
|
+
timeline,
|
|
104332
|
+
analytics: { tier, retentionDays, truncated: retentionDays < 3650 },
|
|
102417
104333
|
commits
|
|
102418
104334
|
}, { headers: corsHeaders });
|
|
102419
104335
|
}
|
|
102420
104336
|
if (req.method === "PATCH") {
|
|
102421
104337
|
const record = await registry.getPublisherPackageRecord(name);
|
|
102422
|
-
if (!record || !canManagePackage(record, user)) {
|
|
104338
|
+
if (!record || !await canManagePackage(record, user)) {
|
|
102423
104339
|
return Response.json({ error: "Package not found or access denied" }, { status: 403, headers: corsHeaders });
|
|
102424
104340
|
}
|
|
102425
104341
|
let body;
|
|
@@ -102431,7 +104347,19 @@ async function handlePublisherApi(path6, req, registry, analyticsStorage, auth,
|
|
|
102431
104347
|
if (!record.publishedBy && !admin) {
|
|
102432
104348
|
await registry.claimPublisherPackage(name, user.email);
|
|
102433
104349
|
}
|
|
102434
|
-
const
|
|
104350
|
+
const settings = body.settings;
|
|
104351
|
+
if (settings?.visibility === "unlisted" && !admin) {
|
|
104352
|
+
const tier = await auth.getTier(user.email);
|
|
104353
|
+
if (!tierDefinition(tier).privatePackages) {
|
|
104354
|
+
return Response.json({
|
|
104355
|
+
error: "Unlisted packages are a Pro feature",
|
|
104356
|
+
hint: "Subscribe at /pricing, or with: pantry subscribe pro",
|
|
104357
|
+
tier
|
|
104358
|
+
}, { status: 402, headers: corsHeaders });
|
|
104359
|
+
}
|
|
104360
|
+
}
|
|
104361
|
+
const actingAs = record.publishedBy && record.publishedBy !== user.email && _authService && await _authService.canActFor(user.email, record.publishedBy) ? record.publishedBy : user.email;
|
|
104362
|
+
const updated = await registry.updatePublisherPackage(name, actingAs, {
|
|
102435
104363
|
description: body.description,
|
|
102436
104364
|
homepage: body.homepage,
|
|
102437
104365
|
repository: body.repository,
|
|
@@ -103592,6 +105520,15 @@ async function handleSitePackage(name, analytics, binaryStorage, registry, zigSt
|
|
|
103592
105520
|
const aliased = _aliases.get(name);
|
|
103593
105521
|
if (aliased && aliased !== name)
|
|
103594
105522
|
name = aliased;
|
|
105523
|
+
const paywall = registry ? await registry.metadata.getPaywall(name).catch(() => null) : null;
|
|
105524
|
+
const paidProps = paywall?.enabled ? {
|
|
105525
|
+
isPaid: true,
|
|
105526
|
+
priceLabel: formatPrice(paywall.price, paywall.currency),
|
|
105527
|
+
buyUrl: `/packages/${encodeURIComponent(name)}/buy`,
|
|
105528
|
+
freeVersionList: (paywall.freeVersions || []).join(", "),
|
|
105529
|
+
hasFreeVersions: (paywall.freeVersions || []).length > 0
|
|
105530
|
+
} : { isPaid: false, priceLabel: "", buyUrl: "", freeVersionList: "", hasFreeVersions: false };
|
|
105531
|
+
const renderPackagePage = (props) => renderSitePage("package.stx", { ...paidProps, ...props });
|
|
103595
105532
|
const safeName = escapeHtml4(name);
|
|
103596
105533
|
const encodedName = encodeURIComponent(name);
|
|
103597
105534
|
const [rawMeta, stats, timeline, pkgInfo, zigPkg, phpPkg] = await Promise.all([
|
|
@@ -103613,7 +105550,7 @@ async function handleSitePackage(name, analytics, binaryStorage, registry, zigSt
|
|
|
103613
105550
|
packagistPkg = await fetchFromPackagist(name).catch(() => null);
|
|
103614
105551
|
}
|
|
103615
105552
|
if (!meta && !pkgInfo && !zigPkg && !phpPkg && !packagistPkg) {
|
|
103616
|
-
const html2 = await
|
|
105553
|
+
const html2 = await renderPackagePage({
|
|
103617
105554
|
name,
|
|
103618
105555
|
safeName,
|
|
103619
105556
|
encodedName,
|
|
@@ -103636,7 +105573,7 @@ async function handleSitePackage(name, analytics, binaryStorage, registry, zigSt
|
|
|
103636
105573
|
const zigTimeline = (timeline || []).map((d10) => ({ date: d10.date, count: d10.count || 0 }));
|
|
103637
105574
|
const zigLineChart = generateLineChart(zigTimeline, 700, 200);
|
|
103638
105575
|
const zigStats = stats || { totalDownloads: 0, weeklyDownloads: 0, monthlyDownloads: 0, versionDownloads: {} };
|
|
103639
|
-
const html2 = await
|
|
105576
|
+
const html2 = await renderPackagePage({
|
|
103640
105577
|
name,
|
|
103641
105578
|
safeName,
|
|
103642
105579
|
encodedName,
|
|
@@ -103674,7 +105611,7 @@ async function handleSitePackage(name, analytics, binaryStorage, registry, zigSt
|
|
|
103674
105611
|
const phpLineChart = generateLineChart(phpTimeline, 700, 200);
|
|
103675
105612
|
const phpStats = stats || { totalDownloads: 0, weeklyDownloads: 0, monthlyDownloads: 0, versionDownloads: {} };
|
|
103676
105613
|
const phpDeps = phpPkg.require ? Object.keys(phpPkg.require).filter((d10) => d10 !== "php") : [];
|
|
103677
|
-
const html2 = await
|
|
105614
|
+
const html2 = await renderPackagePage({
|
|
103678
105615
|
name,
|
|
103679
105616
|
safeName,
|
|
103680
105617
|
encodedName,
|
|
@@ -103712,7 +105649,7 @@ async function handleSitePackage(name, analytics, binaryStorage, registry, zigSt
|
|
|
103712
105649
|
const pkgStats = stats || { totalDownloads: 0, weeklyDownloads: 0, monthlyDownloads: 0, versionDownloads: {} };
|
|
103713
105650
|
const pkgDeps = Object.keys(packagistPkg.require || {}).filter((d10) => d10 !== "php" && !d10.startsWith("ext-"));
|
|
103714
105651
|
const pkgVersions = packagistPkg.versions || [];
|
|
103715
|
-
const html2 = await
|
|
105652
|
+
const html2 = await renderPackagePage({
|
|
103716
105653
|
name,
|
|
103717
105654
|
safeName,
|
|
103718
105655
|
encodedName,
|
|
@@ -103783,7 +105720,7 @@ async function handleSitePackage(name, analytics, binaryStorage, registry, zigSt
|
|
|
103783
105720
|
const versionItems = Object.entries(versionDownloads).map(([label, value]) => ({ label, value })).sort((a11, b11) => b11.value - a11.value).slice(0, 8);
|
|
103784
105721
|
const versionDistribution = generateHorizontalBarChart(versionItems, 600, 28, 6, 120);
|
|
103785
105722
|
const pkgDescription = meta.description || `${name} \u2014 ${versionCount} versions available for macOS and Linux`;
|
|
103786
|
-
const html2 = await
|
|
105723
|
+
const html2 = await renderPackagePage({
|
|
103787
105724
|
name,
|
|
103788
105725
|
safeName,
|
|
103789
105726
|
encodedName,
|
|
@@ -103820,7 +105757,7 @@ async function handleSitePackage(name, analytics, binaryStorage, registry, zigSt
|
|
|
103820
105757
|
const fbStats = stats || { totalDownloads: pkgInfo?.downloads || 0, weeklyDownloads: 0, monthlyDownloads: 0, versionDownloads: {} };
|
|
103821
105758
|
const fbVersion = pkgInfo?.version || "unknown";
|
|
103822
105759
|
const fbVersions = fbVersion !== "unknown" ? [fbVersion] : [];
|
|
103823
|
-
const html = await
|
|
105760
|
+
const html = await renderPackagePage({
|
|
103824
105761
|
name,
|
|
103825
105762
|
safeName,
|
|
103826
105763
|
encodedName,
|
|
@@ -104366,6 +106303,8 @@ function rewritePackageJsonContent(content, packageDir) {
|
|
|
104366
106303
|
export {
|
|
104367
106304
|
verifyPassword,
|
|
104368
106305
|
validateZigHash,
|
|
106306
|
+
signupsEnabled,
|
|
106307
|
+
setPlugins,
|
|
104369
106308
|
searchPackagist,
|
|
104370
106309
|
searchNpm,
|
|
104371
106310
|
rewriteWorkspaceRanges,
|
|
@@ -104374,14 +106313,25 @@ export {
|
|
|
104374
106313
|
rewriteCatalogRanges,
|
|
104375
106314
|
resolveWorkspaceSpec,
|
|
104376
106315
|
resolveWorkspacePackages,
|
|
106316
|
+
resolveVisibility,
|
|
106317
|
+
resetPlugins,
|
|
106318
|
+
registryInfo,
|
|
104377
106319
|
readWorkspaceGlobsFromManifest,
|
|
104378
106320
|
readWorkspaceGlobs,
|
|
106321
|
+
pluginsAuthorize,
|
|
106322
|
+
pluginSpecifiers,
|
|
106323
|
+
pluginResponse,
|
|
106324
|
+
pluginAccessVerdict,
|
|
104379
106325
|
parseZigZon,
|
|
104380
106326
|
parseComposerJson,
|
|
104381
106327
|
manifestUsesWorkspaceProtocol,
|
|
104382
106328
|
manifestUsesCatalogProtocol,
|
|
106329
|
+
loadPlugins,
|
|
106330
|
+
loadPlugin,
|
|
104383
106331
|
listNpmVersions,
|
|
104384
106332
|
isUserApiToken,
|
|
106333
|
+
isSignupEmailAllowed,
|
|
106334
|
+
isPublicPath,
|
|
104385
106335
|
hashToken,
|
|
104386
106336
|
hashPassword,
|
|
104387
106337
|
handleStripeWebhook,
|
|
@@ -104395,6 +106345,9 @@ export {
|
|
|
104395
106345
|
findWorkspaceRoot,
|
|
104396
106346
|
fetchFromPackagist,
|
|
104397
106347
|
fetchFromNpm,
|
|
106348
|
+
extraPublicPaths,
|
|
106349
|
+
enforceReadAccess,
|
|
106350
|
+
emitPluginEvent,
|
|
104398
106351
|
downloadNpmTarball,
|
|
104399
106352
|
createZigStorage,
|
|
104400
106353
|
createServer,
|
|
@@ -104411,6 +106364,7 @@ export {
|
|
|
104411
106364
|
computeZigHash,
|
|
104412
106365
|
computePhpChecksum,
|
|
104413
106366
|
checkPaywallAccess,
|
|
106367
|
+
allowedSignupDomains,
|
|
104414
106368
|
WORKSPACE_RANGE_SECTIONS,
|
|
104415
106369
|
UnresolvableWorkspaceDependencyError,
|
|
104416
106370
|
UnresolvableCatalogDependencyError,
|