terminalhire 0.6.0 → 0.7.1

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.
@@ -2268,6 +2268,51 @@ var init_workable = __esm({
2268
2268
  }
2269
2269
  });
2270
2270
 
2271
+ // ../../packages/core/src/feeds/directory.ts
2272
+ function personCardToJob(row) {
2273
+ const tags = [...row.skill_tags];
2274
+ return {
2275
+ id: `dev:${row.login}`,
2276
+ source: "person",
2277
+ title: row.name ?? row.login,
2278
+ company: row.login,
2279
+ url: `/r/${row.login}`,
2280
+ remote: true,
2281
+ tags,
2282
+ coreTags: tags.slice(0, TOP_CORE_TAGS),
2283
+ roleType: "full_time",
2284
+ applyMode: "direct"
2285
+ };
2286
+ }
2287
+ function buildDirectoryIndex(people, opts) {
2288
+ return {
2289
+ builtAt: opts?.builtAt ?? (/* @__PURE__ */ new Date()).toISOString(),
2290
+ cards: people.map(personCardToJob)
2291
+ };
2292
+ }
2293
+ function projectCardToJob(row) {
2294
+ const tags = [...row.needed_skills];
2295
+ return {
2296
+ id: `proj:${row.id}`,
2297
+ source: "project",
2298
+ title: row.title,
2299
+ company: row.owner_login,
2300
+ url: `/r/${row.owner_login}`,
2301
+ remote: true,
2302
+ tags,
2303
+ coreTags: tags.slice(0, TOP_CORE_TAGS),
2304
+ roleType: "full_time",
2305
+ applyMode: "direct"
2306
+ };
2307
+ }
2308
+ var TOP_CORE_TAGS;
2309
+ var init_directory = __esm({
2310
+ "../../packages/core/src/feeds/directory.ts"() {
2311
+ "use strict";
2312
+ TOP_CORE_TAGS = 4;
2313
+ }
2314
+ });
2315
+
2271
2316
  // ../../packages/core/src/feeds/index.ts
