Files
template-app/profiles/convex/backup/test-deployed-app.mjs
T

609 lines
29 KiB
JavaScript

// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// Local generated-app proof: deploy the actual functions to local Convex and exercise the
// server-only BFF identity contract plus atomic tenancy/notes mutations.
import assert from "node:assert/strict"
import { createObjectStore } from "@conusai-kit/platform"
import { ConvexHttpClient } from "convex/browser"
import { makeFunctionReference } from "convex/server"
const url = process.env.CONVEX_SELF_HOSTED_URL
const adminKey = process.env.CONVEX_SELF_HOSTED_ADMIN_KEY
if (!url || !adminKey) throw new Error("local app smoke needs the self-hosted Convex URL and admin key")
const ref = (name) => makeFunctionReference(name)
const client = new ConvexHttpClient(url)
client.setAdminAuth(adminKey)
const ready = await client.query(ref("system:ready"), {})
assert.equal(ready.ready, true)
assert.deepEqual(await client.action(ref("filesActions:readiness"), {}), { ready: true })
for (const name of ["auth", "files", "notes", "tenancy", "branding"])
assert(ready.modules.includes(name), `missing module ${name}`)
await assert.rejects(
() => client.mutation(ref("generated/tenancy:tenancy_org_create"), { input: { slug: "qa-org", name: "QA" } }),
(error) => error?.data?.code === "unauthenticated",
"unauthenticated requests must not run a public tenancy mutation",
)
const subject = `local-smoke-${Date.now()}`
const issuer = "https://conusai.internal/test"
// The BFF's first-login transaction (system:appUserResolve replaced system:userInsert).
const user = await client.mutation(ref("system:appUserResolve"), {
claims: {
issuer,
subject,
email: `${subject}@example.test`,
displayName: "Local smoke",
locale: "en",
},
create: true,
})
const identity = {
issuer: "https://conusai.internal/bff",
subject: user.id,
tokenIdentifier: `https://conusai.internal/bff|${user.id}`,
conusaiOrgId: undefined,
}
client.setAdminAuth(adminKey, identity)
const organization = await client.mutation(ref("generated/tenancy:tenancy_org_create"), {
input: { slug: "smoke-org", name: "Local smoke organization" },
})
const project = await client.mutation(ref("generated/tenancy:tenancy_project_create"), {
input: {
org: organization.slug,
workspace: organization.workspaceSlug,
slug: "smoke-project",
name: "Local smoke project",
moduleKey: "notes",
},
})
identity.conusaiOrgId = organization.orgId
client.setAdminAuth(adminKey, identity)
const brand = await client.mutation(ref("generated/branding:branding_brand_set"), {
input: { org: organization.slug, displayName: "Convex brand smoke", primary: "#1a1a8c", accent: "#f4c430" },
})
assert.equal(brand.displayName, "Convex brand smoke")
assert.equal(brand.primary, "#1a1a8c")
assert.deepEqual(
await client.query(ref("generated/branding:branding_brand_get"), {
input: { org: organization.slug },
}),
brand,
)
const scope = { org: organization.slug, workspace: organization.workspaceSlug, project: project.slug }
const note = await client.mutation(ref("generated/notes:notes_note_create"), {
input: { ...scope, title: "Convex generated-app smoke", body: "One atomic Convex mutation." },
})
const apiUrl = process.env.CONUSAI_API_URL
if (!apiUrl) throw new Error("local app smoke needs the fake platform API URL")
const blob = createObjectStore({
CONUSAI_API_URL: apiUrl,
CONUSAI_API_TOKEN: process.env.CONUSAI_API_TOKEN ?? "dev-token",
CONUSAI_PROJECT_ID: process.env.CONUSAI_PROJECT_ID ?? "1",
})
const bytes = new TextEncoder().encode("%PDF-1.4\nConvex local file action proof\n%%EOF\n")
const upload = await client.action(ref("filesActions:begin"), {
input: { ...scope, filename: "convex-action-proof.pdf", contentType: "application/pdf", size: bytes.length },
})
const partInfo = await client.query(ref("files:partInfo"), {
input: { ...scope, fileId: upload.fileId, part: 1 },
})
const sent = await blob.uploadContent(partInfo.uploadId, bytes, { part: 1, size: bytes.length })
const stored = await client.action(ref("filesActions:complete"), {
input: { ...scope, fileId: upload.fileId, parts: [{ number: 1, etag: sent.etag }] },
})
assert.equal(stored.size, bytes.length, "the action must finalize the bytes Blob actually accepted")
assert.equal(typeof stored.sha256, "string")
const firstRelay = await client.mutation(ref("relay:relayPass"), {})
assert.equal(firstRelay.failed, 0, "the Convex file event subscriber must enqueue its OCR job successfully")
let extracted
for (let i = 0; i < 60; i += 1) {
const listing = await client.query(ref("generated/documents:documents_document_list"), { input: scope })
extracted = listing.documents.find((document) => document.fileId === upload.fileId)
if (extracted?.status === "extracted") break
await new Promise((resolve) => setTimeout(resolve, 500))
}
assert.equal(extracted?.status, "extracted", "the stored-file event must dispatch the Convex OCR action")
assert(extracted?.pending >= 1, "the OCR action must persist review proposals")
const document = await client.query(ref("generated/documents:documents_document_get"), {
input: { ...scope, fileId: upload.fileId },
})
assert.equal(document.lines[0]?.quote, "Reference: QR-2026-04")
assert.equal(document.lines[0]?.status, "pending")
await client.mutation(ref("generated/proposals:proposals_proposal_approve"), {
input: {
...scope,
proposalId: document.lines[0].lineId,
expectedVersion: document.lines[0].expectedVersion,
},
})
const reviewed = await client.query(ref("generated/documents:documents_document_get"), {
input: { ...scope, fileId: upload.fileId },
})
assert.equal(reviewed.document.approved, 1, "a reviewed proposal must write an approved document line atomically")
assert.equal(reviewed.lines[0]?.status, "approved")
const attachment = await client.mutation(ref("generated/notes:notes_attachment_add"), {
input: { ...scope, noteId: note.noteId, fileId: upload.fileId },
})
const withAttachment = await client.query(ref("generated/notes:notes_note_get"), {
input: { ...scope, noteId: note.noteId },
})
assert.equal(withAttachment.attachmentsEnabled, true)
assert.equal(withAttachment.attachments[0]?.attachmentId, attachment.attachmentId)
await client.mutation(ref("generated/notes:notes_attachment_remove"), {
input: { ...scope, noteId: note.noteId, attachmentId: attachment.attachmentId },
})
await client.action(ref("filesActions:remove"), { input: { ...scope, fileId: upload.fileId } })
await assert.rejects(
() => client.query(ref("generated/files:files_file_get"), { input: { ...scope, fileId: upload.fileId } }),
/no such file/i,
"delete must remove the Blob object before finalizing the file row",
)
const listed = await client.query(ref("generated/notes:notes_note_list"), { input: scope })
assert(
listed.notes.some((item) => item.noteId === note.noteId),
"notes mutation must be readable through Convex",
)
const details = await client.query(ref("generated/notes:notes_note_get"), { input: { ...scope, noteId: note.noteId } })
assert.equal(details.attachmentsEnabled, true, "the composed file workflow enables notes attachments")
assert.equal(details.attachments.length, 0, "removing an attachment must detach the existing file link")
// Effect procedures of guides and support are generated Convex actions (convex/lib/effects.ts): the
// kernel resolves the member's scope and permission in an internal query, runs the nested
// `tenancy.me.get` as its generated query, and calls the platform through the module's port.
const effectState = async () => {
const response = await fetch(new URL("/__e2e/state", apiUrl), { headers: { "x-conusai-organization": "e2e" } })
return response.json()
}
// The widget calls these as a signed-in member: the BFF pins an organization only for an API key
// (convex-caller.ts), and a pinned credential has no user scope for the nested tenancy.me.get.
const memberIdentity = { ...identity }
delete memberIdentity.conusaiOrgId
client.setAdminAuth(adminKey, memberIdentity)
// A procedure without input, called the way the BFF calls it (input undefined, dropped on the wire).
const me = await client.query(ref("generated/tenancy:tenancy_me_get"), {})
assert(
me.organizations.some((entry) => entry.slug === organization.slug && entry.role === "owner"),
"tenancy.me.get answers a call without input",
)
// kit:guides:begin
const bootstrap = await client.action(ref("generated/guides:guides_runtime_bootstrap"), {
input: { org: organization.slug, path: "/notes", locale: "en" },
})
assert.deepEqual(bootstrap, { mode: "platform", etag: '"fake-guides"', guides: [] })
const eventId = globalThis.crypto.randomUUID()
const recorded = await client.action(ref("generated/guides:guides_runtime_record"), {
input: {
org: organization.slug,
idempotencyKey: "convex-smoke-batch",
events: [
{
schema_version: 1,
event_id: eventId,
type: "guide.started",
guide_key: "notes.first-note",
guide_version: 1,
expected_progress_revision: 0,
progress_epoch: 0,
occurred_at: new Date().toISOString(),
},
],
},
})
assert.equal(recorded.outcomes[0]?.event_id, eventId)
assert.equal(recorded.outcomes[0]?.status, "applied")
const guideCalls = (await effectState()).guides
const sentBootstrap = guideCalls.find((call) => call.route === "bootstrap")
assert.equal(sentBootstrap?.body?.identity?.subject, user.id, "the subject is the app's own user id")
assert.equal(
sentBootstrap?.body?.identity?.app_tenant,
organization.orgId,
"the app tenant is the resolved organization",
)
assert.equal(
sentBootstrap?.body?.context?.traits?.role,
"owner",
"the role trait came from the nested tenancy.me.get query",
)
assert(guideCalls.some((call) => call.route === "record" && call.body?.events?.[0]?.event_id === eventId))
// kit:guides:end
// kit:support:begin
assert.deepEqual(
await client.action(ref("generated/support:support_widget_config"), { input: { org: organization.slug } }),
{ mode: "platform", channel: "convex-smoke-channel" },
)
const supportSession = await client.action(ref("generated/support:support_widgetSession_mint"), {
input: { org: organization.slug },
})
assert(supportSession.token.startsWith("sws_"), "a minted support session")
const minted = (await effectState()).support.find((call) => call.route === "mint")
assert.equal(minted?.body?.subject, user.id)
assert.equal(minted?.body?.app_tenant, organization.orgId)
assert.equal(minted?.body?.display_name, "Local smoke", "the display name came from the nested tenancy.me.get query")
// kit:support:end
// A member of no organization: the action's kernel refuses before any platform call. The user is
// created with plain admin auth, as the first one was (system:appUserResolve is internal).
client.setAdminAuth(adminKey)
const stranger = await client.mutation(ref("system:appUserResolve"), {
claims: {
issuer,
subject: `${subject}-stranger`,
email: `${subject}-stranger@example.test`,
displayName: "Stranger",
locale: "en",
},
create: true,
})
const strangerIdentity = {
...identity,
subject: stranger.id,
tokenIdentifier: `https://conusai.internal/bff|${stranger.id}`,
}
delete strangerIdentity.conusaiOrgId
client.setAdminAuth(adminKey, strangerIdentity)
const callsBefore = await effectState()
const strangerCalls = []
strangerCalls.push(["generated/guides:guides_runtime_bootstrap", { org: organization.slug, path: "/notes" }]) // kit:guides
strangerCalls.push(["generated/support:support_widgetSession_mint", { org: organization.slug }]) // kit:support
for (const [reference, input] of strangerCalls) {
await assert.rejects(
() => client.action(ref(reference), { input }),
(error) => error?.data?.code === "not_found",
`${reference}: a stranger to the organization gets the not-found of a missing scope`,
)
}
const callsAfter = await effectState()
assert.equal(callsAfter.guides.length, callsBefore.guides.length, "no platform call for a stranger")
assert.equal(callsAfter.support.length, callsBefore.support.length, "no platform call for a stranger")
client.setAdminAuth(adminKey, identity)
identity.conusaiOrgId = "another-org-id"
client.setAdminAuth(adminKey, identity)
await assert.rejects(
() => client.query(ref("generated/branding:branding_brand_get"), { input: { org: organization.slug } }),
/scope|organization|tenant|membership|not found/i,
"branding must refuse the same mismatched organization identity as notes",
)
await assert.rejects(
() => client.query(ref("generated/notes:notes_note_list"), { input: scope }),
/scope|organization|tenant|membership|not found/i,
"a BFF identity pinned to another organization must not cross the tenant wall",
)
// kit:guides:begin
await assert.rejects(
() =>
client.action(ref("generated/guides:guides_runtime_bootstrap"), {
input: { org: organization.slug, path: "/notes" },
}),
(error) => error?.data?.code === "not_found",
"an effect action refuses an identity pinned to another organization",
)
// kit:guides:end
client.setAdminAuth(adminKey)
const relayResult = await client.mutation(ref("relay:relayPass"), {})
assert.equal(relayResult.failed, 0, "outbox subscribers must complete without failure")
const platformHeaders = {
authorization: "Bearer dev-token",
"x-conusai-organization": "fixture-org",
}
const auditHeaders = { ...platformHeaders, "x-conusai-organization": "e2e" }
const stateUrl = new URL("/__e2e/state", apiUrl)
const auditUrl = new URL("/_conusai/audit/events", apiUrl)
let delivered = false
let usageCount = 0
let auditCount = 0
for (let i = 0; i < 30; i += 1) {
const stateResponse = await fetch(stateUrl, { headers: platformHeaders })
const state = await stateResponse.json()
const auditResponse = await fetch(auditUrl, { headers: auditHeaders })
const audit = await auditResponse.json()
usageCount = state.usage?.length ?? 0
auditCount = audit.items?.length ?? 0
if (state.usage?.some((event) => event.meter === "notes_created") && audit.items?.length) {
delivered = true
break
}
await new Promise((resolve) => setTimeout(resolve, 500))
}
assert(
delivered,
`outbox relay must reach fake platform audit and usage sinks (usage=${usageCount}, audit=${auditCount})`,
)
// The account module on Convex (convex/account.ts): export my data and delete my account through the
// generated actions, with the ledger, the order and the partial-failure export of the Postgres profile.
// Every step is checked against the real backend's rows and what the fake platform received.
client.setAdminAuth(adminKey)
const outage = async (prefix, down) => {
const response = await fetch(new URL("/__e2e/outage", apiUrl), {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ prefix, down }),
})
assert.equal(response.status, 204, `the fake platform takes ${prefix} ${down ? "down" : "up"}`)
}
const storeRead = (method, ...args) => client.query(ref("account:storeRead"), { method, args })
const iso = (ms) => new Date(ms).toISOString()
// The identity of each person: under the test issuer, or -- in a Better Auth app, whose deletions
// refuse an identity under any other namespace (`provider-account:not_mapped`) and end the account
// at Better Auth -- a real Better Auth account under the app's namespace.
const accounts = { issuer, subjectFor: async (name) => `${subject}-${name}` }
// kit:convex-better-auth:begin
const betterAuthSite = process.env.SMOKE_BETTER_AUTH_SITE_URL
const namespace = process.env.SMOKE_AUTH_IDENTITY_NAMESPACE
const appOrigin = process.env.SMOKE_PUBLIC_URL
if (!betterAuthSite || !namespace || !appOrigin)
throw new Error("the Better Auth smoke needs its deployment's site URL")
const betterAuth = (path, body) =>
fetch(`${betterAuthSite}/api/auth${path}`, {
method: "POST",
headers: { "content-type": "application/json", origin: appOrigin },
body: JSON.stringify(body),
})
const password = `convex-smoke-${Date.now()}-password`
const signUpAt = async (name, email) => {
const signUp = await betterAuth("/sign-up/email", { name, email, password })
assert.equal(signUp.status, 200, "a person signs up at the Better Auth deployment")
return (await signUp.json()).user.id
}
accounts.issuer = namespace
accounts.subjectFor = (name) => signUpAt(name, `${subject}-${name}@example.test`)
// kit:convex-better-auth:end
/** A person signed in with a kit session row, and the BFF identity of that device. */
const signedIn = async (name) => {
const personSubject = await accounts.subjectFor(name)
client.setAdminAuth(adminKey)
const person = await client.mutation(ref("system:appUserResolve"), {
claims: {
issuer: accounts.issuer,
subject: personSubject,
email: `${subject}-${name}@example.test`,
displayName: name,
locale: "en",
},
create: true,
})
const now = Date.now()
const device = await client.mutation(ref("system:sessionInsert"), {
input: {
userId: person.id,
issuer: accounts.issuer,
subject: personSubject,
handleHash: Array.from(globalThis.crypto.getRandomValues(new Uint8Array(32)), (b) =>
b.toString(16).padStart(2, "0"),
).join(""),
tokens: "sealed-tokens-of-the-smoke",
accessTokenExpiresAt: iso(now + 3_600_000),
lastSeenAt: iso(now),
absoluteExpiresAt: iso(now + 86_400_000),
revokedAt: null,
},
})
const browser = {
issuer: "https://conusai.internal/bff",
subject: person.id,
tokenIdentifier: `https://conusai.internal/bff|${person.id}`,
conusaiCredential: "interactive",
conusaiSessionId: device.id,
}
return { person, device, browser, email: `${subject}-${name}@example.test` }
}
const ada = await signedIn("account")
client.setAdminAuth(adminKey, ada.browser)
const accountOrg = await client.mutation(ref("generated/tenancy:tenancy_org_create"), {
input: { slug: "account-smoke", name: "Account smoke" },
})
// The support module knows the person once a widget session was minted for them.
await client.action(ref("generated/support:support_widgetSession_mint"), { input: { org: accountOrg.slug } }) // kit:support
const exportAs = () => client.action(ref("generated/account:account_data_export"), { input: {} })
// An API credential acting as the person is refused: the export is their own, in full.
const { conusaiCredential: _interactive, conusaiSessionId: _session, ...apiIdentity } = ada.browser
client.setAdminAuth(adminKey, apiIdentity)
await assert.rejects(exportAs, (error) => error?.data?.code === "forbidden", "an API credential cannot export")
client.setAdminAuth(adminKey, ada.browser)
const exported = await exportAs()
assert.equal(
exported.complete,
true,
`every installed module exported its share: ${JSON.stringify(exported.failed)} ${JSON.stringify(Object.fromEntries(Object.entries(exported.sections).map(([key, section]) => [key, section.status === "failed" ? section : section.status])))}`,
)
assert.equal(exported.subject, ada.person.id)
assert.equal(exported.sections.account?.status, "ok")
assert.equal(exported.sections.account.data.profile.email, ada.email, "this app's own rows, from the Convex tables")
assert.deepEqual(
exported.sections.account.data.organizations.map((org) => org.slug),
["account-smoke"],
)
// A module whose service is down is a failed section, named, never left out.
await outage("/v1/support/", true) // kit:support
const partial = await exportAs()
// kit:support:begin
assert.equal(partial.complete, false)
assert.deepEqual(partial.failed, ["support"])
assert.equal(partial.sections.support?.status, "failed")
assert.equal(partial.sections.support.retryable, true)
// kit:support:end
assert.equal(partial.sections.account?.status, "ok")
const status = await client.query(ref("generated/account:account_deletion_status"), {})
assert.equal(status.deletion, null)
assert.equal(status.interactive, true)
assert.equal(status.recentSignIn, true, "the sign-in of the session row the BFF named is recent")
assert.deepEqual(
status.onlyMemberOf.map((org) => org.slug),
["account-smoke"],
)
const deleteAs = (confirm) => client.action(ref("generated/account:account_deletion_request"), { input: { confirm } })
await assert.rejects(
() => deleteAs("someone-else@example.test"),
(error) => error?.data?.code === "confirmation_mismatch",
"a deletion is confirmed with the account's own address",
)
assert.equal(await storeRead("deletion", ada.person.id), null, "a refused deletion writes no ledger row")
const supportBefore = (await effectState()).support.length
const held = await deleteAs(ada.email.toUpperCase())
const ledger = await storeRead("deletion", ada.person.id)
// kit:support:begin
assert.equal(held.state, "erasing", "the support module is down: the deletion holds")
assert.deepEqual(held.pending, ["support"], `held at ${ledger.lastFailure} after ${JSON.stringify(ledger.steps)}`)
assert.match(ledger.lastFailure, /^support:/, "the ledger names the module that held it, by its problem code")
// The steps before the erasures completed, except the widget-session revocation the platform refused.
assert(!ledger.steps.includes("service-sessions"))
// kit:support:end
assert(
["credentials", "identity"].every((step) => ledger.steps.includes(step)),
"the ledger records the steps",
)
assert.equal((await client.query(ref("system:sessionById"), { id: ada.device.id }))?.revokedAt === null, false)
assert.equal(await storeRead("emailOf", ada.person.id), `${ada.person.id}@deleted.invalid`, "the identity is detached")
assert.deepEqual(await storeRead("identities", ada.person.id), [], "no sign-in reaches the account")
// kit:guides:begin
assert(
(await effectState()).guides.some((call) => call.route === "erase" && call.subject === ada.person.id),
"the guides module erased its share by the subject while the user row still existed",
)
// kit:guides:end
client.setAdminAuth(adminKey)
// kit:support:begin
await assert.rejects(
() => client.action(ref("account:sweep"), {}),
(error) => error?.data?.code === "account_deletion_waiting",
"the sweep fails while a deletion waits: the line an operator is alerted on",
)
// kit:support:end
await outage("/v1/support/", false) // kit:support
await client.action(ref("account:sweep"), {})
const done = await storeRead("deletion", ada.person.id)
assert.equal(done.state, "completed", "the sweep finished the deletion once the module was back")
assert.deepEqual(done.pending, [])
assert.equal(await storeRead("emailOf", ada.person.id), null, "the user row goes last")
assert.equal(await client.query(ref("system:sessionById"), { id: ada.device.id }), null)
// kit:support:begin
const supportCalls = (await effectState()).support.slice(supportBefore)
const revokedAt = supportCalls.findIndex((call) => call.route === "revoke" && call.body?.subject === ada.person.id)
const erasedAt = supportCalls.findIndex((call) => call.route === "erase" && call.subject === ada.person.id)
assert(revokedAt >= 0 && erasedAt > revokedAt, "the widget sessions were revoked before the contact was erased")
assert(done.steps.includes("service-sessions"), "the resumption repeated the refused revocation")
assert.deepEqual(
[...done.erased].sort(),
["guides", "support"].filter((key) => status.modules.includes(key)),
)
// kit:support:end
// Asked again, the deletion that has completed answers its state: one deletion, however often asked.
client.setAdminAuth(adminKey, ada.browser)
assert.equal((await deleteAs(ada.email)).state, "completed")
// The identity provider's account events (Better Auth's relay, with the admin key): a ban revokes
// every credential and erases nothing; a deletion at the provider erases through the same ledger.
const grace = await signedIn("provider-event")
client.setAdminAuth(adminKey)
await client.action(ref("account:providerEvent"), { reason: "user-disabled", userId: grace.person.id })
assert.notEqual((await client.query(ref("system:sessionById"), { id: grace.device.id }))?.revokedAt, null)
assert.equal(await storeRead("deletion", grace.person.id), null, "a ban erases nothing")
await client.action(ref("account:providerEvent"), { reason: "account-deleted", userId: grace.person.id })
await client.action(ref("account:providerEvent"), { reason: "account-deleted", userId: grace.person.id })
assert.equal((await storeRead("deletion", grace.person.id))?.state, "completed")
assert.equal(await storeRead("emailOf", grace.person.id), null)
// kit:support:begin
// The sign-out revocation outbox (convex/account.ts): a widget session minted for one device's app
// session is ended by that device's sign-out. The web's `account:revokeSession` records it in the
// backend and answers while the support platform is down; the cron's `account:revocationSweep`
// retries it once due, delivers it once, and a revocation older than 65 minutes lapses unasked.
const revocationRead = (method, ...args) => client.query(ref("account:revocationRead"), { method, args })
const revocationWrite = (method, ...args) => client.mutation(ref("account:revocationWrite"), { method, args })
const pendingRevocation = async (session) =>
(await revocationRead("due", iso(Date.now() + 3_600_000), 100)).find((row) => row.session === session)
const revokesOf = async (session) =>
(await effectState()).support.filter((call) => call.route === "revoke" && call.body?.identity_session === session)
// A person under the test issuer: no Better Auth sign-up (its sign-up rate limit is for the rows below).
client.setAdminAuth(adminKey)
const leaver = await client.mutation(ref("system:appUserResolve"), {
claims: {
issuer,
subject: `${subject}-sign-out`,
email: `${subject}-sign-out@example.test`,
displayName: "Sign-out",
locale: "en",
},
create: true,
})
const leaving = {
browser: {
issuer: "https://conusai.internal/bff",
subject: leaver.id,
tokenIdentifier: `https://conusai.internal/bff|${leaver.id}`,
},
}
const deviceSession = `4:oidc:${leaver.id.length}:${leaver.id}`
client.setAdminAuth(adminKey, leaving.browser)
const leavingOrg = await client.mutation(ref("generated/tenancy:tenancy_org_create"), {
input: { slug: "revocation-smoke", name: "Revocation smoke" },
})
client.setAdminAuth(adminKey, { ...leaving.browser, conusaiIdentitySession: deviceSession })
await client.action(ref("generated/support:support_widgetSession_mint"), { input: { org: leavingOrg.slug } })
assert.equal(
(await effectState()).support.filter((call) => call.route === "mint" && call.body?.identity_session === deviceSession)
.length,
1,
"the widget session names the device's app session it was minted for",
)
client.setAdminAuth(adminKey)
await outage("/v1/support/", true)
await client.action(ref("account:revokeSession"), { session: deviceSession })
const waiting = await pendingRevocation(deviceSession)
assert.equal(waiting?.state, "pending", "the sign-out answered and the revocation is recorded in the backend")
assert.deepEqual(waiting.pending, ["support"])
assert.equal(waiting.attempts, 1)
assert.match(waiting.lastFailure, /^support:/)
assert.deepEqual(await revokesOf(deviceSession), [])
await outage("/v1/support/", false)
// Not due yet (the row's 30 s backoff): the cron's run asks nobody.
await client.action(ref("account:revocationSweep"), {})
assert.deepEqual(await revokesOf(deviceSession), [], "a retry waits for its backoff")
// Due: the retry brought forward rather than waited out.
await revocationWrite("change", waiting, { nextAttemptAt: iso(Date.now()) })
await client.action(ref("account:revocationSweep"), {})
const revocations = await revokesOf(deviceSession)
assert.equal(revocations.length, 1, "the retry delivered the revocation")
assert.equal(revocations[0].revoked, 1, "the device's widget session ended")
assert.equal(await pendingRevocation(deviceSession), undefined)
await client.action(ref("account:revocationSweep"), {})
await client.action(ref("account:revokeSession"), { session: deviceSession })
assert.equal((await revokesOf(deviceSession)).length, 1, "delivered once: a second sweep or notification asks nobody")
const stale = `${deviceSession}:stale`
const longAgo = iso(Date.now() - 66 * 60_000)
await revocationWrite("record", { session: stale, modules: ["support"], at: longAgo, claimedUntil: longAgo })
await client.action(ref("account:revocationSweep"), {})
assert.equal(await pendingRevocation(stale), undefined, "a revocation older than 65 minutes lapses")
assert.deepEqual(await revokesOf(stale), [], "and nobody is asked")
// kit:support:end
// kit:convex-better-auth:begin
// With Better Auth, a deletion ends the person's account in the app's Better Auth deployment (a real
// backend here), found by the identity mapping, before the identity is detached.
const linusEmail = `${subject}-better-auth@example.test`
const betterAuthUser = await signUpAt("Linus", linusEmail)
client.setAdminAuth(adminKey)
const linus = await client.mutation(ref("system:appUserResolve"), {
claims: { issuer: namespace, subject: betterAuthUser, email: linusEmail, displayName: "Linus", locale: "en" },
create: true,
})
client.setAdminAuth(adminKey, {
issuer: "https://conusai.internal/bff",
subject: linus.id,
tokenIdentifier: `https://conusai.internal/bff|${linus.id}`,
conusaiCredential: "interactive",
// A Better Auth session has no kit row: the BFF names the sign-in time Better Auth reported.
conusaiSignedInAt: iso(Date.now() - 1000),
})
assert.equal((await deleteAs(linusEmail)).state, "completed")
const linusLedger = await storeRead("deletion", linus.id)
assert.equal(linusLedger.provider, "convex-better-auth", "the deletion recorded the provider it ends the account at")
assert(linusLedger.steps.includes("provider-account"))
assert.notEqual(
(await betterAuth("/sign-in/email", { email: linusEmail, password })).status,
200,
"the Better Auth account is gone: its password signs nobody in",
)
// kit:convex-better-auth:end
console.log(
"local Convex app smoke green: deployed functions, BFF identity, tenancy, project, file Blob actions, OCR action, proposal approval, notes read/write, guides/support effect actions, account export/deletion/sweep/provider events, sign-out revocation outbox",
)