Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 43 additions & 15 deletions examples/app-showcase/src/data/seed/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,34 @@ import { Contact } from '../objects/contact.object.js';
import { Inquiry } from '../objects/inquiry.object.js';
import { FieldZoo } from '../objects/field-zoo.object.js';
import { Announcement } from '../objects/announcement.object.js';
import { ADMIN_EMAIL, PHONE_DEMO_USER, AUDITOR_DEMO_USER } from '../../security/demo-personas.js';

/**
* ## Why assignees/owners are addressed to the PROVISIONED personas (#7746)
*
* `showcase_task.assignee` and `showcase_project.owner` are `Field.text`, but
* they are not free text: the notify nodes in `automation/flows` read them as
* RECIPIENTS (`recipients: ['{record.assignee}']`, `['{record.owner}']`). An
* email-shaped recipient is resolved against `sys_user`, and on a MISS the
* resolver keeps it VERBATIM as the recipient id — so a made-up address does
* not fail loudly, it persists a `sys_inbox_message` whose `user_id` is a
* string no authenticated user can ever match.
*
* The seed used to write `ada@` / `linus@` / `grace@` / `sam@example.com`
* here, none of which is a `sys_user`. Reassigning a seeded task — the stock
* demo of `showcase_task_assigned_notify` — therefore delivered into a void,
* and the QA run that found this (#7690) had to sign up its own personas to
* exercise the notify path at all.
*
* The three constants below are the identities this app actually provisions on
* a dev boot. `demo-personas.ts` documents that set, its dev-only limits, and
* why `invoice.owner` / `team.lead` deliberately KEEP their `ada@`-style
* emails. Three distinct values still spread across every Kanban column,
* gallery card and list, so the "feed every view" principle below is intact.
*/
const ADMIN = ADMIN_EMAIL;
const MEI = PHONE_DEMO_USER.email;
const ADA = AUDITOR_DEMO_USER.email;

