diff --git a/public/banners/vetting-an-open-source-dependency-before-you-bet-on-it.png b/public/banners/vetting-an-open-source-dependency-before-you-bet-on-it.png new file mode 100644 index 0000000..7881543 Binary files /dev/null and b/public/banners/vetting-an-open-source-dependency-before-you-bet-on-it.png differ diff --git a/public/og/vetting-an-open-source-dependency-before-you-bet-on-it.png b/public/og/vetting-an-open-source-dependency-before-you-bet-on-it.png new file mode 100644 index 0000000..c8784a7 Binary files /dev/null and b/public/og/vetting-an-open-source-dependency-before-you-bet-on-it.png differ diff --git a/scripts/banner-gen/generate.mjs b/scripts/banner-gen/generate.mjs index 9a904c9..c997908 100644 --- a/scripts/banner-gen/generate.mjs +++ b/scripts/banner-gen/generate.mjs @@ -706,6 +706,68 @@ BANNERS['adding-english-mode-to-a-chinese-only-web-app'] = { ], }; +BANNERS['vetting-an-open-source-dependency-before-you-bet-on-it'] = { + titlebar: 'root@dsm — due diligence · 6 checks', + lines: [ + { t: 'prompt', text: '$' }, { t: 'cmd', text: 'curl -s api.github.com/repos/getcoherence/openpartner' }, + { t: 'dim', text: 'MIT · created 2026-04 · 8 stars · 15 open issues' }, + { t: 'prompt', text: '$' }, { t: 'cmd', text: "grep -in 'oidc\\|sso\\|saml' README.md ARCHITECTURE.md docs/*.md" }, + { t: 'err', text: '0 hits ← the spec assumed OIDC for partner login' }, + { t: 'hl', text: 'Community: API tokens ✓ · SSO ✗ (Enterprise only)' }, + { t: 'prompt', text: '$' }, { t: 'cmd', text: 'selfhost mode · manual payouts · pin the commit' }, + { t: 'ok', text: '→ 6 assumptions corrected before integration ✓' }, + ], + flow: [ + { n: '1', label: '1,306-line spec' }, + { n: '2', label: 'repo vital signs' }, + { n: '3', label: 'grep the feature' }, + { n: '4', label: 'tier + rail fit' }, + { n: '5', label: 'fallback decided ✓' }, + ], +}; + +BANNERS['authentik-forward-auth-gate-wasnt-live'] = { + titlebar: 'root@dsm — forward-auth · two ingress layers', + lines: [ + { t: 'prompt', text: '$' }, { t: 'cmd', text: "curl -sk -H 'Host: app.example.com' https://localhost/" }, + { t: 'err', text: '302 → /outpost.goauthentik.io/start ← looks gated' }, + { t: 'prompt', text: '$' }, { t: 'cmd', text: 'curl -sI https://app.example.com/' }, + { t: 'err', text: 'x-powered-by: Express ← the app answered, not the outpost' }, + { t: 'hl', text: 'the tunnel rule went straight to the container — nginx never saw the request' }, + { t: 'dim', text: 'grep -c "proxy_pass :10000" → 11 vhosts share that port' }, + { t: 'cmd', text: 'verify from outside · read which layer replied' }, + { t: 'ok', text: '→ one line to revert · ten gates left intact ✓' }, + ], + flow: [ + { n: '1', label: 'vhost → outpost' }, + { n: '2', label: 'tunnel → app', err: true }, + { n: '3', label: 'x-powered-by told' }, + { n: '4', label: 'count the port' }, + { n: '5', label: 'gate verified ✓' }, + ], +}; + +BANNERS['nocodb-sso-is-a-licensed-feature'] = { + titlebar: 'root@dsm — nocodb CE 2026.09.0', + lines: [ + { t: 'prompt', text: '$' }, { t: 'cmd', text: 'NC_SSO=oidc NC_SSO_OIDC_ISSUER=https://auth…' }, + { t: 'dim', text: 'enforced at boot — the vars are wired in, not vestigial' }, + { t: 'prompt', text: '$' }, { t: 'cmd', text: 'docker run --name nc-sso-test -p 10399:8080 (no NC_DB → SQLite)' }, + { t: 'err', text: 'curl /auth/oidc → ### UNCAUGHT EXCEPTION ### exits(1)' }, + { t: 'hl', text: 'one unauthenticated GET is enough to stop the instance' }, + { t: 'dim', text: 'CE mode · meta=MySQL (licensing needs Postgres) · fork 0.255.2' }, + { t: 'prompt', text: '$' }, { t: 'cmd', text: 'gate at the edge instead · prod container never touched' }, + { t: 'ok', text: '→ OIDC SSO is Business+ · one sentence, not an afternoon ✓' }, + ], + flow: [ + { n: '1', label: 'NC_SSO vars', err: true }, + { n: '2', label: 'throwaway box' }, + { n: '3', label: '/auth/oidc dies' }, + { n: '4', label: 'licence + Postgres' }, + { n: '5', label: 'edge gate ✓' }, + ], +}; + // ---------- read frontmatter ---------- const postPath = join(ROOT, 'src', 'content', 'posts', `${slug}.md`); let category = 'devops'; diff --git a/scripts/og-gen/generate.mjs b/scripts/og-gen/generate.mjs index 4586a27..99c3dfa 100644 --- a/scripts/og-gen/generate.mjs +++ b/scripts/og-gen/generate.mjs @@ -219,6 +219,24 @@ TERMINALS['adding-english-mode-to-a-chinese-only-web-app'] = `
 the UI is 100% Chinese · browsers then never offer translate
