ConusAI vendor v0.1.0-dev.20261027+9b1131fb
ci / gate (push) Waiting to run

This commit is contained in:
ConusAI vendor v0.1.0-dev.20261027+9b1131fb
2026-10-27 18:05:17 +00:00
commit 3102273ec7
538 changed files with 80203 additions and 0 deletions
+53
View File
@@ -0,0 +1,53 @@
// The module's names and limits, in a file with no import: a browser bundle can take them without
// loading the data layer the schemas live beside.
/** The format an export names itself with, so a reader of the file knows which shape it holds. */
export const ACCOUNT_EXPORT_FORMAT = "conusai.account-export.v1"
/** The section of an export that holds this app's own rows about the person. */
export const ACCOUNT_SECTION = "account"
/**
* How recent the sign-in of the session that asks for a deletion must be. Deleting an account is the
* owner action the template spec's session policy asks a re-authentication for (§7): a session that
* was left open on a shared machine is not enough.
*/
export const RECENT_SIGN_IN_SECONDS = 15 * 60
/** The job that resumes a deletion a module's erasure held up, and how often the worker runs it. */
export const DELETION_SWEEP_JOB = "account.deletion.sweep"
export const DELETION_SWEEP_CRON = "*/5 * * * *"
/** What a deleted user row is left holding until the deletion completes: no provider identity, no address. */
export const DETACHED_ISSUER = "urn:conusai:account:deleted"
export const DETACHED_DISPLAY_NAME = "Deleted account"
/** Where the account page lives: `settings` is a reserved organization slug, so no organization shadows it. */
export const ACCOUNT_PATH = "/settings/account"
/**
* The job that delivers the revocations of app sessions whose derived service sessions a module's
* platform has not confirmed ending yet, and how often the worker runs it (pg-boss's finest cron).
*/
export const REVOCATION_JOB = "account.sessions.revoke"
export const REVOCATION_CRON = "* * * * *"
/** How long one module's revocation call may take before it counts as failed (a sign-out waits no longer). */
export const REVOCATION_CALL_MS = 3000
/** How long a delivery pass holds its row: well above one bounded call, so a pass that died is retried. */
export const REVOCATION_CLAIM_MS = 30_000
/** The first retry waits this long, each next one twice as long, up to REVOCATION_BACKOFF_MAX_MS. */
export const REVOCATION_BACKOFF_MS = 30_000
export const REVOCATION_BACKOFF_MAX_MS = 5 * 60_000
/**
* The longest a derived service session can outlive its app session's revocation: a Support widget
* session lives 60 minutes from its mint (the platform's fixed lifetime), plus a margin for clocks.
* A revocation still undelivered after this has nothing left to end: it is `lapsed`, not retried.
*/
export const DERIVED_SESSION_LIFETIME_MS = 65 * 60_000
/** Delivered and lapsed revocation rows are removed this long after they finished. */
export const REVOCATION_RETENTION_MS = 24 * 60 * 60_000
+113
View File
@@ -0,0 +1,113 @@
// The one table of the account module: the deletion ledger.
//
// A row is written before anything of the account is removed, and it outlives the user row: it is
// the erasure reference the joint contract (§12) asks a deletion to keep, so a module whose erasure
// was unavailable is erased later by the same subject, and a deletion interrupted at any step resumes
// from the row rather than from the person, who can no longer sign in. It holds the app user id (an
// opaque id) and module ids, never an e-mail address, a name or a provider subject.
//
// `global` in kit-data's sense: an account belongs to no organization.
import { defineEntity, idSchema, instantSchema } from "@conusai-kit/data"
import { z } from "zod"
export const DELETION_STATES = ["erasing", "completed"] as const
export type DeletionState = (typeof DELETION_STATES)[number]
/**
* The steps of a deletion before the modules' erasures, in their order (core/deletion.ts). The ledger
* records each one when it completed, so a resumption -- the person's retry or the worker's sweep --
* runs exactly the steps still to do, and never one it already finished:
*
* credentials every session and API key of the person revoked
* service-sessions every module's derived service sessions revoked (a module that could not be
* reached leaves it open; it does not hold the deletion up)
* provider-account the account ended at the app's external identity provider (Better Auth), when
* the deletion recorded one in `provider`
* identity the identity mapping detached, and the sessions revoked once more
*/
export const DELETION_STEPS = ["credentials", "service-sessions", "provider-account", "identity"] as const
export type DeletionStep = (typeof DELETION_STEPS)[number]
const moduleIds = z.array(z.string().min(1).max(64)).max(64)
export const accountDeletions = defineEntity({
name: "accountDeletion",
table: "account_deletion",
tenancy: "global",
fields: {
/** The app user id: the subject every module's erasure is keyed by. */
userId: idSchema,
state: z.enum(DELETION_STATES),
/** The modules whose erasure has not been confirmed yet. */
pending: moduleIds,
/** The modules that confirmed their erasure. */
erased: moduleIds,
/** Modules that were removed from the app before their erasure ran: an operator erases them by `userId`. */
abandoned: moduleIds,
/** Attempts that left a module pending. */
attempts: z.number().int().nonnegative(),
/** The last refusal, as `<module>:<problem code>`: no detail, which could carry a person's data. */
lastFailure: z.string().max(200).nullable(),
requestedAt: instantSchema,
completedAt: instantSchema.nullable(),
/** The steps of DELETION_STEPS that completed, in the order they did; null for a row written before steps were recorded. */
steps: z.array(z.enum(DELETION_STEPS)).max(DELETION_STEPS.length).nullable(),
/**
* The external identity provider whose account this deletion ends (`convex-better-auth`), or null:
* the provider bound where the deletion began. A resumption that has no such provider bound waits
* at that step rather than detach the identity it needs to find the account by.
*/
provider: z.string().min(1).max(64).nullable(),
},
unique: [["userId"]],
indexes: [["state"]],
})
export const REVOCATION_STATES = ["pending", "delivered", "lapsed"] as const
export type RevocationState = (typeof REVOCATION_STATES)[number]
/**
* The revocation outbox: one row per revoked app session (a sign-out, a refused refresh, an expiry)
* whose derived service sessions -- a Support widget session minted for that device -- the installed
* modules still have to end on their platform (joint contract §14 "Lifecycle, CSP and revocation").
*
* Written when kit-auth reports the revocation, before its session row is marked revoked, and
* delivered at once; a module that could not be reached stays `pending`, and the worker's revocation
* job retries it with backoff (REVOCATION_JOB) until its platform confirms, or until no session it
* minted can still be alive (`lapsed`, DERIVED_SESSION_LIFETIME_MS after the request). A claim
* (`claimedUntil`, written with the row's version) keeps two processes from delivering one row at once.
*
* It holds the app session's opaque reference (kit-auth `sessionRefKey`: its provider label and row
* id, never a token or cookie) and module ids: nothing about the person. `global`: a session belongs
* to no organization.
*/
export const accountSessionRevocations = defineEntity({
name: "accountSessionRevocation",
table: "account_session_revocation",
tenancy: "global",
fields: {
/** The revoked app session's opaque reference (kit-auth `sessionRefKey`). */
session: z.string().min(1).max(512),
state: z.enum(REVOCATION_STATES),
/** The modules whose platform has not confirmed the revocation yet. */
pending: moduleIds,
/** The modules whose platform confirmed it. */
delivered: moduleIds,
/** Delivery passes that left a module pending. */
attempts: z.number().int().nonnegative(),
/** When the next delivery pass is due. */
nextAttemptAt: instantSchema,
/** A delivery pass holds the row until then; another process leaves it alone. */
claimedUntil: instantSchema.nullable(),
/** The last refusal, as `<module>:<problem code>`. */
lastFailure: z.string().max(200).nullable(),
requestedAt: instantSchema,
completedAt: instantSchema.nullable(),
},
unique: [["session"]],
indexes: [["state", "nextAttemptAt"]],
})
/** Every table this module owns, in the order a migration creates them. */
export const ACCOUNT_ENTITIES = Object.freeze([accountDeletions, accountSessionRevocations])
+48
View File
@@ -0,0 +1,48 @@
// The public API of the account module: what a page, another module and the OpenAPI document may
// import.
export {
ACCOUNT_EXPORT_FORMAT,
ACCOUNT_PATH,
ACCOUNT_SECTION,
DELETION_SWEEP_CRON,
DELETION_SWEEP_JOB,
DERIVED_SESSION_LIFETIME_MS,
DETACHED_DISPLAY_NAME,
DETACHED_ISSUER,
RECENT_SIGN_IN_SECONDS,
REVOCATION_CRON,
REVOCATION_JOB,
} from "./constants.js"
export {
ACCOUNT_ENTITIES,
accountDeletions,
accountSessionRevocations,
DELETION_STATES,
DELETION_STEPS,
type DeletionState,
type DeletionStep,
REVOCATION_STATES,
type RevocationState,
} from "./entities.js"
export {
type AccountExport,
ANALYTICS_REQUEST_KINDS,
ANALYTICS_STATUSES,
type AnalyticsView,
accountAnalyticsExport,
accountAnalyticsRequest,
accountAnalyticsStatus,
accountDataExport,
accountDeletionRequest,
accountDeletionStatus,
accountExport,
analyticsStore,
analyticsView,
type DeletionStatus,
type DeletionView,
deletionStatusOutput,
deletionView,
type ExportSection,
exportSection,
} from "./procedures.js"
+158
View File
@@ -0,0 +1,158 @@
// The account self-service procedures: export my data, and delete my account (joint contract §12).
//
// All three are user-scoped: they take no organization and no id, so the only account they reach is
// the signed-in person's own, and an organization's API key cannot call them at all (the kernel
// refuses a user scope to a pinned credential). The export and the deletion are `effect`s because
// every installed module contributes its share through kit-tenancy's `tenancy.account` point, and
// the shares of guides and support are calls to the platform.
import { procedure } from "@conusai-kit/core"
import { z } from "zod"
import { ACCOUNT_EXPORT_FORMAT } from "./constants.js"
import { DELETION_STATES } from "./entities.js"
/**
* One module's share of an export. `failed` and `not_exported` are explicit: an export never
* silently leaves a module out (joint contract §12). `data: null` is a module that holds nothing
* about the person in this deployment (its service offline or not configured).
*/
export const exportSection = z.discriminatedUnion("status", [
z.object({ status: z.literal("ok"), data: z.record(z.string(), z.unknown()).nullable() }),
z.object({ status: z.literal("failed"), code: z.string(), retryable: z.boolean() }),
z.object({ status: z.literal("not_exported") }),
])
export type ExportSection = z.output<typeof exportSection>
export const accountExport = z.object({
format: z.literal(ACCOUNT_EXPORT_FORMAT),
exportedAt: z.string(),
/** The app user id every section is keyed by. */
subject: z.string(),
/** Every section exported. False names the sections in `failed` (or one with `not_exported`). */
complete: z.boolean(),
failed: z.array(z.string()),
sections: z.record(z.string(), exportSection),
})
export type AccountExport = z.output<typeof accountExport>
export const accountDataExport = procedure("account.data.export")
.kind("effect")
.scope("user")
.permission("none")
.rateLimit("account")
// An empty object: what a browser's JSON call sends for "no input" (`{}`), refused with anything in it.
.input(z.object({}).strict())
.output(accountExport)
.contract()
const organizationRef = z.object({ slug: z.string(), name: z.string() })
export const deletionView = z.object({
state: z.enum(DELETION_STATES),
erased: z.array(z.string()),
pending: z.array(z.string()),
abandoned: z.array(z.string()),
requestedAt: z.string(),
completedAt: z.string().nullable(),
})
export type DeletionView = z.output<typeof deletionView>
export const deletionStatusOutput = z.object({
/** A deletion already requested for this account, or null. */
deletion: deletionView.nullable(),
/** A person signed in to this app (not an API credential): the only caller a deletion accepts. */
interactive: z.boolean(),
/** The session's sign-in is recent enough to delete the account without signing in again. */
recentSignIn: z.boolean(),
signedInAt: z.string().nullable(),
recentSignInSeconds: z.number().int().positive(),
/** Organizations whose last owner this person is while others are members: they block a deletion. */
lastOwnerOf: z.array(organizationRef),
/** Organizations this person is the only member of: a deletion suspends them. */
onlyMemberOf: z.array(organizationRef),
/** The modules whose data a deletion erases, besides this app's own rows. */
modules: z.array(z.string()),
})
export type DeletionStatus = z.output<typeof deletionStatusOutput>
export const accountDeletionStatus = procedure("account.deletion.status")
.kind("read")
.scope("user")
.permission("none")
.output(deletionStatusOutput)
.contract()
/**
* Deletes the signed-in person's account. The person confirms by typing the account's e-mail address,
* from a session whose sign-in is recent (RECENT_SIGN_IN_SECONDS). Calling it again for an account
* whose deletion has begun resumes that deletion and answers its state, whatever the input.
*/
export const accountDeletionRequest = procedure("account.deletion.request")
.kind("effect")
.scope("user")
.permission("none")
.rateLimit("account")
.input(z.object({ confirm: z.string().min(1).max(320) }))
.output(deletionView)
.contract()
// The platform's analytics and observability data about the signed-in person (ADR-049): what the
// app's visitors leave in the platform's own stores (replay, errors, proxy logs, traces, logs,
// analytics), erased or exported by one platform job. The browser never sees the deployment token
// or the platform's request id in a form it could swap for another person's: it holds a `ticket`,
// the request id signed for this user by the server (core/analytics.ts).
export const ANALYTICS_REQUEST_KINDS = ["erase", "export"] as const
export const ANALYTICS_STATUSES = ["pending", "running", "completed", "failed", "expired"] as const
export const analyticsStore = z.object({
status: z.enum(["completed", "not_keyed", "failed"]),
deleted: z.number().int().nonnegative(),
anonymized: z.number().int().nonnegative(),
exported: z.number().int().nonnegative(),
})
export const analyticsView = z.object({
ticket: z.string(),
kind: z.enum(ANALYTICS_REQUEST_KINDS),
status: z.enum(ANALYTICS_STATUSES),
/** False when the platform answered the job already open for the same kind and person. */
created: z.boolean(),
stores: z.record(z.string(), analyticsStore),
requestedAt: z.string(),
finishedAt: z.string().nullable(),
/** Exports only: when the platform deletes the export document. */
expiresAt: z.string().nullable(),
})
export type AnalyticsView = z.output<typeof analyticsView>
const ticketInput = z.object({ ticket: z.string().min(1).max(512) }).strict()
/** Starts the erasure or the export of the person's platform data, or answers the one already open. */
export const accountAnalyticsRequest = procedure("account.analytics.request")
.kind("effect")
.scope("user")
.permission("none")
.rateLimit("account")
.input(z.object({ kind: z.enum(ANALYTICS_REQUEST_KINDS) }).strict())
.output(analyticsView)
.contract()
/** The state of a request this person started, by its ticket. */
export const accountAnalyticsStatus = procedure("account.analytics.status")
.kind("effect")
.scope("user")
.permission("none")
.input(ticketInput)
.output(analyticsView)
.contract()
/** A completed export's document, by its ticket; a conflict until the export completed. */
export const accountAnalyticsExport = procedure("account.analytics.export")
.kind("effect")
.scope("user")
.permission("none")
.rateLimit("account")
.input(ticketInput)
.output(z.object({ exportedAt: z.string(), document: z.record(z.string(), z.unknown()) }))
.contract()