/**
* Seed data sized to "feed every view": every Kanban column is populated,
Expand Down Expand Up @@ -143,11 +171,11 @@ const projects = defineSeed(Project, {
mode: 'upsert',
externalId: 'name',
records: [
{ name: 'Website Relaunch', account: 'Northwind', status: 'active', health: 'green', budget: 150_000, spent: 60_000, owner: 'ada@example.com', start_date: cel`daysAgo(30)`, end_date: cel`daysFromNow(60)` },
{ name: 'Data Platform', account: 'Contoso', status: 'active', health: 'yellow', budget: 600_000, spent: 420_000, owner: 'linus@example.com', start_date: cel`daysAgo(90)`, end_date: cel`daysFromNow(120)` },
{ name: 'Compliance Audit', account: 'Fabrikam', status: 'on_hold', health: 'red', budget: 90_000, spent: 88_000, owner: 'grace@example.com', start_date: cel`daysAgo(15)`, end_date: cel`daysFromNow(30)` },
{ name: 'Mobile App', account: 'Contoso', status: 'planned', health: 'green', budget: 200_000, spent: 0, owner: 'ada@example.com', start_date: cel`daysFromNow(14)`, end_date: cel`daysFromNow(140)` },
{ name: 'Legacy Sunset', account: 'Northwind', status: 'completed', health: 'green', budget: 50_000, spent: 48_000, owner: 'linus@example.com', start_date: cel`daysAgo(180)`, end_date: cel`daysAgo(20)` },
{ name: 'Website Relaunch', account: 'Northwind', status: 'active', health: 'green', budget: 150_000, spent: 60_000, owner: ADA, start_date: cel`daysAgo(30)`, end_date: cel`daysFromNow(60)` },
{ name: 'Data Platform', account: 'Contoso', status: 'active', health: 'yellow', budget: 600_000, spent: 420_000, owner: ADMIN, start_date: cel`daysAgo(90)`, end_date: cel`daysFromNow(120)` },
{ name: 'Compliance Audit', account: 'Fabrikam', status: 'on_hold', health: 'red', budget: 90_000, spent: 88_000, owner: MEI, start_date: cel`daysAgo(15)`, end_date: cel`daysFromNow(30)` },
{ name: 'Mobile App', account: 'Contoso', status: 'planned', health: 'green', budget: 200_000, spent: 0, owner: ADA, start_date: cel`daysFromNow(14)`, end_date: cel`daysFromNow(140)` },
{ name: 'Legacy Sunset', account: 'Northwind', status: 'completed', health: 'green', budget: 50_000, spent: 48_000, owner: ADMIN, start_date: cel`daysAgo(180)`, end_date: cel`daysAgo(20)` },
],
});

Expand All @@ -156,16 +184,16 @@ const tasks = defineSeed(Task, {
mode: 'upsert',
externalId: 'title',
records: [
{ title: 'Audit current IA', project: 'Website Relaunch', assignee: 'ada@example.com', status: 'done', priority: 'medium', estimate_hours: 8, progress: 100, done: true, created_at: cel`daysAgo(20)`, start_date: cel`daysAgo(20)`, end_date: cel`daysAgo(18)`, due_date: cel`daysAgo(18)`, location: { lat: 47.6062, lng: -122.3321 } },
{ title: 'Design system', project: 'Website Relaunch', assignee: 'ada@example.com', status: 'in_review', priority: 'high', estimate_hours: 24, progress: 80, done: false, created_at: cel`daysAgo(14)`, start_date: cel`daysAgo(12)`, end_date: cel`daysFromNow(2)`, due_date: cel`daysFromNow(2)`, location: { lat: 37.7749, lng: -122.4194 } },
{ title: 'Build homepage', project: 'Website Relaunch', assignee: 'sam@example.com', status: 'in_progress', priority: 'high', estimate_hours: 40, progress: 45, done: false, created_at: cel`daysAgo(8)`, start_date: cel`daysAgo(6)`, end_date: cel`daysFromNow(10)`, due_date: cel`daysFromNow(10)`, location: { lat: 40.7128, lng: -74.0060 } },
{ title: 'SEO migration plan', project: 'Website Relaunch', assignee: 'sam@example.com', status: 'todo', priority: 'medium', estimate_hours: 16, progress: 0, done: false, created_at: cel`daysAgo(3)`, start_date: cel`daysFromNow(5)`, end_date: cel`daysFromNow(15)`, due_date: cel`daysFromNow(15)`, location: { lat: 30.2672, lng: -97.7431 } },
{ title: 'Content backlog', project: 'Website Relaunch', assignee: 'grace@example.com', status: 'backlog', priority: 'low', estimate_hours: 12, progress: 0, done: false, created_at: cel`daysAgo(2)`, due_date: cel`daysFromNow(30)`, location: { lat: 41.8781, lng: -87.6298 } },
{ title: 'Ingest pipeline', project: 'Data Platform', assignee: 'linus@example.com', status: 'in_progress', priority: 'urgent', estimate_hours: 60, progress: 55, done: false, created_at: cel`daysAgo(40)`, start_date: cel`daysAgo(35)`, end_date: cel`daysFromNow(20)`, due_date: cel`daysFromNow(20)`, location: { lat: 39.7392, lng: -104.9903 } },
{ title: 'Warehouse schema', project: 'Data Platform', assignee: 'linus@example.com', status: 'in_review', priority: 'high', estimate_hours: 30, progress: 90, done: false, created_at: cel`daysAgo(25)`, start_date: cel`daysAgo(22)`, end_date: cel`daysFromNow(3)`, due_date: cel`daysFromNow(3)`, location: { lat: 42.3601, lng: -71.0589 } },
{ title: 'PII access review', project: 'Compliance Audit', assignee: 'grace@example.com', status: 'todo', priority: 'urgent', estimate_hours: 20, progress: 0, done: false, created_at: cel`daysAgo(5)`, start_date: cel`daysFromNow(2)`, end_date: cel`daysFromNow(12)`, due_date: cel`daysFromNow(12)`, location: { lat: 38.9072, lng: -77.0369 } },
{ title: 'Evidence collection', project: 'Compliance Audit', assignee: 'grace@example.com', status: 'backlog', priority: 'medium', estimate_hours: 18, progress: 0, done: false, created_at: cel`daysAgo(1)`, due_date: cel`daysFromNow(25)`, location: { lat: 34.0522, lng: -118.2437 } },
{ title: 'App wireframes', project: 'Mobile App', assignee: 'ada@example.com', status: 'done', priority: 'medium', estimate_hours: 16, progress: 100, done: true, created_at: cel`daysAgo(10)`, start_date: cel`daysAgo(10)`, end_date: cel`daysAgo(6)`, due_date: cel`daysAgo(6)`, location: { lat: 45.5152, lng: -122.6784 } },
{ title: 'Audit current IA', project: 'Website Relaunch', assignee: ADA, status: 'done', priority: 'medium', estimate_hours: 8, progress: 100, done: true, created_at: cel`daysAgo(20)`, start_date: cel`daysAgo(20)`, end_date: cel`daysAgo(18)`, due_date: cel`daysAgo(18)`, location: { lat: 47.6062, lng: -122.3321 } },
{ title: 'Design system', project: 'Website Relaunch', assignee: ADA, status: 'in_review', priority: 'high', estimate_hours: 24, progress: 80, done: false, created_at: cel`daysAgo(14)`, start_date: cel`daysAgo(12)`, end_date: cel`daysFromNow(2)`, due_date: cel`daysFromNow(2)`, location: { lat: 37.7749, lng: -122.4194 } },
{ title: 'Build homepage', project: 'Website Relaunch', assignee: ADMIN, status: 'in_progress', priority: 'high', estimate_hours: 40, progress: 45, done: false, created_at: cel`daysAgo(8)`, start_date: cel`daysAgo(6)`, end_date: cel`daysFromNow(10)`, due_date: cel`daysFromNow(10)`, location: { lat: 40.7128, lng: -74.0060 } },
{ title: 'SEO migration plan', project: 'Website Relaunch', assignee: ADMIN, status: 'todo', priority: 'medium', estimate_hours: 16, progress: 0, done: false, created_at: cel`daysAgo(3)`, start_date: cel`daysFromNow(5)`, end_date: cel`daysFromNow(15)`, due_date: cel`daysFromNow(15)`, location: { lat: 30.2672, lng: -97.7431 } },
{ title: 'Content backlog', project: 'Website Relaunch', assignee: MEI, status: 'backlog', priority: 'low', estimate_hours: 12, progress: 0, done: false, created_at: cel`daysAgo(2)`, due_date: cel`daysFromNow(30)`, location: { lat: 41.8781, lng: -87.6298 } },
{ title: 'Ingest pipeline', project: 'Data Platform', assignee: ADMIN, status: 'in_progress', priority: 'urgent', estimate_hours: 60, progress: 55, done: false, created_at: cel`daysAgo(40)`, start_date: cel`daysAgo(35)`, end_date: cel`daysFromNow(20)`, due_date: cel`daysFromNow(20)`, location: { lat: 39.7392, lng: -104.9903 } },
{ title: 'Warehouse schema', project: 'Data Platform', assignee: ADMIN, status: 'in_review', priority: 'high', estimate_hours: 30, progress: 90, done: false, created_at: cel`daysAgo(25)`, start_date: cel`daysAgo(22)`, end_date: cel`daysFromNow(3)`, due_date: cel`daysFromNow(3)`, location: { lat: 42.3601, lng: -71.0589 } },
{ title: 'PII access review', project: 'Compliance Audit', assignee: MEI, status: 'todo', priority: 'urgent', estimate_hours: 20, progress: 0, done: false, created_at: cel`daysAgo(5)`, start_date: cel`daysFromNow(2)`, end_date: cel`daysFromNow(12)`, due_date: cel`daysFromNow(12)`, location: { lat: 38.9072, lng: -77.0369 } },
{ title: 'Evidence collection', project: 'Compliance Audit', assignee: MEI, status: 'backlog', priority: 'medium', estimate_hours: 18, progress: 0, done: false, created_at: cel`daysAgo(1)`, due_date: cel`daysFromNow(25)`, location: { lat: 34.0522, lng: -118.2437 } },
{ title: 'App wireframes', project: 'Mobile App', assignee: ADA, status: 'done', priority: 'medium', estimate_hours: 16, progress: 100, done: true, created_at: cel`daysAgo(10)`, start_date: cel`daysAgo(10)`, end_date: cel`daysAgo(6)`, due_date: cel`daysAgo(6)`, location: { lat: 45.5152, lng: -122.6784 } },
],
});

Expand Down
106 changes: 106 additions & 0 deletions examples/app-showcase/src/security/demo-personas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* The identities the showcase actually PROVISIONS — the one place that answers
* "which `sys_user` rows exist on a fresh boot of this app?".
*
* ## Why this module exists (#7746)
*
* `sys_user` rows cannot be seeded — they come from sign-up — so for a long time
* the seed simply wrote plausible-looking emails (`ada@example.com`,
* `linus@example.com`, `sam@example.com`) into the fields that FEED THE NOTIFY
* PATH: `showcase_task.assignee` and `showcase_project.owner`. Those fields are
* `Field.text`, so nothing rejected the values, and every view rendered happily.
*
* The cost showed up one layer down. `RecipientResolver` (ADR-0030 P1) resolves
* an email-shaped recipient by looking it up in `sys_user` and, on a MISS,
* **keeps the string verbatim** as the recipient id
* (`service-messaging/src/recipient-resolver.ts` — "no 'sys_user' matched email
* '…'; keeping verbatim"). So the stock reassignment demo
* (`showcase_task_assigned_notify`) persisted a `sys_inbox_message` whose
* `user_id` was the literal text `ada@example.com`: a row addressed to nobody,
* which no authenticated user can ever read. The reference app's marquee
* "reassign a task and watch the inbox" story quietly delivered into a void.
*
* The fix is to address those fields to identities that REALLY EXIST. On a fresh
* dev boot that set is exactly three rows, and this module is their registry:
*
* 1. {@link ADMIN_EMAIL} — the dev admin seeded by `plugin-auth`
* (`OS_SEED_ADMIN_EMAIL`, default below). The only LOGINABLE one.
* 2. {@link PHONE_DEMO_USER} / 3. {@link AUDITOR_DEMO_USER} — the personas
* `seed-approval-demo.ts` provisions on `kernel:bootstrapped`.
*
* ## The honest limits of that set
*
* Both persona rows are **display/routing identities, not accounts**: they carry
* no better-auth credential, so they cannot sign in (see `ensureDemoUser`). They
* are enough to make a recipient RESOLVE — which is the whole of this defect —
* and enough to make the inbox row addressable to a real user id, but reading
* that row as that persona still needs the sign-up the showcase deliberately
* leaves to the operator.
*
* And all three are DEV-ONLY: the dev admin is gated on
* `NODE_ENV === 'development'`, and the two personas are provisioned only when
* that admin exists. In a real deployment a fresh showcase has NO users at all,
* so nothing here could resolve — that is a property of the environment, not a
* defect in the seed, and no seed value can repair it.
*
* ## What is deliberately NOT addressed to these, and why
*
* `showcase_team.lead` is display-only — it reaches no notify recipient, so it
* carries none of this defect and keeps its `ada@`-style email.
*
* `showcase_invoice.owner` is the harder case, and it is left alone KNOWINGLY
* rather than because it is safe. It genuinely does reach a notify recipient
* (`showcase_invoice_lifecycle` sends to `{record.owner}`), so it is the same
* defect class as the fields fixed above. But those three emails are also the
* fixture for the ADR-0055 controlled-by-parent isolation demo, whose whole
* point is that an operator SIGNS UP as `ada@example.com` and then sees only
* their own invoices — `qa/dogfood/test/showcase-invoice-seed-isolation.
* dogfood.test.ts` does exactly that, and pins the seeded owners.
*
* Repointing invoices at the personas above would not fix that demo, it would
* DELETE it: these personas hold no credential, so nobody can sign in as one
* and observe the row-level scoping. Making them loginable, or giving the
* showcase a set of real signup-able contributor accounts, is a larger design
* decision than this defect — so it is reported on #7746 rather than guessed
* at here, and `test/inert-wirings.test.ts` §5 carries the exemption with the
* same reasoning attached.
*/

