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:
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user