@stacksjs/registry 0.10.48 → 0.10.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +2080 -126
  2. 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 || this.queue.includes(d))
42262
+ if (!d)
42263
42263
  return false;
42264
- this.queue.push(d);
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
- var STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY || "";
42457
- var STRIPE_WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET || "";
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
- if (!STRIPE_SECRET_KEY)
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 ${STRIPE_SECRET_KEY}`
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(Object.entries(body).flatMap(([k, v]) => {
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
- async function checkPaywallAccess(storage, packageName, version, authToken) {
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
- if (paywall.freeVersions?.includes(version)) {
42488
- return { allowed: true, paywall };
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 (!authToken) {
42491
- return { allowed: false, paywall, reason: "Authentication required for paid package" };
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
- const grant = await storage.getAccessGrant(packageName, authToken);
42494
- if (grant) {
42495
- return { allowed: true, paywall };
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 { allowed: false, paywall, reason: "Payment required" };
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 (STRIPE_SECRET_KEY) {
42705
+ if (paymentsEnabled()) {
42505
42706
  if (!stripeProductId) {
42506
42707
  const product = await stripeRequest("POST", "/products", {
42507
42708
  name: packageName,
42508
- description: `Access to ${packageName} package`,
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 price = await stripeRequest("POST", "/prices", {
42514
- product: stripeProductId,
42515
- unit_amount: String(config.price),
42516
- currency: config.currency || "usd"
42517
- });
42518
- stripePriceId = price.id;
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: config.currency || "usd",
42728
+ currency,
42525
42729
  stripeProductId,
42526
42730
  stripePriceId,
42527
- freeVersions: config.freeVersions,
42528
- trialDays: config.trialDays,
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, packageName, accessToken, baseUrl) {
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("Package does not have a paywall");
42539
- }
42540
- if (!STRIPE_SECRET_KEY) {
42541
- throw new Error("Stripe is not configured \u2014 set STRIPE_SECRET_KEY");
42542
- }
42543
- if (!paywall.stripePriceId) {
42544
- throw new Error("Stripe price not configured for this package");
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
- success_url: `${baseUrl}/packages/${encodeURIComponent(packageName)}/checkout/success?token=${accessToken}`,
42551
- cancel_url: `${baseUrl}/packages/${encodeURIComponent(packageName)}`,
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[access_token]": accessToken,
42554
- ...paywall.stripeAccountId ? { "payment_intent_data[transfer_data][destination]": paywall.stripeAccountId } : {}
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 (!STRIPE_WEBHOOK_SECRET) {
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 accessToken = session.metadata?.access_token;
42582
- if (packageName && accessToken) {
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: accessToken,
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(STRIPE_WEBHOOK_SECRET), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
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 amount = price / 100;
42627
- const symbol = currency === "usd" ? "$" : currency === "eur" ? "\u20AC" : currency === "gbp" ? "\xA3" : `${currency.toUpperCase()} `;
42628
- return `${symbol}${amount.toFixed(2)}`;
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: "admin",
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: "admin", createdAt: user.createdAt, updatedAt: user.updatedAt };
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
- if (!tokenRecord.permissions.includes("publish")) {
100346
- return { valid: false, error: "Token does not have publish permission" };
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 queued = getBuildStatus().requestRebuild(domain);
101245
- return Response.json({ queued, domain }, { headers: corsHeaders });
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
- }, { headers: corsHeaders });
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 authResult = await validateToken3(req.headers.get("authorization"));
101555
- if (!authResult.valid) {
101556
- return Response.json({ error: authResult.error }, { status: 401, headers: corsHeaders });
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
- if (!body.price || typeof body.price !== "number" || body.price < 100) {
101565
- return Response.json({ error: "Price must be at least 100 cents ($1.00)" }, { status: 400, headers: corsHeaders });
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 authResult = await validateToken3(req.headers.get("authorization"));
101580
- if (!authResult.valid) {
101581
- return Response.json({ error: authResult.error }, { status: 401, headers: corsHeaders });
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 === "GET") {
101587
- const token = url.searchParams.get("token");
101588
- if (!token) {
101589
- return Response.json({ error: "Token required \u2014 run `pantry auth login` first" }, { status: 400, headers: corsHeaders });
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, packageName, token, baseUrl);
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: session.url }
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: "Failed to create checkout session" }, { status: 400, headers: corsHeaders });
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 authToken = extractBearerToken(req.headers.get("authorization")) || url.searchParams.get("token");
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 checkoutUrl = `${baseUrl}/packages/${encodeURIComponent(packageName)}/checkout`;
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: formatPrice(access4.paywall.price, access4.paywall.currency),
101628
- checkoutUrl,
101629
- message: `This package requires payment (${formatPrice(access4.paywall.price, access4.paywall.currency)}). Visit ${checkoutUrl} to purchase access, or run: pantry auth login`
101630
- }, { status: 402, headers: corsHeaders });
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
- if (tarballFile.size > 52428800) {
101990
- return Response.json({ error: "Tarball exceeds maximum size of 50MB" }, { status: 413, headers: corsHeaders });
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
- return pkg.publishedBy === user.email;
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 updated = await registry.updatePublisherPackage(name, user.email, {
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 renderSitePage("package.stx", {
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 renderSitePage("package.stx", {
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 renderSitePage("package.stx", {
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 renderSitePage("package.stx", {
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 renderSitePage("package.stx", {
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 renderSitePage("package.stx", {
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,