This commit is contained in:
@@ -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])
|
||||
Reference in New Issue
Block a user