2272
2317
  async function aggregateBounties(opts) {
2273
2318
  const [gh, op] = await Promise.all([
@@ -2384,6 +2429,7 @@ var init_feeds = __esm({
2384
2429
  init_github_bounties();
2385
2430
  init_opire();
2386
2431
  init_workable();
2432
+ init_directory();
2387
2433
  init_bounty_gate();
2388
2434
  init_bounty_gate();
2389
2435
  FEEDS = [greenhouse, ashby, lever, workable, himalayas, wwr, hn];
@@ -2584,6 +2630,176 @@ var init_indexer = __esm({
2584
2630
  }
2585
2631
  });
2586
2632
 
2633
+ // ../../packages/core/src/intro.ts
2634
+ function buildIntroPayload(input) {
2635
+ const payload = {
2636
+ requesterLogin: input.requesterLogin,
2637
+ requesterDisplayName: input.requesterDisplayName,
2638
+ requesterContact: input.requesterContact,
2639
+ targetLogin: input.targetLogin
2640
+ };
2641
+ const note = input.note?.trim();
2642
+ if (note) payload.note = note;
2643
+ return payload;
2644
+ }
2645
+ function rejectExtraIntroFields(body) {
2646
+ for (const key of Object.keys(body)) {
2647
+ if (!INTRO_ALLOWED_SET.has(key)) {
2648
+ return `intro payload contains disallowed field: "${key}"`;
2649
+ }
2650
+ }
2651
+ return null;
2652
+ }
2653
+ function validateIntroPayload(body) {
2654
+ if (typeof body !== "object" || body === null || Array.isArray(body)) {
2655
+ return { ok: false, reason: "intro payload must be a JSON object" };
2656
+ }
2657
+ const b = body;
2658
+ const extra = rejectExtraIntroFields(b);
2659
+ if (extra) return { ok: false, reason: extra };
2660
+ const ok = (v, max) => typeof v === "string" && v.trim().length > 0 && v.length <= max;
2661
+ if (!ok(b.requesterLogin, MAX_SHORT)) return { ok: false, reason: "requesterLogin is required" };
2662
+ if (!ok(b.requesterDisplayName, MAX_SHORT)) return { ok: false, reason: "requesterDisplayName is required" };
2663
+ if (!ok(b.requesterContact, MAX_SHORT)) return { ok: false, reason: "requesterContact is required" };
2664
+ if (!ok(b.targetLogin, MAX_SHORT)) return { ok: false, reason: "targetLogin is required" };
2665
+ if (b.note !== void 0 && (typeof b.note !== "string" || b.note.length > MAX_NOTE)) {
2666
+ return { ok: false, reason: "note must be a string of at most 500 chars" };
2667
+ }
2668
+ const value = {
2669
+ requesterLogin: b.requesterLogin.trim(),
2670
+ requesterDisplayName: b.requesterDisplayName.trim(),
2671
+ requesterContact: b.requesterContact.trim(),
2672
+ targetLogin: b.targetLogin.trim()
2673
+ };
2674
+ const note = typeof b.note === "string" ? b.note.trim() : "";
2675
+ if (note) value.note = note;
2676
+ return { ok: true, value };
2677
+ }
2678
+ function introRateLimitCheck(history, now, opts) {
2679
+ const cutoff = now - opts.windowMs;
2680
+ const recent = history.filter((t) => t > cutoff);
2681
+ if (recent.length >= opts.max) {
2682
+ const oldest = recent[0] ?? now;
2683
+ return { allowed: false, retained: recent, retryAfterMs: Math.max(0, oldest + opts.windowMs - now) };
2684
+ }
2685
+ return { allowed: true, retained: [...recent, now], retryAfterMs: 0 };
2686
+ }
2687
+ function isOverIntroLimit(recentCount, max) {
2688
+ return recentCount >= max;
2689
+ }
2690
+ function composeIntroEmail(args2) {
2691
+ const subject = `New intro request from @${args2.requesterLogin} \xB7 terminalhire`;
2692
+ const text = `@${args2.requesterLogin} wants an intro to you on terminalhire.
2693
+
2694
+ Sign in to view the request and choose whether to share your contact back:
2695
+ ${args2.dashboardUrl}
2696
+
2697
+ You control whether this connects \u2014 no contact details are shared unless you accept.
2698
+
2699
+ \u2014 Terminalhire`;
2700
+ return { subject, text };
2701
+ }
2702
+ function introActorRole(intro, actorLogin) {
2703
+ const a = actorLogin.trim().toLowerCase();
2704
+ if (a && a === intro.targetLogin.trim().toLowerCase()) return "target";
2705
+ if (a && a === intro.requesterLogin.trim().toLowerCase()) return "requester";
2706
+ return "other";
2707
+ }
2708
+ function authorizeIntroDecision(intro, actorLogin) {
2709
+ const role = introActorRole(intro, actorLogin);
2710
+ if (role === "target") return { ok: true };
2711
+ if (role === "requester") {
2712
+ return { ok: false, status: 403, reason: "the requester cannot accept or decline their own intro request" };
2713
+ }
2714
+ return { ok: false, status: 404, reason: "intro not found" };
2715
+ }
2716
+ function authorizeIntroDeletion(intro, actorLogin) {
2717
+ const role = introActorRole(intro, actorLogin);
2718
+ if (role === "other") return { ok: false, status: 404, reason: "intro not found" };
2719
+ return { ok: true };
2720
+ }
2721
+ function revealIntroContacts(intro) {
2722
+ if (intro.status !== "accepted") return { toRequester: null, toTarget: null };
2723
+ return { toRequester: intro.targetContact ?? null, toTarget: intro.requesterContact };
2724
+ }
2725
+ function validateTargetContact(v) {
2726
+ if (typeof v !== "string" || v.trim().length === 0) return { ok: false, reason: "targetContact is required" };
2727
+ if (v.length > MAX_SHORT) return { ok: false, reason: `targetContact must be at most ${MAX_SHORT} chars` };
2728
+ return { ok: true, value: v.trim() };
2729
+ }
2730
+ function buildIntroListItem(intro, viewerLogin) {
2731
+ const role = introActorRole(intro, viewerLogin);
2732
+ if (role === "other") return null;
2733
+ const reveal = revealIntroContacts(intro);
2734
+ if (role === "target") {
2735
+ return {
2736
+ id: intro.id,
2737
+ role: "incoming",
2738
+ counterpartyLogin: intro.requesterLogin,
2739
+ status: intro.status,
2740
+ note: intro.note ?? null,
2741
+ contact: reveal.toTarget
2742
+ };
2743
+ }
2744
+ return {
2745
+ id: intro.id,
2746
+ role: "outgoing",
2747
+ counterpartyLogin: intro.targetLogin,
2748
+ status: intro.status,
2749
+ note: intro.note ?? null,
2750
+ contact: reveal.toRequester
2751
+ };
2752
+ }
2753
+ function composeIntroAcceptedEmail(args2) {
2754
+ const subject = `Intro connected with @${args2.counterpartyLogin} \xB7 terminalhire`;
2755
+ const lead = args2.recipientRole === "requester" ? `@${args2.counterpartyLogin} accepted your intro request on terminalhire.` : `You accepted @${args2.counterpartyLogin}'s intro request on terminalhire.`;
2756
+ const text = `${lead}
2757
+
2758
+ You can now reach them directly:
2759
+ @${args2.counterpartyLogin} \u2014 ${args2.counterpartyContact}
2760
+
2761
+ Take it from here.
2762
+
2763
+ \u2014 Terminalhire`;
2764
+ return { subject, text };
2765
+ }
2766
+ function introRetentionAction(row, now) {
2767
+ if (row.status === "pending") {
2768
+ const created = Date.parse(row.createdAt);
2769
+ if (Number.isFinite(created) && now - created > INTRO_PENDING_TTL_MS) return "purge";
2770
+ return "keep";
2771
+ }
2772
+ if (row.status === "declined") {
2773
+ return row.hasContact ? "scrub-declined" : "keep";
2774
+ }
2775
+ if (row.status === "accepted") {
2776
+ const updated = Date.parse(row.updatedAt);
2777
+ if (row.hasContact && Number.isFinite(updated) && now - updated > INTRO_ACCEPTED_TTL_MS) {
2778
+ return "expire-accepted";
2779
+ }
2780
+ return "keep";
2781
+ }
2782
+ return "keep";
2783
+ }
2784
+ var INTRO_ALLOWED_FIELDS, INTRO_ALLOWED_SET, MAX_SHORT, MAX_NOTE, INTRO_PENDING_TTL_MS, INTRO_ACCEPTED_TTL_MS;
2785
+ var init_intro = __esm({
2786
+ "../../packages/core/src/intro.ts"() {
2787
+ "use strict";
2788
+ INTRO_ALLOWED_FIELDS = [
2789
+ "requesterLogin",
2790
+ "requesterDisplayName",
2791
+ "requesterContact",
2792
+ "note",
2793
+ "targetLogin"
2794
+ ];
2795
+ INTRO_ALLOWED_SET = new Set(INTRO_ALLOWED_FIELDS);
2796
+ MAX_SHORT = 200;
2797
+ MAX_NOTE = 500;
2798
+ INTRO_PENDING_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
2799
+ INTRO_ACCEPTED_TTL_MS = 365 * 24 * 60 * 60 * 1e3;
2800
+ }
2801
+ });
2802
+
2587
2803
  // ../../packages/core/src/index.ts
2588
2804
  var src_exports = {};
2589
2805
  __export(src_exports, {
@@ -2599,6 +2815,9 @@ __export(src_exports, {
2599
2815
  GRAPH: () => GRAPH,
2600
2816
  GREENHOUSE_SLUGS_BY_TIER: () => GREENHOUSE_SLUGS_BY_TIER,
2601
2817
  IDF_BACKGROUND: () => IDF_BACKGROUND,
2818
+ INTRO_ACCEPTED_TTL_MS: () => INTRO_ACCEPTED_TTL_MS,
2819
+ INTRO_ALLOWED_FIELDS: () => INTRO_ALLOWED_FIELDS,
2820
+ INTRO_PENDING_TTL_MS: () => INTRO_PENDING_TTL_MS,
2602
2821
  LEVER_SLUGS_BY_TIER: () => LEVER_SLUGS_BY_TIER,
2603
2822
  SYNONYMS: () => SYNONYMS,
2604
2823
  VOCABULARY: () => VOCABULARY,
@@ -2607,10 +2826,17 @@ __export(src_exports, {
2607
2826
  aggregate: () => aggregate,
2608
2827
  aggregateBounties: () => aggregateBounties,
2609
2828
  ashby: () => ashby,
2829
+ authorizeIntroDecision: () => authorizeIntroDecision,
2830
+ authorizeIntroDeletion: () => authorizeIntroDeletion,
2610
2831
  bestAcceptanceDomain: () => bestAcceptanceDomain,
2832
+ buildDirectoryIndex: () => buildDirectoryIndex,
2611
2833
  buildGraph: () => buildGraph,
2612
2834
  buildIndex: () => buildIndex,
2835
+ buildIntroListItem: () => buildIntroListItem,
2836
+ buildIntroPayload: () => buildIntroPayload,
2613
2837
  buildReason: () => buildReason,
2838
+ composeIntroAcceptedEmail: () => composeIntroAcceptedEmail,
2839
+ composeIntroEmail: () => composeIntroEmail,
2614
2840
  computeAcceptanceCredential: () => computeAcceptanceCredential,
2615
2841
  computeAcceptanceCredentialPublic: () => computeAcceptanceCredentialPublic,
2616
2842
  coreTagsFromTitle: () => coreTagsFromTitle,
@@ -2627,7 +2853,11 @@ __export(src_exports, {
2627
2853
  greenhouse: () => greenhouse,
2628
2854
  himalayas: () => himalayas,
2629
2855
  hn: () => hn,
2856
+ introActorRole: () => introActorRole,
2857
+ introRateLimitCheck: () => introRateLimitCheck,
2858
+ introRetentionAction: () => introRetentionAction,
2630
2859
  isBounty: () => isBounty,
2860
+ isOverIntroLimit: () => isOverIntroLimit,
2631
2861
  lever: () => lever,
2632
2862
  loadPartnerRoles: () => loadPartnerRoles,
2633
2863
  looksLikeEngRole: () => looksLikeEngRole,
@@ -2635,8 +2865,14 @@ __export(src_exports, {
2635
2865
  normalize: () => normalize,
2636
2866
  opire: () => opire,
2637
2867
  passesMaturityGate: () => passesMaturityGate,
2868
+ personCardToJob: () => personCardToJob,
2869
+ projectCardToJob: () => projectCardToJob,
2870
+ rejectExtraIntroFields: () => rejectExtraIntroFields,
2871
+ revealIntroContacts: () => revealIntroContacts,
2638
2872
  tokenize: () => tokenize,
2639
2873
  validateGraph: () => validateGraph,
2874
+ validateIntroPayload: () => validateIntroPayload,
2875
+ validateTargetContact: () => validateTargetContact,
2640
2876
  workable: () => workable,
2641
2877
  wwr: () => wwr
2642
2878
  });
@@ -2650,6 +2886,7 @@ var init_src = __esm({
2650
2886
  init_indexer();
2651
2887
  init_partners();
2652
2888
  init_github();
2889
+ init_intro();
2653
2890
  }
2654
2891
  });
2655
2892