/** The dev admin `plugin-auth` seeds when `NODE_ENV === 'development'`. */
export const ADMIN_EMAIL = 'admin@objectos.ai';

/** A phone-based demo persona (§6 "phone sign-in surfaces"). */
export const PHONE_DEMO_USER = {
id: 'usr_showcase_phone_demo',
name: 'Mei Phone (demo)',
email: 'phone.demo@example.com',
phone_number: '+8613800138000',
} as const;

/**
* A second persona holding ONLY `auditor`, which is the position behind the
* `finance` group of the per-group (会签) demo. It has to be a *different* user
* from the admin: with one user in both groups a single decision would satisfy
* both tallies at once, and "one approval per group" would never be observable.
*/
export const AUDITOR_DEMO_USER = {
id: 'usr_showcase_auditor_demo',
name: 'Ada Auditor (demo)',
email: 'auditor.demo@example.com',
} as const;

/**
* Every email a fresh boot of this app turns into a real `sys_user` row.
*
* This is the allow-list a seed value must come from before it may be written
* to a field the notify path reads as a recipient — pinned by
* `test/inert-wirings.test.ts` §5, so re-introducing a made-up persona fails a
* test instead of shipping another unreadable inbox row.
*/
export const PROVISIONED_USER_EMAILS: readonly string[] = [
ADMIN_EMAIL,
PHONE_DEMO_USER.email,
AUDITOR_DEMO_USER.email,
];
31 changes: 9 additions & 22 deletions examples/app-showcase/src/security/seed-approval-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,16 @@
* which we swallow.
*/