$nginx sub_filter + gate-en.js · 871 zh→en labels→ 88% English ✓
`; +TERMINALS['vetting-an-open-source-dependency-before-you-bet-on-it'] = ` +
$grep -in 'oidc\|sso\|saml' README.md ARCHITECTURE.md docs/*.md→ 0 hits
+
 the spec assumed OIDC · partner login was never implementable
+
$head -5 docs/white-label-custom-domains.md→ Target branch: multi-tenant
+
$selfhost mode · manual payouts · pin the commit→ 6 assumptions corrected ✓
`; + +TERMINALS['authentik-forward-auth-gate-wasnt-live'] = ` +
$curl -sk -H 'Host: app.example.com' https://localhost/→ 302 gated ✓
+
$curl -sI https://app.example.com/→ 200 x-powered-by: Express
+
 the tunnel rule answered · nginx was never in the path
+
$verify from outside · read which layer replied→ gate real ✓
`; + +TERMINALS['nocodb-sso-is-a-licensed-feature'] = ` +
$NC_SSO=oidc · app boot→ env keys enforced
+
$curl -s localhost:10399/auth/oidc→ uncaught TypeError
+
 unhandledRejection · container exits(1)
+
$CE mode · meta=MySQL · fork 0.255.2 (2024)→ gate at the edge ✓
`; + // ---------- read frontmatter ---------- const postPath = join(ROOT, 'src', 'content', 'posts', `${slug}.md`); if (!existsSync(postPath)) { diff --git a/src/content/posts/vetting-an-open-source-dependency-before-you-bet-on-it.md b/src/content/posts/vetting-an-open-source-dependency-before-you-bet-on-it.md new file mode 100644 index 0000000..c61cdfc --- /dev/null +++ b/src/content/posts/vetting-an-open-source-dependency-before-you-bet-on-it.md @@ -0,0 +1,177 @@ +--- +title: "How I Vet an Open-Source Dependency Before Betting On It" +description: "Six checks that catch a repo which doesn't do what its docs claim — grep for the feature, read the doc's target branch, check the tier gate and the payment rail." +pubDate: 2026-09-29 +category: devops +tags: ["open-source", "due-diligence", "oidc", "self-hosting", "docker"] +ogImage: "/og/vetting-an-open-source-dependency-before-you-bet-on-it.png" +banner: "/banners/vetting-an-open-source-dependency-before-you-bet-on-it.png" +draft: false +--- + +I was handed a 1,300-line architecture spec for a partner program. It read like a +plan: consistent, cross-referenced, confident. It named a self-hosted +open-source engine as the attribution and commission layer, and it assumed three +things about that engine — that it spoke OIDC so my existing identity provider +could log partners in, that its commission rules covered the five shapes the +business needed, and that its white-label feature was shipped. + +Ten minutes of `curl` and `grep` later, two of those were false and the third was +half-true. None of that is unusual. The README describes the *product*; the +repository describes the *software*. The gap between those two is exactly where +integration projects die — and it is cheap to measure before you write any code. + +## The problem, phrased as the question I'd actually search for + +**How do I know an open-source project does what its documentation claims before +I build on it?** + +A spec is a claim *about other software*. The more polished the spec, the more its +assumptions read like facts — this one cited sources, defined stable IDs, and had +a source-of-truth matrix. What it never did was check whether the engine down the +chain implemented the features being assigned to it. So before installing +anything, I went looking for those specific features. + +## What I tried first, and why it wasn't enough + +**1. Read the README end to end.** It sells the pipeline well: +`click → identity → event → attribution → commission → payout`. It never once +says which authentication protocol the app speaks. For a feature you *need*, +silence isn't neutral — but an absent mention is easy to skim past when the rest +of the page is convincing. + +**2. Searched the web.** Several of my queries returned nothing usable. The +project is a few months old and its SEO surface is thin. For software that's +fine — the source *is* the documentation. It just means the reading has to happen +in the repo, not in a blog post. + +**3. Found a 78 KB feature design doc and assumed the feature shipped.** It was +labelled "FINAL, post-review", listed three reviewers, and carried a date. I took +that as proof. That was the worst inference of the three, and the easiest to check. + +## The six checks that actually settled it + +### 1. Read the repo's vital signs before any feature claim + +```bash +curl -s -H "User-Agent: hermes" https://api.github.com/repos// \ + | grep -E '"created_at"|"pushed_at"|"stargazers_count"|"forks_count"|"open_issues_count"|"archived"|"spdx_id"' +``` + +What came back: MIT, created 2026-04-23, last push 2026-09-10, 8 stars, 3 forks, +15 open issues, not archived. Read the same as: **real, still moving, and very +small** — one vendor, which also sells a hosted tier. Now I know how much of my +business I want resting on it *before* I start grading its features. + +### 2. Grep for the feature instead of reading for it + +```bash +grep -in 'oidc\|sso\|saml' README.md ARCHITECTURE.md docs/*.md | wc -l +``` + +Zero hits. The app authenticates with its own magic-link sessions and +per-persona API keys; it does not speak OIDC at all. That one line of output +killed a phase of the plan — "put the portal behind our SSO provider" — which +would otherwise have surfaced weeks later, mid-integration, with a partner +already invited. Reading for a feature you can't find is how you lose an +afternoon; grepping makes the absence loud. + +### 3. Read the target branch line at the top of a feature doc + +```bash +head -5 docs/white-label-custom-domains.md +``` + +`Target branch: multi-tenant`. The doc is a design for a branch that is not the +default branch — it describes work in flight, so the feature is not in the code +you would install. A finished-looking document with reviewers and dates can +document something unshipped. That isn't dishonesty; it's what design docs are. + +### 4. Read the data model, not the feature list + +The commission section named exactly two rule types — `percent` (with a recurring +flag) and `fixed`. The business spec had enumerated five. That gap isn't a bug; +it's a modelling job I'd have to do in configuration instead of expecting from +the product: + +| Spec assumed | What the engine actually has | +|---|---| +| first-order bonus | a `fixed` rule on the first event | +| event-specific rules (`lead` vs `invoice_paid`) | one rule per campaign — so: two campaigns | +| recurring commission | `percent` + `recurring: true` | +| percentage | `percent` ✅ | +| fixed | `fixed` ✅ | + +Two rule primitives, five behaviours — workable, but only if you design for it up +front instead of discovering it during implementation. + +### 5. Check which paid tier gates the feature you need + +For the database tool in the same stack, the edition table was blunt: + +| Feature I needed | Community (free) | Enterprise (paid) | +|---|---|---| +| API tokens, so I can automate it | ✅ | ✅ | +| SSO / OIDC | ❌ | ✅ | +| Page designer, hide branding, 2FA | ❌ | ✅ | + +The thing I wanted for the partner-facing door (SSO) sits behind the commercial +gate, while the thing I wanted for automation (an API token) is free. Knowing +which side of the wall each requirement falls on changes the design — before +you've designed around it. + +### 6. Check that the money rail works in your country + +The payout model was `stripe_connect | manual`, and self-hosted mode was +documented as "operator manages out-of-band" — i.e. you move the money yourself. +Stripe Connect is not how Malaysian businesses pay partners, so that one line +decided the entire deployment shape: self-host mode, manual payouts, and event +ingestion pushed from my own order system rather than leaning on the provider's +payment integration. That line is never on a marketing page, and it mattered more +than any feature. + +### Then: ask the fallback question before you need it + +"What if the feature I need isn't there?" For the missing SSO the answer was: +don't put login in the app, put it in front of the app. authentik's proxy +provider exists to protect "applications that do not support native +authentication protocols such as OIDC, SAML, or LDAP", and it injects +`X-authentik-username` and `X-authentik-groups` headers upstream, so the reverse +proxy can do the gating. That turned "no SSO support" from a disqualification +into an afternoon of config. + +## What I'd do differently + +- **Write the requirement list first, then go look for it.** Five lines — the + features *I* need — and search for those. Starting from the README's feature + list means grading the software on the vendor's exam. +- **Treat "a doc exists" as different from "a feature ships".** Read the branch + line. +- **Grep, don't read.** Presence is cheap to prove; absence is the thing you're + hunting. +- **Decide the fallback before the decision**, so a missing feature becomes a + design change rather than a surprise. +- **Pin the commit.** This project's README says its API is "stable but + unversioned" and there is no OpenAPI description anywhere in the tree. There's + no contract to compile against, so the commit hash *is* the contract — the + endpoints I use go into my own docs, and upgrades get a regression pass. + +## The result + +Six assumptions corrected in about ten minutes of `curl` and `grep`, before a +single line of integration code existed. The project stayed viable — but +reshaped: no OIDC, manual payouts, a pinned commit, and a plan that no longer +depends on a feature sitting on an unmerged branch. + +The spec wasn't wrong to be optimistic. It was wrong to be unverified, and that's +the cheapest thing on the whole project to fix. + +--- + +**Building on a self-hosted stack, and want a second pair of eyes on whether +those features will actually be there when you integrate?** That's the kind of +review I do. + +[WhatsApp +60 12-797 2969](https://wa.me/60127972969) · +[me@hoelee.com](mailto:me@hoelee.com?subject=Open-source%20dependency%20review) · +[hoelee.com](https://hoelee.com) diff --git a/src/content/posts/zh/vetting-an-open-source-dependency-before-you-bet-on-it.md b/src/content/posts/zh/vetting-an-open-source-dependency-before-you-bet-on-it.md new file mode 100644 index 0000000..fdc3cfa --- /dev/null +++ b/src/content/posts/zh/vetting-an-open-source-dependency-before-you-bet-on-it.md @@ -0,0 +1,113 @@ +--- +title: "如何在押注前验证一个开源依赖:我用的六项检查" +description: "六项检查,专门抓出「文档说得漂亮、代码里其实没有」的仓库:用 grep 找功能、读文档头部标注的目标分支、看清付费档位门槛,以及收款通道在不在你的国家能用。" +pubDate: 2026-09-29 +category: devops +tags: ["open-source", "due-diligence", "oidc", "self-hosting", "docker"] +ogImage: "/og/vetting-an-open-source-dependency-before-you-bet-on-it.png" +banner: "/banners/vetting-an-open-source-dependency-before-you-bet-on-it.png" +draft: false +--- + +有人给我一份 1306 行的伙伴系统架构规格书。读起来像一份计划:前后一致、交叉引用、语气笃定。它指定一个自托管的开源引擎作为归因与佣金层,并且假定了那个引擎的三件事——它支持 OIDC,所以我现有的身份提供方能登录伙伴;它的佣金规则覆盖业务需要的五种形态;它的白标功能已经上线。 + +十分钟的 `curl` 加 `grep` 之后,三件里有两件是假的,第三件只对了一半。 + +这没什么稀奇。README 描述的是**产品**,仓库描述的是**软件**。这两者之间的落差,正是集成项目死掉的地方——而且在写任何代码之前,测量这个落差很便宜。 + +## 问题,写成我真的会去搜的那句话 + +**我怎么知道一个开源项目真的做了它文档宣称的事,然后才在它上面开始建东西?** + +规格书是**关于别的软件**的一种主张。规格书越漂亮,它的假设就越像事实——这一份引用了来源、定义了稳定 ID、还画了一张「真源矩阵」。它唯一没做的事,就是核对链条下游那个引擎到底有没有实现被指派给它的功能。所以在安装任何东西之前,我去找那几个具体功能。 + +## 我一开始试的,以及为什么不够 + +**一、把 README 从头读到尾。** 它把流水线卖得很好:`click → identity → event → attribution → commission → payout`。但它一次都没说这个应用讲哪种认证协议。对一个你**必须要有**的功能来说,沉默不是中立——只是当页面其他部分都很有说服力时,一个「没被提到」很容易被眼睛滑过去。 + +**二、上网搜。** 好几条查询都没返回能用的结果。这个项目才几个月大,SEO 面很薄。对软件来说这没关系——源码**就是**文档。只是意味着阅读必须在仓库里进行,而不是在博客文章里。 + +**三、看到一份 78 KB 的功能设计文档,就假定功能已上线。** 它标着「FINAL, post-review」,列了三位评审,还带着日期。我把它当成了证据。这是三个推断里最糟的一个,也是最容易核对的一个。 + +## 真正定案的六项检查 + +### 1. 先看仓库的生命体征,再看任何功能宣称 + +```bash +curl -s -H "User-Agent: hermes" https://api.github.com/repos// \ + | grep -E '"created_at"|"pushed_at"|"stargazers_count"|"forks_count"|"open_issues_count"|"archived"|"spdx_id"' +``` + +返回的是:MIT、创建于 2026-04-23、最后推送 2026-09-10、8 个 star、3 个 fork、15 个 open issue、未归档。翻译过来就是:**真实、还在动、但非常小**——单一厂商,而且它同时卖托管档。现在我在开始评它的功能之前,先知道了我愿意把多少生意压在它身上。 + +### 2. 用 grep 去找功能,而不是用读的 + +```bash +grep -in 'oidc\|sso\|saml' README.md ARCHITECTURE.md docs/*.md | wc -l +``` + +零命中。这个应用用的是它自己的 magic-link 会话和按身份签发的 API key;它根本不讲 OIDC。就这一行输出,砍掉了计划里的一个阶段——「把门户挂到我们 SSO 后面」——否则这一步会在几周后才浮现,而且是在集成到一半、伙伴已经被邀请进来的时候。**用读的方式去找一个找不到的功能,就是你失去一个下午的方式;grep 让「不存在」这件事变得响亮。** + +### 3. 读功能文档最上方那行「目标分支」 + +```bash +head -5 docs/white-label-custom-domains.md +``` + +`Target branch: multi-tenant`。这份文档描述的是**另一个分支**上的设计,不是你默认会安装的那个分支——它写的是在途工作,所以功能不在你会装下来的代码里。一份有评审、有日期、看起来已经完成的设计文档,完全可能描述着尚未上线的东西。这不是不诚实,设计文档本来就是这个样子。 + +### 4. 读数据模型,不要读功能清单 + +佣金那一节只列了两种规则类型——`percent`(带续期标记)与 `fixed`。业务规格书列了五种。这个落差不是 bug,而是我必须在**配置层**做的建模工作,不能指望产品自带: + +| 规格书假设 | 引擎实际有的 | +|---|---| +| 首单奖励 | 对首个事件用一条 `fixed` 规则 | +| 按事件区分(`lead` vs `invoice_paid`) | 一个 campaign 一条规则——所以:两个 campaign | +| 续期佣金 | `percent` + `recurring: true` | +| 百分比 | `percent` ✅ | +| 固定金额 | `fixed` ✅ | + +两种规则原语,五种行为——能做,但前提是你**一开始**就照这个设计,而不是在实现途中才发现。 + +### 5. 看清你需要的那项功能被锁在哪一档 + +同一个技术栈里还有个数据库工具,它的版本对照表很直白: + +| 我需要的功能 | Community(免费) | Enterprise(付费) | +|---|---|---| +| API token,好让我自动化 | ✅ | ✅ | +| SSO / OIDC | ❌ | ✅ | +| 页面设计器、隐藏品牌、双因素 | ❌ | ✅ | + +我想给「伙伴看到的那个门」用的东西(SSO)在商业门后面;而我想用来做自动化的东西(API token)是免费的。在围绕它做设计**之前**,先弄清每项需求落在墙的哪一侧。 + +### 6. 核对收款通道在你的国家能不能用 + +它的打款模型是 `stripe_connect | manual`,而自托管模式被写成「operator manages out-of-band」——也就是钱由你自己搬。Stripe Connect 不是马来西亚企业付钱给伙伴的方式,所以就是这一行决定了整个部署形态:自托管模式、手工打款、事件由我自己的订单系统推送过去,而不是依赖它内置的支付集成。这一行从来不会出现在营销页上,而它比任何功能都重要。 + +### 然后:在你需要之前,先问「兜底方案」 + +「如果我需要的功能不在,怎么办?」针对缺失的 SSO,答案是:不要把登录做进应用里,把登录放到应用**前面**。authentik 的 proxy provider 存在的意义就是保护「不支持 OIDC、SAML 或 LDAP 等原生认证协议的应用」,并且向上游注入 `X-authentik-username` 和 `X-authentik-groups` 头,于是反向代理就能承担准入判断。这就把「不支持 SSO」从一条否决理由,变成了一个下午的配置工作。 + +## 如果重来,我会怎么做 + +- **先写需求清单,再去找它。** 五行——**我**需要的功能——然后就搜这几个。从 README 的功能列表出发,等于用厂商的考卷给软件打分。 +- **把「文档存在」和「功能上线」当成两件不同的事。** 去读那行目标分支。 +- **用 grep,不要用读的。** 证明「有」很便宜;你要猎的是「没有」。 +- **在决定之前先定兜底**,这样一个缺失的功能就只是一次设计调整,而不是一场意外。 +- **把 commit 钉住。** 这个项目的 README 自己写着 API「stable but unversioned」,而且仓库里没有任何 OpenAPI 描述。没有可以编译期对齐的契约,那么 commit hash **就是**契约——我用到的端点要记进我自己的文档,升级前跑一遍回归。 + +## 结果 + +六个假设在大概十分钟的 `curl` 和 `grep` 里被修正,而当时一行集成代码都还没写。项目依然可行——只是换了形状:没有 OIDC、手工打款、钉住 commit,以及一份不再依赖某个未合并分支上功能的计划。 + +那份规格书乐观并没有错。它错在**没被验证过**——而这是整个项目里最便宜的修补项。 + +--- + +**你正在自托管技术栈上建东西,想在集成之前找人看一眼那些功能到底在不在?** 这正是我在做的复核工作。 + +[WhatsApp +60 12-797 2969](https://wa.me/60127972969) · +[me@hoelee.com](mailto:me@hoelee.com?subject=Open-source%20dependency%20review) · +[hoelee.com](https://hoelee.com)