console: signup mints NO credit — $0 until granted/paid (kill auto welcome-grant)

Removed the server-side signup auto-grant: app/auth/signup/route.ts no longer
calls grantWelcomeCredit (→ commerce /v1/billing/grant-starter). Deleted the now-
dead lib/server/billing-grant.ts helper + its test (no residual). Together with
the session-bootstrap claimWelcomeGrantOnce removal (2fe3e197a), the console no
longer auto-grants any credit on signup or load. A new account starts at $0;
credit comes only from an admin grant (admin.hanzo.ai) or the user adding funds.
This commit is contained in:
z
2026-07-16 00:28:38 -07:00
parent 45d07c1235
commit 31ae930438
2 changed files with 0 additions and 113 deletions
-59
View File
@@ -1,59 +0,0 @@
import { describe, it, expect, beforeEach, vi } from 'vitest'
/**
* The signup-time welcome grant — the WRITE-path onboarding paywall fix. Proves it
* POSTs commerce's idempotent `grant-starter` with the SERVICE bearer + `X-Org-Id` +
* the personal-org subject body, no-ops honestly when unconfigured, and SWALLOWS any
* failure so a grant hiccup never blocks signup (the read-path self-heal re-lands it).
*/
const { fetchWithTimeout, baseUrl, token } = vi.hoisted(() => ({
fetchWithTimeout: vi.fn(),
baseUrl: vi.fn(() => 'http://commerce.test'),
token: vi.fn(() => 'svc-token'),
}))
vi.mock('./fetch-timeout', () => ({ fetchWithTimeout }))
vi.mock('./billing-proxy', () => ({ commerceBaseUrl: baseUrl, commerceServiceToken: token }))
import { grantWelcomeCredit } from './billing-grant'
describe('grantWelcomeCredit (server-to-server starter grant)', () => {
beforeEach(() => {
fetchWithTimeout.mockReset()
baseUrl.mockReturnValue('http://commerce.test')
token.mockReturnValue('svc-token')
})
it('POSTs grant-starter with the service bearer, X-Org-Id, and subject body', async () => {
fetchWithTimeout.mockResolvedValue({ ok: true })
const ok = await grantWelcomeCredit('acme-personal')
expect(ok).toBe(true)
expect(fetchWithTimeout).toHaveBeenCalledTimes(1)
const [url, init] = fetchWithTimeout.mock.calls[0] as [string, RequestInit & { headers: Record<string, string> }]
expect(url).toBe('http://commerce.test/v1/billing/grant-starter')
expect(init.method).toBe('POST')
expect(init.headers.Authorization).toBe('Bearer svc-token')
expect(init.headers['X-Org-Id']).toBe('acme-personal')
expect(JSON.parse(init.body as string)).toEqual({ user: 'acme-personal', trigger: 'console_signup' })
})
it('is a no-op (false) when the service token is unset', async () => {
token.mockReturnValue('')
expect(await grantWelcomeCredit('acme')).toBe(false)
expect(fetchWithTimeout).not.toHaveBeenCalled()
})
it('is a no-op (false) for an empty org slug', async () => {
expect(await grantWelcomeCredit('')).toBe(false)
expect(fetchWithTimeout).not.toHaveBeenCalled()
})
it('swallows a non-ok commerce response (returns false, never throws)', async () => {
fetchWithTimeout.mockResolvedValue({ ok: false })
await expect(grantWelcomeCredit('acme')).resolves.toBe(false)
})
it('swallows a network failure (returns false, never throws)', async () => {
fetchWithTimeout.mockRejectedValue(new Error('commerce down'))
await expect(grantWelcomeCredit('acme')).resolves.toBe(false)
})
})
-54
View File
@@ -1,54 +0,0 @@
/**
* Server-to-server WELCOME GRANT — the onboarding paywall fix on the WRITE path.
*
* A brand-new self-serve signup lands in its OWN personal org with a $0 balance and
* 402s on the first chat. This grants the one-time $5 trial credit at signup, via the
* DESIGNED trusted-service path: commerce's idempotent, tag-deduped
* `POST /v1/billing/grant-starter`, authenticated with the COMMERCE SERVICE TOKEN the
* console already holds (the same credential + base URL the per-tenant billing proxy
* uses — DRY, `billing-proxy.ts`).
*
* Why NOT a user-bound bearer here: the fresh account lives in its own personal org,
* which the confidential `hanzo-console` client (org `hanzo`) cannot resolve — an
* `issue-user-token`/password-grant for it fails ("the user does not exist"). The
* service-token path is exactly "a trusted service grants on signup" and needs no user
* session. For the subject, a personal org's billing key IS the org slug
* (commerce `BillingSubject` = org.Name), so `X-Org-Id` + `user=<orgSlug>` target it.
*
* BEST-EFFORT: never throws, never blocks signup. The grant is idempotent (tag-deduped
* per subject), so a transient failure is harmless — the read-path self-heal
* (`/v1/billing/me/welcome` on first authenticated load) re-lands it. Returns true only
* when commerce accepted the call.
*/
import { commerceBaseUrl, commerceServiceToken } from './billing-proxy'
import { fetchWithTimeout } from './fetch-timeout'
/**
* Best-effort $5 welcome/starter grant for a freshly created personal org. No-op
* (returns false) when the commerce service token is unwired — the deployment then
* relies purely on the read-path self-heal.
*/
export async function grantWelcomeCredit(orgSlug: string): Promise<boolean> {
const token = commerceServiceToken()
if (!token || !orgSlug) return false
try {
const res = await fetchWithTimeout(`${commerceBaseUrl()}/v1/billing/grant-starter`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'X-Org-Id': orgSlug,
'Content-Type': 'application/json',
Accept: 'application/json',
},
// Subject == the personal org slug; `trigger` tags the grant for the idempotency
// dedupe + the audit trail.
body: JSON.stringify({ user: orgSlug, trigger: 'console_signup' }),
cache: 'no-store',
})
return res.ok
} catch (e) {
// Redact the exception (it carries the internal commerce host) — log server-side only.
console.error('welcome-grant: grant-starter failed for', orgSlug, e instanceof Error ? e.message : String(e))
return false
}
}