const SYS = { isSystem: true } as const;
/**
* The identities this bootstrap provisions now live in `demo-personas.ts`,
* because the SEED needs them too: the fields that feed the notify path
* (`showcase_task.assignee`, `showcase_project.owner`) must name identities
* this file really creates, or a reassignment writes an inbox row addressed to
* nobody (#7746). One registry, two consumers — so the two cannot drift apart.
*/
import { ADMIN_EMAIL, PHONE_DEMO_USER, AUDITOR_DEMO_USER } from './demo-personas.js';

const ADMIN_EMAIL = 'admin@objectos.ai';
const SYS = { isSystem: true } as const;

/**
* Positions the admin is granted so they resolve as an approver on the demos.
Expand All @@ -52,26 +59,6 @@ const ADMIN_EMAIL = 'admin@objectos.ai';
*/
const ADMIN_APPROVAL_POSITIONS = ['manager', 'finance', 'legal', 'exec'] as const;

/** A phone-based demo persona (§6 "phone sign-in surfaces"). */
const PHONE_DEMO_USER = {
id: 'usr_showcase_phone_demo',
name: 'Mei Phone (demo)',
email: 'phone.demo@example.com',
phone_number: '+8613800138000',
} as const;

/**
* A second persona holding ONLY `auditor`, which is the position behind the
* `finance` group of the per-group (会签) demo. It has to be a *different* user
* from the admin: with one user in both groups a single decision would satisfy
* both tallies at once, and "one approval per group" would never be observable.
*/
const AUDITOR_DEMO_USER = {
id: 'usr_showcase_auditor_demo',
name: 'Ada Auditor (demo)',
email: 'auditor.demo@example.com',
} as const;

interface ApprovalDemoContext {
ql: {
find: (object: string, query: unknown, options?: unknown) => Promise<unknown>;
Expand Down
Loading
Loading