@objectstack/verify 9.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/LICENSE.apache +202 -0
- package/README.md +116 -0
- package/dist/index.cjs +547 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +172 -0
- package/dist/index.d.ts +172 -0
- package/dist/index.js +504 -0
- package/dist/index.js.map +1 -0
- package/package.json +66 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/harness.ts","../src/derive.ts","../src/verify.ts","../src/rls.ts"],"sourcesContent":["// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n//\n// @objectstack/verify — public API.\n//\n// Boot any ObjectStack app in-process and verify it through the real HTTP\n// stack. Two proof families, both app-agnostic (derived from your metadata):\n// - data fidelity : runCrudVerification — author → write → read → assert\n// - authorization : runRlsProofs — \"you can't write what you can't read\"\n\nexport { bootStack } from './harness.js';\nexport type { VerifyStack, BootOptions } from './harness.js';\n\nexport { deriveCrudCases, fillRelationalRefs } from './derive.js';\nexport type { CrudCase, DerivedAssert, AssertKind, RelationalRef } from './derive.js';\n\nexport { runCrudVerification, formatReport } from './verify.js';\nexport type { VerifyReport, ObjectVerifyResult } from './verify.js';\n\nexport { runRlsProofs, formatRlsReport } from './rls.js';\nexport type { RlsReport, RlsResult } from './rls.js';\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n//\n// @objectstack/verify — boot harness.\n//\n// Boots a real ObjectStack app **in-process** against an in-memory SQLite\n// database, wired with the same service plugins `objectstack dev` loads, and\n// exposes the live HTTP surface via Hono's request-injection (no port, no\n// sockets — CI-stable). A verifier then exercises the app exactly as a browser\n// client would: sign in, hit `/api/v1/...`, assert on real responses.\n//\n// Why in-process + real HTTP: a whole class of regressions only surfaces when\n// the real engine + strategies + services + REST context run together — each\n// layer can be individually correct (and individually mocked in unit tests) yet\n// break at the seams (e.g. timezone date-bucketing across analytics strategy,\n// in-memory aggregation, and the REST execution context). This harness runs the\n// integrated stack so those breaks are observable.\n//\n// Posture: development / in-memory. `NODE_ENV` is forced to `development` so the\n// auth plugin's dev-admin bootstrap provisions a known, loginable admin (mirrors\n// `objectstack dev`). This is a verification harness — it never touches a real\n// database or production data.\n\nimport { ObjectKernel, AppPlugin, DriverPlugin, createDispatcherPlugin } from '@objectstack/runtime';\nimport { ObjectQLPlugin } from '@objectstack/objectql';\nimport { SqliteWasmDriver } from '@objectstack/driver-sqlite-wasm';\nimport { HonoServerPlugin } from '@objectstack/plugin-hono-server';\nimport { createRestApiPlugin } from '@objectstack/rest';\nimport { AuthPlugin } from '@objectstack/plugin-auth';\nimport { SecurityPlugin } from '@objectstack/plugin-security';\nimport { SharingServicePlugin } from '@objectstack/plugin-sharing';\nimport { SettingsServicePlugin, LocalCryptoProvider } from '@objectstack/service-settings';\nimport { AnalyticsServicePlugin } from '@objectstack/service-analytics';\n\n/** A Hono app exposes `.request(path, init)` returning a standard `Response`. */\ninterface InjectableApp {\n request(input: string, init?: RequestInit): Promise<Response>;\n}\n\nconst API_PREFIX = '/api/v1';\nconst DEFAULT_ADMIN_EMAIL = 'admin@objectos.ai';\nconst DEFAULT_ADMIN_PASSWORD = 'admin123';\nconst DEFAULT_AUTH_SECRET = 'objectstack-verify-secret';\n\nexport interface VerifyStack {\n /** The booted kernel — for direct service calls when bypassing HTTP is intentional. */\n kernel: ObjectKernel;\n /** Inject an HTTP request through the real Hono app (no socket). Path is relative to `/api/v1`. */\n api(path: string, init?: RequestInit): Promise<Response>;\n /** Inject a request at an absolute path (e.g. `/api/settings/...`). */\n raw(path: string, init?: RequestInit): Promise<Response>;\n /** Sign in through the real auth route; returns a bearer token. Defaults to the dev admin. */\n signIn(email?: string, password?: string): Promise<string>;\n /** Sign up a NEW user through the real auth route; returns their bearer token.\n * The first user is the seeded dev admin, so a fresh sign-up is a plain member\n * (no roles/grants) — exactly what RLS cross-owner proofs need. */\n signUp(email: string, password?: string, name?: string): Promise<string>;\n /** Convenience: an authed JSON request relative to `/api/v1`. */\n apiAs(token: string, method: string, path: string, body?: unknown): Promise<Response>;\n /** Tear down the kernel (close DB / HTTP handles). */\n stop(): Promise<void>;\n}\n\nexport interface BootOptions {\n /** Override the dev admin credentials the harness signs in with. */\n admin?: { email: string; password: string };\n /** Override the auth signing secret. Defaults to a fixed in-process dev secret. */\n authSecret?: string;\n /**\n * Override the SecurityPlugin instance. Pass a `new SecurityPlugin({...})`\n * to carry a custom `fallbackPermissionSet` / extra permission sets — this\n * is how an owner-isolated RLS fixture makes a fresh member fall back to a\n * permission set that carries `RLS.ownerPolicy(...)` instead of the broad-read\n * `member_default`. Defaults to a vanilla `new SecurityPlugin()`.\n */\n security?: SecurityPlugin;\n /**\n * Boot multi-tenant: register `@objectstack/plugin-org-scoping` BEFORE the\n * SecurityPlugin so the wildcard `organization_id` RLS policies that ship in\n * the default permission sets actually apply (SecurityPlugin probes the\n * `org-scoping` service once at start and otherwise STRIPS them — see\n * `collectRLSPolicies`). This exercises the org-scoped isolation real apps\n * rely on, rather than the single-tenant default where every tenant policy is\n * stripped and a member sees every row. Default `false`.\n */\n multiTenant?: boolean;\n /**\n * Register `@objectstack/service-automation` so authored flows execute against\n * the real stack. The plugin seeds the built-in node executors and, at start(),\n * pulls every flow in the app config from the ObjectQL registry and registers\n * it — so `POST /api/v1/automation/:name/trigger` actually runs the flow's\n * nodes. Without this the dispatcher's automation routes resolve no `automation`\n * service and flow execution is unreachable. Opt-in (like `multiTenant`) so the\n * default boot stays lean for apps that don't exercise flows. Default `false`.\n */\n automation?: boolean;\n}\n\n/**\n * Boot an app config in-process and return a live verification stack.\n *\n * `NODE_ENV` is forced to `development` so the auth plugin's dev-admin\n * bootstrap provisions a known, loginable admin (mirrors `objectstack dev`).\n */\nexport async function bootStack(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n config: any,\n opts: BootOptions = {},\n): Promise<VerifyStack> {\n process.env.NODE_ENV = 'development';\n\n const kernel = new ObjectKernel();\n\n // Data engine + in-memory SQLite (pure-JS WASM driver — no native build, CI-safe).\n await kernel.use(new ObjectQLPlugin());\n await kernel.use(new DriverPlugin(new SqliteWasmDriver({ filename: ':memory:' })));\n\n // HTTP server (registers the `http-server` IHttpServer service the REST +\n // dispatcher plugins mount their routes onto). Port 0 = ephemeral; we never\n // hit the socket — requests are injected through the Hono app directly.\n await kernel.use(new HonoServerPlugin({ port: 0 }));\n\n // The app under test (objects, datasets, cubes, flows, seed data).\n await kernel.use(new AppPlugin(config));\n\n // Service plugins `objectstack dev` auto-loads for an app of this shape.\n await kernel.use(new SettingsServicePlugin());\n await kernel.use(new AnalyticsServicePlugin());\n await kernel.use(new AuthPlugin({ secret: opts.authSecret ?? DEFAULT_AUTH_SECRET }));\n\n // Multi-tenant: org-scoping MUST register BEFORE SecurityPlugin — the latter\n // probes the `org-scoping` service exactly once at start and caches it, then\n // keeps (vs strips) the wildcard `organization_id` RLS policies accordingly.\n // Mirrors the CLI's ordering for `OS_MULTI_ORG_ENABLED`.\n if (opts.multiTenant) {\n const { OrgScopingPlugin } = await import('@objectstack/plugin-org-scoping');\n await kernel.use(new OrgScopingPlugin());\n }\n\n // Automation service — opt-in. Registered before bootstrap so its start()\n // phase pulls the app's flows from the ObjectQL registry (populated by\n // AppPlugin.init) and registers them. `memory` suspended-run store keeps the\n // harness free of any manifest/persistence dependency for flow execution.\n if (opts.automation) {\n const { AutomationServicePlugin } = await import('@objectstack/service-automation');\n await kernel.use(new AutomationServicePlugin({ suspendedRunStore: 'memory' }));\n }\n\n await kernel.use(opts.security ?? new SecurityPlugin());\n // Sharing service — apps that declare `requires: ['sharing']` rely on it for\n // record-share grants; without it their RLS/sharing rules are inert and the\n // verifier would under-report authorization.\n await kernel.use(new SharingServicePlugin());\n\n // REST + dispatcher route surfaces (mount onto the http-server service).\n await kernel.use(createRestApiPlugin({ api: { api: { requireAuth: true } } as never }));\n await kernel.use(createDispatcherPlugin({}));\n\n // Fire the ready lifecycle: seed data, dev-admin bootstrap, route registration.\n await kernel.bootstrap();\n\n // Secret fields (Field.secret) refuse to persist without a crypto provider —\n // mirror `objectstack dev`, which wires LocalCryptoProvider in development so\n // an app with an encrypted field is exercisable end-to-end.\n try {\n const engine = await kernel.getServiceAsync<{ setCryptoProvider?: (p: unknown) => void }>('objectql');\n if (engine && typeof engine.setCryptoProvider === 'function') {\n engine.setCryptoProvider(new LocalCryptoProvider());\n }\n } catch {\n /* no engine / no crypto support — secret fields will fail closed, as in prod */\n }\n\n const httpServer = await kernel.getServiceAsync<{ getRawApp(): InjectableApp; close?(): Promise<void> }>(\n 'http-server',\n );\n const app = httpServer.getRawApp();\n\n // Same-origin loopback base for request-injection. A *ported* localhost origin\n // matches better-auth's default dev trusted-origins set (`http://localhost:*`),\n // so the in-process dev-admin sign-in passes the CSRF origin check regardless\n // of runtime (a bare `node` CLI vs a test runner) or ambient CORS env. A\n // path-only inject yields `http://localhost` (no port), which does NOT match\n // the `:*` wildcard and gets a 403. Routing is by path; the host:port only\n // shapes `new URL(request.url).origin`, which the auth layer reads.\n const ORIGIN = 'http://localhost:3000';\n const raw = (path: string, init?: RequestInit) => app.request(`${ORIGIN}${path}`, init);\n const api = (path: string, init?: RequestInit) => raw(`${API_PREFIX}${path}`, init);\n\n const admin = opts.admin ?? { email: DEFAULT_ADMIN_EMAIL, password: DEFAULT_ADMIN_PASSWORD };\n\n const signIn = async (\n email: string = admin.email,\n password: string = admin.password,\n ): Promise<string> => {\n const res = await api('/auth/sign-in/email', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ email, password }),\n });\n if (!res.ok) {\n throw new Error(`verify signIn failed: ${res.status} ${await res.text()}`);\n }\n const data = (await res.json()) as { token?: string };\n if (!data.token) throw new Error('verify signIn: no token in response');\n return data.token;\n };\n\n const signUp = async (\n email: string,\n password = 'Member-Pass-123',\n name?: string,\n ): Promise<string> => {\n const res = await api('/auth/sign-up/email', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ email, password, name: name ?? email.split('@')[0] }),\n });\n if (!res.ok) {\n throw new Error(`verify signUp failed: ${res.status} ${await res.text()}`);\n }\n const data = (await res.json()) as { token?: string };\n if (!data.token) throw new Error('verify signUp: no token in response');\n return data.token;\n };\n\n const apiAs = (token: string, method: string, path: string, body?: unknown) =>\n api(path, {\n method,\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${token}`,\n },\n ...(body !== undefined ? { body: JSON.stringify(body) } : {}),\n });\n\n const stop = async () => {\n try {\n await httpServer.close?.();\n } catch {\n /* best-effort */\n }\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n await (kernel as any).shutdown?.();\n } catch {\n /* best-effort */\n }\n };\n\n return { kernel, api, raw, signIn, signUp, apiAs, stop };\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n//\n// Metadata-driven proof derivation.\n//\n// The platform's apps are 100% declarative metadata, so a baseline runtime\n// contract can be DERIVED from the metadata itself — no hand-written tests. This\n// is the seed of `objectstack verify`: point it at any app (a framework example\n// OR a third-party app like hotcrm) and it auto-generates \"author this object →\n// write it → read it back → assert type fidelity\" for every object, then runs\n// it against the real in-process stack.\n//\n// v0 derived per-object CRUD round-trip cases and SKIPPED any object with a\n// required relation (lookup / master_detail) — it had no target id to write.\n// v1 (ADR-0055 P0) closes that gap with RELATED-RECORD TOPOLOGICAL SYNTHESIS:\n// build the object dependency graph from required relational fields, topologically\n// order it (targets before dependents), and have the runner thread real ids — so\n// relationship-dense objects (the core of real apps) are verified, not skipped.\n// What it still can't satisfy (required-reference cycles, external/missing targets)\n// is reported `blocked` with a precise reason — the gate stays honest.\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\nconst COMPUTED = new Set(['formula', 'summary', 'autonumber', 'rollup', 'vector']);\nconst RELATIONAL = new Set(['lookup', 'master_detail', 'master-detail', 'masterdetail', 'tree']);\nconst STRUCTURED = new Set(['composite', 'repeater', 'record', 'location', 'address']);\nconst MEDIA = new Set(['image', 'file', 'avatar', 'video', 'audio', 'signature', 'qrcode']);\nconst SYSTEM_NAMES = new Set([\n 'id', 'created_at', 'updated_at', 'created_by', 'updated_by', 'owner',\n 'space', 'instance_state', 'record_id', 'is_deleted',\n]);\n\nexport type AssertKind = 'equal' | 'set' | 'none';\nexport interface DerivedAssert {\n field: string;\n type: string;\n value: unknown;\n kind: AssertKind;\n}\n/** A relational field to fill with a real target id at run time (threaded by the runner). */\nexport interface RelationalRef {\n field: string; // the FK field key on this object\n target: string; // the referenced object name\n required: boolean;\n multiple: boolean; // store as an array of ids\n}\nexport interface CrudCase {\n object: string;\n blocked?: string; // why this object can't be auto-CRUD'd (e.g. required-reference cycle)\n body?: Record<string, unknown>;\n asserts?: DerivedAssert[];\n skippedFields?: Array<{ name: string; type: string; reason: string }>;\n relationalRefs?: RelationalRef[]; // resolved against created-record ids by the runner\n}\n\nfunction clampNum(f: any, fallback: number): number {\n const { min, max, step } = f;\n let v = fallback;\n if (typeof min === 'number' && v < min) v = min;\n if (typeof max === 'number' && v > max) v = max;\n if (typeof step === 'number' && typeof min === 'number') {\n v = min + step * Math.round((v - min) / step);\n }\n return v;\n}\n\n/** Synthesize a valid value for a field type, or null if not synthesizable. */\nfunction synth(type: string, f: any): { value: unknown; kind: AssertKind } | null {\n switch (type) {\n case 'text': case 'textarea': case 'string':\n case 'markdown': case 'html': case 'richtext': case 'code':\n return { value: 'verify-sample', kind: 'equal' };\n case 'email': return { value: 'verify@example.com', kind: 'equal' };\n case 'url': return { value: 'https://example.com', kind: 'equal' };\n case 'phone': return { value: '+14155550100', kind: 'equal' };\n case 'color': return { value: '#3366CC', kind: 'equal' };\n case 'number': return { value: clampNum(f, 7), kind: 'equal' };\n case 'currency': return { value: clampNum(f, 100), kind: 'equal' };\n case 'percent': return { value: clampNum(f, 50), kind: 'equal' };\n case 'rating': return { value: clampNum(f, Math.min(3, f.max ?? 5)), kind: 'equal' };\n case 'slider': case 'progress': return { value: clampNum(f, 25), kind: 'equal' };\n case 'boolean': case 'toggle': return { value: true, kind: 'equal' };\n case 'date': return { value: '2024-03-15', kind: 'equal' };\n case 'datetime': return { value: '2024-03-15T08:30:00.000Z', kind: 'equal' };\n case 'time': return { value: '14:30:00', kind: 'equal' };\n case 'json': return { value: { sample: true }, kind: 'equal' };\n case 'select': case 'radio': {\n const opt = f.options?.[0]?.value;\n return opt != null ? { value: opt, kind: 'equal' } : null;\n }\n case 'multiselect': case 'checkboxes': {\n const opt = f.options?.[0]?.value;\n return opt != null ? { value: [opt], kind: 'set' } : null;\n }\n case 'tags': return { value: ['alpha', 'beta'], kind: 'set' };\n // Opaque-on-read: write a value but don't assert a round-trip (hashed/encrypted).\n case 'password': case 'secret': return { value: 'Sample-Secret-123', kind: 'none' };\n default: return null;\n }\n}\n\n/** The target object a relational field references (snake_case object name), or null. */\nfunction relationTarget(f: any): string | null {\n const ref = f?.reference ?? f?.reference_to ?? f?.referenceTo;\n return typeof ref === 'string' && ref.length > 0 ? ref : null;\n}\n\ninterface Draft {\n name: string;\n body: Record<string, unknown>;\n asserts: DerivedAssert[];\n skippedFields: Array<{ name: string; type: string; reason: string }>;\n relationalRefs: RelationalRef[];\n requiredTargets: string[]; // referenced objects that MUST exist + be ordered-before\n blocked?: string;\n}\n\n/**\n * Derive one CRUD round-trip case per authorable object, in DEPENDENCY ORDER.\n *\n * Required relational fields no longer block the object outright: the referenced\n * target is created first (topological order) and the runner threads its real id\n * in. Only genuinely unsatisfiable shapes are `blocked`:\n * - a required relation whose target is missing from the app config (external), or\n * - a required-reference cycle (incl. a required self-reference), or\n * - a required relation whose target is itself blocked (cascade), or\n * - a required non-relational field that can't be synthesized (unchanged from v0).\n */\nexport function deriveCrudCases(config: any): CrudCase[] {\n const objects: any[] = config?.objects ?? [];\n const byName = new Map<string, any>();\n for (const o of objects) if (o?.name) byName.set(o.name, o);\n\n const drafts = new Map<string, Draft>();\n\n // ── Pass 1: per-object field classification ───────────────────────────────\n for (const obj of objects) {\n if (!obj?.name) continue;\n const fields: Record<string, any> = obj?.fields ?? {};\n const d: Draft = {\n name: obj.name, body: {}, asserts: [], skippedFields: [], relationalRefs: [], requiredTargets: [],\n };\n\n for (const [name, f] of Object.entries(fields)) {\n const type = String((f as any)?.type ?? '').toLowerCase();\n const isRequired = !!(f as any)?.required;\n if (SYSTEM_NAMES.has(name) || (f as any)?.system || (f as any)?.readonly) continue;\n if (COMPUTED.has(type)) continue;\n\n if (RELATIONAL.has(type)) {\n const target = relationTarget(f);\n if (!target) {\n if (isRequired) { d.blocked = `required ${type} field \"${name}\" has no \\`reference\\` target`; break; }\n d.skippedFields.push({ name, type, reason: 'relation-missing-reference' });\n continue;\n }\n if (!byName.has(target)) {\n // External / cross-app target — we can't synthesize a record for it.\n if (isRequired) { d.blocked = `required ${type} field \"${name}\" → target \"${target}\" not in app config`; break; }\n d.skippedFields.push({ name, type, reason: `relation-target-external:${target}` });\n continue;\n }\n d.relationalRefs.push({ field: name, target, required: isRequired, multiple: !!(f as any)?.multiple });\n if (isRequired) d.requiredTargets.push(target);\n continue;\n }\n\n if (STRUCTURED.has(type) || MEDIA.has(type)) {\n if (isRequired) { d.blocked = `required ${type} field \"${name}\" (needs structured/media value)`; break; }\n d.skippedFields.push({ name, type, reason: 'unsynthesizable-optional' });\n continue;\n }\n\n const s = synth(type, f);\n if (!s) {\n if (isRequired) { d.blocked = `required field \"${name}\" of type \"${type}\" is not synthesizable`; break; }\n d.skippedFields.push({ name, type, reason: 'no-synth' });\n continue;\n }\n d.body[name] = s.value;\n if (s.kind !== 'none') d.asserts.push({ field: name, type, value: s.value, kind: s.kind });\n }\n\n drafts.set(obj.name, d);\n }\n\n // ── Pass 2: cascade-block on missing/blocked required targets (to fixpoint) ─\n let changed = true;\n while (changed) {\n changed = false;\n for (const d of drafts.values()) {\n if (d.blocked) continue;\n for (const t of d.requiredTargets) {\n const td = drafts.get(t);\n if (!td || td.blocked) {\n d.blocked = `required relational target \"${t}\" is ${!td ? 'missing' : 'not synthesizable'}`;\n changed = true;\n break;\n }\n }\n }\n }\n\n // ── Pass 3: topological order (targets before dependents) over required edges ─\n const emitted = new Set<string>();\n const order: string[] = [];\n const live = [...drafts.values()].filter((d) => !d.blocked).map((d) => d.name);\n let progress = true;\n while (progress) {\n progress = false;\n for (const name of live) {\n if (emitted.has(name)) continue;\n const d = drafts.get(name)!;\n if (d.requiredTargets.every((t) => emitted.has(t))) {\n emitted.add(name);\n order.push(name);\n progress = true;\n }\n }\n }\n // Residue = a required-reference cycle (incl. required self-reference).\n for (const name of live) {\n if (!emitted.has(name)) drafts.get(name)!.blocked = 'unsatisfiable required-reference cycle';\n }\n\n // ── Assemble: ordered live cases first, then blocked ones ──────────────────\n const cases: CrudCase[] = [];\n for (const name of order) {\n const d = drafts.get(name)!;\n cases.push({\n object: d.name,\n body: d.body,\n asserts: d.asserts,\n skippedFields: d.skippedFields,\n ...(d.relationalRefs.length ? { relationalRefs: d.relationalRefs } : {}),\n });\n }\n for (const d of drafts.values()) {\n if (d.blocked) cases.push({ object: d.name, blocked: d.blocked });\n }\n return cases;\n}\n\n/**\n * Resolve a case's relational fields against the registry of already-created\n * record ids, returning the body to POST. When a REQUIRED target has no created\n * record (its own creation failed at run time), returns a `missing` reason so the\n * caller can skip rather than POST an invalid body.\n */\nexport function fillRelationalRefs(\n c: CrudCase,\n created: Map<string, string>,\n): { body: Record<string, unknown>; missing?: string } {\n const body: Record<string, unknown> = { ...(c.body ?? {}) };\n for (const ref of c.relationalRefs ?? []) {\n const id = created.get(ref.target);\n if (id == null) {\n if (ref.required) return { body, missing: `required relation \"${ref.field}\" → no created \"${ref.target}\" record` };\n continue; // optional: leave unset\n }\n body[ref.field] = ref.multiple ? [id] : id;\n }\n return { body };\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n//\n// The verify runner — executes derived proofs against a live dogfood stack.\n//\n// This is what a consumer (a framework example OR a third-party app like hotcrm)\n// would run: boot my app in-process, auto-derive a runtime contract from my\n// metadata, exercise it through the real HTTP surface, and tell me where the\n// declared behavior doesn't actually hold at runtime.\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\nimport type { VerifyStack } from './harness.js';\nimport { deriveCrudCases, fillRelationalRefs, type CrudCase } from './derive.js';\n\nexport interface ObjectVerifyResult {\n object: string;\n status: 'verified' | 'fidelity-gaps' | 'create-failed' | 'read-failed' | 'skipped' | 'needs-fixture';\n checked?: number;\n reason?: string;\n code?: number;\n detail?: string;\n mismatches?: Array<{ field: string; type: string; wrote: unknown; read: unknown }>;\n}\n\nexport interface VerifyReport {\n app: string;\n results: ObjectVerifyResult[];\n summary: {\n objects: number;\n verified: number;\n fidelityGaps: number;\n createFailed: number;\n needsFixture: number;\n readFailed: number;\n skipped: number;\n mismatchTotal: number;\n };\n}\n\nfunction setEqual(a: unknown, b: unknown[]): boolean {\n if (!Array.isArray(a)) return false;\n return JSON.stringify([...a].sort()) === JSON.stringify([...b].sort());\n}\nfunction deepEqual(a: unknown, b: unknown): boolean {\n return JSON.stringify(a) === JSON.stringify(b);\n}\n\n/**\n * Run auto-derived CRUD round-trip proofs for every object in `config`, as the\n * authenticated `token`, against the live `stack`. Returns a structured report;\n * never throws on a per-object failure (collects them).\n */\nexport async function runCrudVerification(\n stack: VerifyStack,\n token: string,\n config: any,\n): Promise<VerifyReport> {\n const cases = deriveCrudCases(config);\n const results: ObjectVerifyResult[] = [];\n // Registry of created record ids, threaded so a dependent object's required\n // relation is filled with a real target id (topological order guarantees the\n // target was created first). One instance per object — reused across refs.\n const createdIds = new Map<string, string>();\n\n for (const c of cases as CrudCase[]) {\n if (c.blocked) {\n results.push({ object: c.object, status: 'skipped', reason: c.blocked });\n continue;\n }\n const { body, missing } = fillRelationalRefs(c, createdIds);\n if (missing) {\n results.push({ object: c.object, status: 'skipped', reason: missing });\n continue;\n }\n let created: Response;\n try {\n created = await stack.apiAs(token, 'POST', `/data/${c.object}`, body);\n } catch (e: any) {\n results.push({ object: c.object, status: 'create-failed', detail: String(e?.message ?? e).slice(0, 200) });\n continue;\n }\n if (created.status >= 300) {\n const text = (await created.text()).slice(0, 280);\n // A 400 validation failure means our auto-derived record didn't satisfy the\n // app's own validation rules (record-level format/json-schema/CEL) — that's\n // a fixture gap, not a platform finding. A 5xx (or crypto/driver error) is a\n // real runtime failure the app's author needs to see.\n const isValidation = created.status === 400 && /VALIDATION_FAILED|Validation failed/i.test(text);\n results.push({\n object: c.object,\n status: isValidation ? 'needs-fixture' : 'create-failed',\n code: created.status,\n detail: text,\n });\n continue;\n }\n const cj = (await created.json()) as any;\n const id = cj?.id ?? cj?.record?.id;\n if (!id) {\n results.push({ object: c.object, status: 'create-failed', detail: 'no id returned' });\n continue;\n }\n createdIds.set(c.object, String(id));\n\n const got = await stack.apiAs(token, 'GET', `/data/${c.object}/${id}`);\n if (got.status !== 200) {\n results.push({ object: c.object, status: 'read-failed', code: got.status });\n continue;\n }\n const rec = ((await got.json()) as any)?.record ?? {};\n\n const mismatches: ObjectVerifyResult['mismatches'] = [];\n for (const a of c.asserts ?? []) {\n const actual = rec[a.field];\n const ok = a.kind === 'set' ? setEqual(actual, a.value as unknown[]) : deepEqual(actual, a.value);\n if (!ok) mismatches.push({ field: a.field, type: a.type, wrote: a.value, read: actual });\n }\n results.push({\n object: c.object,\n status: mismatches.length ? 'fidelity-gaps' : 'verified',\n checked: (c.asserts ?? []).length,\n ...(mismatches.length ? { mismatches } : {}),\n });\n }\n\n const summary = {\n objects: results.length,\n verified: results.filter((r) => r.status === 'verified').length,\n fidelityGaps: results.filter((r) => r.status === 'fidelity-gaps').length,\n createFailed: results.filter((r) => r.status === 'create-failed').length,\n needsFixture: results.filter((r) => r.status === 'needs-fixture').length,\n readFailed: results.filter((r) => r.status === 'read-failed').length,\n skipped: results.filter((r) => r.status === 'skipped').length,\n mismatchTotal: results.reduce((n, r) => n + (r.mismatches?.length ?? 0), 0),\n };\n return { app: config?.manifest?.id ?? config?.manifest?.namespace ?? 'app', results, summary };\n}\n\n/** Pretty one-line-per-object summary for logs. */\nexport function formatReport(report: VerifyReport): string {\n const lines: string[] = [`\\n=== objectstack verify — ${report.app} ===`];\n for (const r of report.results) {\n if (r.status === 'verified') lines.push(` ✓ ${r.object} (${r.checked} fields)`);\n else if (r.status === 'fidelity-gaps') {\n lines.push(` ⚠ ${r.object} ${r.mismatches!.length} fidelity gap(s):`);\n for (const m of r.mismatches!) lines.push(` ${m.field} <${m.type}>: wrote ${JSON.stringify(m.wrote)} → read ${JSON.stringify(m.read)}`);\n }\n else if (r.status === 'skipped') lines.push(` – ${r.object} skipped: ${r.reason}`);\n else if (r.status === 'needs-fixture') lines.push(` ~ ${r.object} needs-fixture (app validation rejected the auto-record): ${(r.detail ?? '').slice(0,120)}`);\n else lines.push(` ✗ ${r.object} ${r.status}${r.code ? ` (${r.code})` : ''}: ${r.detail ?? ''}`);\n }\n const s = report.summary;\n lines.push(` ── ${s.verified} verified, ${s.fidelityGaps} gaps, ${s.createFailed + s.readFailed} FAILED, ${s.needsFixture} needs-fixture, ${s.skipped} skipped (${s.mismatchTotal} mismatches)`);\n return lines.join('\\n');\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n//\n// Metadata-driven RLS cross-owner proofs — the #1994 invariant.\n//\n// #1994 (\"member edits others' records\") was a by-id write that skipped the\n// row-level predicate: `driver.update(object, id, …)` builds no AST, so RLS\n// never scoped it. The clean, app-agnostic invariant that catches it without\n// interpreting each sharing rule:\n//\n// A user who CANNOT READ a record must not be able to WRITE it.\n// (\"You can't mutate what you can't see.\")\n//\n// Derivation, per object: admin creates a record; a fresh member (no roles or\n// grants) tries to read it, then tries to mutate it by id; we re-read as admin\n// to see if the row actually changed. If the member couldn't see it yet changed\n// it, that's the #1994 class of hole — regardless of the app's sharing config.\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\nimport type { VerifyStack } from './harness.js';\nimport { deriveCrudCases, fillRelationalRefs } from './derive.js';\n\nconst PROBE_TYPES = new Set(['text', 'textarea', 'string']);\nconst MUTATION = 'rls-mutated-by-B';\n\nexport interface RlsResult {\n object: string;\n status: 'rls-consistent' | 'rls-hole' | 'member-visible' | 'skipped';\n detail?: string;\n}\n\nexport interface RlsReport {\n app: string;\n results: RlsResult[];\n summary: { objects: number; consistent: number; holes: number; memberVisible: number; skipped: number };\n}\n\nexport async function runRlsProofs(\n stack: VerifyStack,\n adminToken: string,\n memberToken: string,\n config: any,\n): Promise<RlsReport> {\n const cases = deriveCrudCases(config);\n const results: RlsResult[] = [];\n // Admin-created ids, threaded so a detail's required relation points at a real\n // master (topological order created it first) — lets the #1994 invariant reach\n // relationship-dense objects, not just the leaves.\n const createdIds = new Map<string, string>();\n\n for (const c of cases) {\n if (c.blocked) { results.push({ object: c.object, status: 'skipped', detail: c.blocked }); continue; }\n\n // A plain-text field to mutate (avoid email/url/phone — their format checks\n // would reject the probe for a benign reason, masking the RLS signal).\n const probe = (c.asserts ?? []).find((a) => PROBE_TYPES.has(a.type));\n if (!probe) { results.push({ object: c.object, status: 'skipped', detail: 'no plain-text probe field' }); continue; }\n\n const { body, missing } = fillRelationalRefs(c, createdIds);\n if (missing) { results.push({ object: c.object, status: 'skipped', detail: missing }); continue; }\n\n // Admin (owner) creates the record.\n const created = await stack.apiAs(adminToken, 'POST', `/data/${c.object}`, body);\n if (created.status >= 300) {\n results.push({ object: c.object, status: 'skipped', detail: `admin create failed (${created.status})` });\n continue;\n }\n const cj = (await created.json()) as any;\n const id = cj?.id ?? cj?.record?.id;\n if (!id) { results.push({ object: c.object, status: 'skipped', detail: 'no id from create' }); continue; }\n createdIds.set(c.object, String(id));\n\n // Member B: can they SEE it?\n const bRead = await stack.apiAs(memberToken, 'GET', `/data/${c.object}/${id}`);\n let canRead = false;\n if (bRead.status === 200) {\n const rec = ((await bRead.json()) as any)?.record;\n canRead = !!rec && rec.id === id;\n }\n\n // Member B: try to MUTATE it by id.\n const bWrite = await stack.apiAs(memberToken, 'PATCH', `/data/${c.object}/${id}`, { [probe.field]: MUTATION });\n\n // Ground truth: re-read as admin — did the row actually change?\n const after = await stack.apiAs(adminToken, 'GET', `/data/${c.object}/${id}`);\n const afterVal = (((await after.json()) as any)?.record ?? {})[probe.field];\n const changed = afterVal === MUTATION;\n\n if (canRead) {\n results.push({ object: c.object, status: 'member-visible', detail: 'member can read this object — not a cross-owner scenario (no RLS isolation, or read is granted)' });\n } else if (changed) {\n results.push({\n object: c.object,\n status: 'rls-hole',\n detail: `member B cannot read it (GET ${bRead.status}) yet MUTATED it by id (PATCH ${bWrite.status}) — by-id write bypassed RLS (#1994 class)`,\n });\n } else {\n results.push({\n object: c.object,\n status: 'rls-consistent',\n detail: `member B cannot read (GET ${bRead.status}) and could not mutate (PATCH ${bWrite.status}, row unchanged)`,\n });\n }\n }\n\n const summary = {\n objects: results.length,\n consistent: results.filter((r) => r.status === 'rls-consistent').length,\n holes: results.filter((r) => r.status === 'rls-hole').length,\n memberVisible: results.filter((r) => r.status === 'member-visible').length,\n skipped: results.filter((r) => r.status === 'skipped').length,\n };\n return { app: config?.manifest?.id ?? 'app', results, summary };\n}\n\nexport function formatRlsReport(report: RlsReport): string {\n const lines: string[] = [`\\n=== objectstack verify (RLS / #1994) — ${report.app} ===`];\n for (const r of report.results) {\n const mark = r.status === 'rls-hole' ? '✗✗' : r.status === 'rls-consistent' ? '✓' : r.status === 'member-visible' ? '·' : '–';\n lines.push(` ${mark} ${r.object} [${r.status}] ${r.detail ?? ''}`);\n }\n const s = report.summary;\n lines.push(` ── ${s.consistent} consistent, ${s.holes} HOLES, ${s.memberVisible} member-visible, ${s.skipped} skipped`);\n return lines.join('\\n');\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { ObjectKernel } from '@objectstack/runtime';
|
|
2
|
+
import { SecurityPlugin } from '@objectstack/plugin-security';
|
|
3
|
+
|
|
4
|
+
interface VerifyStack {
|
|
5
|
+
/** The booted kernel — for direct service calls when bypassing HTTP is intentional. */
|
|
6
|
+
kernel: ObjectKernel;
|
|
7
|
+
/** Inject an HTTP request through the real Hono app (no socket). Path is relative to `/api/v1`. */
|
|
8
|
+
api(path: string, init?: RequestInit): Promise<Response>;
|
|
9
|
+
/** Inject a request at an absolute path (e.g. `/api/settings/...`). */
|
|
10
|
+
raw(path: string, init?: RequestInit): Promise<Response>;
|
|
11
|
+
/** Sign in through the real auth route; returns a bearer token. Defaults to the dev admin. */
|
|
12
|
+
signIn(email?: string, password?: string): Promise<string>;
|
|
13
|
+
/** Sign up a NEW user through the real auth route; returns their bearer token.
|
|
14
|
+
* The first user is the seeded dev admin, so a fresh sign-up is a plain member
|
|
15
|
+
* (no roles/grants) — exactly what RLS cross-owner proofs need. */
|
|
16
|
+
signUp(email: string, password?: string, name?: string): Promise<string>;
|
|
17
|
+
/** Convenience: an authed JSON request relative to `/api/v1`. */
|
|
18
|
+
apiAs(token: string, method: string, path: string, body?: unknown): Promise<Response>;
|
|
19
|
+
/** Tear down the kernel (close DB / HTTP handles). */
|
|
20
|
+
stop(): Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
interface BootOptions {
|
|
23
|
+
/** Override the dev admin credentials the harness signs in with. */
|
|
24
|
+
admin?: {
|
|
25
|
+
email: string;
|
|
26
|
+
password: string;
|
|
27
|
+
};
|
|
28
|
+
/** Override the auth signing secret. Defaults to a fixed in-process dev secret. */
|
|
29
|
+
authSecret?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Override the SecurityPlugin instance. Pass a `new SecurityPlugin({...})`
|
|
32
|
+
* to carry a custom `fallbackPermissionSet` / extra permission sets — this
|
|
33
|
+
* is how an owner-isolated RLS fixture makes a fresh member fall back to a
|
|
34
|
+
* permission set that carries `RLS.ownerPolicy(...)` instead of the broad-read
|
|
35
|
+
* `member_default`. Defaults to a vanilla `new SecurityPlugin()`.
|
|
36
|
+
*/
|
|
37
|
+
security?: SecurityPlugin;
|
|
38
|
+
/**
|
|
39
|
+
* Boot multi-tenant: register `@objectstack/plugin-org-scoping` BEFORE the
|
|
40
|
+
* SecurityPlugin so the wildcard `organization_id` RLS policies that ship in
|
|
41
|
+
* the default permission sets actually apply (SecurityPlugin probes the
|
|
42
|
+
* `org-scoping` service once at start and otherwise STRIPS them — see
|
|
43
|
+
* `collectRLSPolicies`). This exercises the org-scoped isolation real apps
|
|
44
|
+
* rely on, rather than the single-tenant default where every tenant policy is
|
|
45
|
+
* stripped and a member sees every row. Default `false`.
|
|
46
|
+
*/
|
|
47
|
+
multiTenant?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Register `@objectstack/service-automation` so authored flows execute against
|
|
50
|
+
* the real stack. The plugin seeds the built-in node executors and, at start(),
|
|
51
|
+
* pulls every flow in the app config from the ObjectQL registry and registers
|
|
52
|
+
* it — so `POST /api/v1/automation/:name/trigger` actually runs the flow's
|
|
53
|
+
* nodes. Without this the dispatcher's automation routes resolve no `automation`
|
|
54
|
+
* service and flow execution is unreachable. Opt-in (like `multiTenant`) so the
|
|
55
|
+
* default boot stays lean for apps that don't exercise flows. Default `false`.
|
|
56
|
+
*/
|
|
57
|
+
automation?: boolean;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Boot an app config in-process and return a live verification stack.
|
|
61
|
+
*
|
|
62
|
+
* `NODE_ENV` is forced to `development` so the auth plugin's dev-admin
|
|
63
|
+
* bootstrap provisions a known, loginable admin (mirrors `objectstack dev`).
|
|
64
|
+
*/
|
|
65
|
+
declare function bootStack(config: any, opts?: BootOptions): Promise<VerifyStack>;
|
|
66
|
+
|
|
67
|
+
type AssertKind = 'equal' | 'set' | 'none';
|
|
68
|
+
interface DerivedAssert {
|
|
69
|
+
field: string;
|
|
70
|
+
type: string;
|
|
71
|
+
value: unknown;
|
|
72
|
+
kind: AssertKind;
|
|
73
|
+
}
|
|
74
|
+
/** A relational field to fill with a real target id at run time (threaded by the runner). */
|
|
75
|
+
interface RelationalRef {
|
|
76
|
+
field: string;
|
|
77
|
+
target: string;
|
|
78
|
+
required: boolean;
|
|
79
|
+
multiple: boolean;
|
|
80
|
+
}
|
|
81
|
+
interface CrudCase {
|
|
82
|
+
object: string;
|
|
83
|
+
blocked?: string;
|
|
84
|
+
body?: Record<string, unknown>;
|
|
85
|
+
asserts?: DerivedAssert[];
|
|
86
|
+
skippedFields?: Array<{
|
|
87
|
+
name: string;
|
|
88
|
+
type: string;
|
|
89
|
+
reason: string;
|
|
90
|
+
}>;
|
|
91
|
+
relationalRefs?: RelationalRef[];
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Derive one CRUD round-trip case per authorable object, in DEPENDENCY ORDER.
|
|
95
|
+
*
|
|
96
|
+
* Required relational fields no longer block the object outright: the referenced
|
|
97
|
+
* target is created first (topological order) and the runner threads its real id
|
|
98
|
+
* in. Only genuinely unsatisfiable shapes are `blocked`:
|
|
99
|
+
* - a required relation whose target is missing from the app config (external), or
|
|
100
|
+
* - a required-reference cycle (incl. a required self-reference), or
|
|
101
|
+
* - a required relation whose target is itself blocked (cascade), or
|
|
102
|
+
* - a required non-relational field that can't be synthesized (unchanged from v0).
|
|
103
|
+
*/
|
|
104
|
+
declare function deriveCrudCases(config: any): CrudCase[];
|
|
105
|
+
/**
|
|
106
|
+
* Resolve a case's relational fields against the registry of already-created
|
|
107
|
+
* record ids, returning the body to POST. When a REQUIRED target has no created
|
|
108
|
+
* record (its own creation failed at run time), returns a `missing` reason so the
|
|
109
|
+
* caller can skip rather than POST an invalid body.
|
|
110
|
+
*/
|
|
111
|
+
declare function fillRelationalRefs(c: CrudCase, created: Map<string, string>): {
|
|
112
|
+
body: Record<string, unknown>;
|
|
113
|
+
missing?: string;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
interface ObjectVerifyResult {
|
|
117
|
+
object: string;
|
|
118
|
+
status: 'verified' | 'fidelity-gaps' | 'create-failed' | 'read-failed' | 'skipped' | 'needs-fixture';
|
|
119
|
+
checked?: number;
|
|
120
|
+
reason?: string;
|
|
121
|
+
code?: number;
|
|
122
|
+
detail?: string;
|
|
123
|
+
mismatches?: Array<{
|
|
124
|
+
field: string;
|
|
125
|
+
type: string;
|
|
126
|
+
wrote: unknown;
|
|
127
|
+
read: unknown;
|
|
128
|
+
}>;
|
|
129
|
+
}
|
|
130
|
+
interface VerifyReport {
|
|
131
|
+
app: string;
|
|
132
|
+
results: ObjectVerifyResult[];
|
|
133
|
+
summary: {
|
|
134
|
+
objects: number;
|
|
135
|
+
verified: number;
|
|
136
|
+
fidelityGaps: number;
|
|
137
|
+
createFailed: number;
|
|
138
|
+
needsFixture: number;
|
|
139
|
+
readFailed: number;
|
|
140
|
+
skipped: number;
|
|
141
|
+
mismatchTotal: number;
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Run auto-derived CRUD round-trip proofs for every object in `config`, as the
|
|
146
|
+
* authenticated `token`, against the live `stack`. Returns a structured report;
|
|
147
|
+
* never throws on a per-object failure (collects them).
|
|
148
|
+
*/
|
|
149
|
+
declare function runCrudVerification(stack: VerifyStack, token: string, config: any): Promise<VerifyReport>;
|
|
150
|
+
/** Pretty one-line-per-object summary for logs. */
|
|
151
|
+
declare function formatReport(report: VerifyReport): string;
|
|
152
|
+
|
|
153
|
+
interface RlsResult {
|
|
154
|
+
object: string;
|
|
155
|
+
status: 'rls-consistent' | 'rls-hole' | 'member-visible' | 'skipped';
|
|
156
|
+
detail?: string;
|
|
157
|
+
}
|
|
158
|
+
interface RlsReport {
|
|
159
|
+
app: string;
|
|
160
|
+
results: RlsResult[];
|
|
161
|
+
summary: {
|
|
162
|
+
objects: number;
|
|
163
|
+
consistent: number;
|
|
164
|
+
holes: number;
|
|
165
|
+
memberVisible: number;
|
|
166
|
+
skipped: number;
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
declare function runRlsProofs(stack: VerifyStack, adminToken: string, memberToken: string, config: any): Promise<RlsReport>;
|
|
170
|
+
declare function formatRlsReport(report: RlsReport): string;
|
|
171
|
+
|
|
172
|
+
export { type AssertKind, type BootOptions, type CrudCase, type DerivedAssert, type ObjectVerifyResult, type RelationalRef, type RlsReport, type RlsResult, type VerifyReport, type VerifyStack, bootStack, deriveCrudCases, fillRelationalRefs, formatReport, formatRlsReport, runCrudVerification, runRlsProofs };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { ObjectKernel } from '@objectstack/runtime';
|
|
2
|
+
import { SecurityPlugin } from '@objectstack/plugin-security';
|
|
3
|
+
|
|
4
|
+
interface VerifyStack {
|
|
5
|
+
/** The booted kernel — for direct service calls when bypassing HTTP is intentional. */
|
|
6
|
+
kernel: ObjectKernel;
|
|
7
|
+
/** Inject an HTTP request through the real Hono app (no socket). Path is relative to `/api/v1`. */
|
|
8
|
+
api(path: string, init?: RequestInit): Promise<Response>;
|
|
9
|
+
/** Inject a request at an absolute path (e.g. `/api/settings/...`). */
|
|
10
|
+
raw(path: string, init?: RequestInit): Promise<Response>;
|
|
11
|
+
/** Sign in through the real auth route; returns a bearer token. Defaults to the dev admin. */
|
|
12
|
+
signIn(email?: string, password?: string): Promise<string>;
|
|
13
|
+
/** Sign up a NEW user through the real auth route; returns their bearer token.
|
|
14
|
+
* The first user is the seeded dev admin, so a fresh sign-up is a plain member
|
|
15
|
+
* (no roles/grants) — exactly what RLS cross-owner proofs need. */
|
|
16
|
+
signUp(email: string, password?: string, name?: string): Promise<string>;
|
|
17
|
+
/** Convenience: an authed JSON request relative to `/api/v1`. */
|
|
18
|
+
apiAs(token: string, method: string, path: string, body?: unknown): Promise<Response>;
|
|
19
|
+
/** Tear down the kernel (close DB / HTTP handles). */
|
|
20
|
+
stop(): Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
interface BootOptions {
|
|
23
|
+
/** Override the dev admin credentials the harness signs in with. */
|
|
24
|
+
admin?: {
|
|
25
|
+
email: string;
|
|
26
|
+
password: string;
|
|
27
|
+
};
|
|
28
|
+
/** Override the auth signing secret. Defaults to a fixed in-process dev secret. */
|
|
29
|
+
authSecret?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Override the SecurityPlugin instance. Pass a `new SecurityPlugin({...})`
|
|
32
|
+
* to carry a custom `fallbackPermissionSet` / extra permission sets — this
|
|
33
|
+
* is how an owner-isolated RLS fixture makes a fresh member fall back to a
|
|
34
|
+
* permission set that carries `RLS.ownerPolicy(...)` instead of the broad-read
|
|
35
|
+
* `member_default`. Defaults to a vanilla `new SecurityPlugin()`.
|
|
36
|
+
*/
|
|
37
|
+
security?: SecurityPlugin;
|
|
38
|
+
/**
|
|
39
|
+
* Boot multi-tenant: register `@objectstack/plugin-org-scoping` BEFORE the
|
|
40
|
+
* SecurityPlugin so the wildcard `organization_id` RLS policies that ship in
|
|
41
|
+
* the default permission sets actually apply (SecurityPlugin probes the
|
|
42
|
+
* `org-scoping` service once at start and otherwise STRIPS them — see
|
|
43
|
+
* `collectRLSPolicies`). This exercises the org-scoped isolation real apps
|
|
44
|
+
* rely on, rather than the single-tenant default where every tenant policy is
|
|
45
|
+
* stripped and a member sees every row. Default `false`.
|
|
46
|
+
*/
|
|
47
|
+
multiTenant?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Register `@objectstack/service-automation` so authored flows execute against
|
|
50
|
+
* the real stack. The plugin seeds the built-in node executors and, at start(),
|
|
51
|
+
* pulls every flow in the app config from the ObjectQL registry and registers
|
|
52
|
+
* it — so `POST /api/v1/automation/:name/trigger` actually runs the flow's
|
|
53
|
+
* nodes. Without this the dispatcher's automation routes resolve no `automation`
|
|
54
|
+
* service and flow execution is unreachable. Opt-in (like `multiTenant`) so the
|
|
55
|
+
* default boot stays lean for apps that don't exercise flows. Default `false`.
|
|
56
|
+
*/
|
|
57
|
+
automation?: boolean;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Boot an app config in-process and return a live verification stack.
|
|
61
|
+
*
|
|
62
|
+
* `NODE_ENV` is forced to `development` so the auth plugin's dev-admin
|
|
63
|
+
* bootstrap provisions a known, loginable admin (mirrors `objectstack dev`).
|
|
64
|
+
*/
|
|
65
|
+
declare function bootStack(config: any, opts?: BootOptions): Promise<VerifyStack>;
|
|
66
|
+
|
|
67
|
+
type AssertKind = 'equal' | 'set' | 'none';
|
|
68
|
+
interface DerivedAssert {
|
|
69
|
+
field: string;
|
|
70
|
+
type: string;
|
|
71
|
+
value: unknown;
|
|
72
|
+
kind: AssertKind;
|
|
73
|
+
}
|
|
74
|
+
/** A relational field to fill with a real target id at run time (threaded by the runner). */
|
|
75
|
+
interface RelationalRef {
|
|
76
|
+
field: string;
|
|
77
|
+
target: string;
|
|
78
|
+
required: boolean;
|
|
79
|
+
multiple: boolean;
|
|
80
|
+
}
|
|
81
|
+
interface CrudCase {
|
|
82
|
+
object: string;
|
|
83
|
+
blocked?: string;
|
|
84
|
+
body?: Record<string, unknown>;
|
|
85
|
+
asserts?: DerivedAssert[];
|
|
86
|
+
skippedFields?: Array<{
|
|
87
|
+
name: string;
|
|
88
|
+
type: string;
|
|
89
|
+
reason: string;
|
|
90
|
+
}>;
|
|
91
|
+
relationalRefs?: RelationalRef[];
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Derive one CRUD round-trip case per authorable object, in DEPENDENCY ORDER.
|
|
95
|
+
*
|
|
96
|
+
* Required relational fields no longer block the object outright: the referenced
|
|
97
|
+
* target is created first (topological order) and the runner threads its real id
|
|
98
|
+
* in. Only genuinely unsatisfiable shapes are `blocked`:
|
|
99
|
+
* - a required relation whose target is missing from the app config (external), or
|
|
100
|
+
* - a required-reference cycle (incl. a required self-reference), or
|
|
101
|
+
* - a required relation whose target is itself blocked (cascade), or
|
|
102
|
+
* - a required non-relational field that can't be synthesized (unchanged from v0).
|
|
103
|
+
*/
|
|
104
|
+
declare function deriveCrudCases(config: any): CrudCase[];
|
|
105
|
+
/**
|
|
106
|
+
* Resolve a case's relational fields against the registry of already-created
|
|
107
|
+
* record ids, returning the body to POST. When a REQUIRED target has no created
|
|
108
|
+
* record (its own creation failed at run time), returns a `missing` reason so the
|
|
109
|
+
* caller can skip rather than POST an invalid body.
|
|
110
|
+
*/
|
|
111
|
+
declare function fillRelationalRefs(c: CrudCase, created: Map<string, string>): {
|
|
112
|
+
body: Record<string, unknown>;
|
|
113
|
+
missing?: string;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
interface ObjectVerifyResult {
|
|
117
|
+
object: string;
|
|
118
|
+
status: 'verified' | 'fidelity-gaps' | 'create-failed' | 'read-failed' | 'skipped' | 'needs-fixture';
|
|
119
|
+
checked?: number;
|
|
120
|
+
reason?: string;
|
|
121
|
+
code?: number;
|
|
122
|
+
detail?: string;
|
|
123
|
+
mismatches?: Array<{
|
|
124
|
+
field: string;
|
|
125
|
+
type: string;
|
|
126
|
+
wrote: unknown;
|
|
127
|
+
read: unknown;
|
|
128
|
+
}>;
|
|
129
|
+
}
|
|
130
|
+
interface VerifyReport {
|
|
131
|
+
app: string;
|
|
132
|
+
results: ObjectVerifyResult[];
|
|
133
|
+
summary: {
|
|
134
|
+
objects: number;
|
|
135
|
+
verified: number;
|
|
136
|
+
fidelityGaps: number;
|
|
137
|
+
createFailed: number;
|
|
138
|
+
needsFixture: number;
|
|
139
|
+
readFailed: number;
|
|
140
|
+
skipped: number;
|
|
141
|
+
mismatchTotal: number;
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Run auto-derived CRUD round-trip proofs for every object in `config`, as the
|
|
146
|
+
* authenticated `token`, against the live `stack`. Returns a structured report;
|
|
147
|
+
* never throws on a per-object failure (collects them).
|
|
148
|
+
*/
|
|
149
|
+
declare function runCrudVerification(stack: VerifyStack, token: string, config: any): Promise<VerifyReport>;
|
|
150
|
+
/** Pretty one-line-per-object summary for logs. */
|
|
151
|
+
declare function formatReport(report: VerifyReport): string;
|
|
152
|
+
|
|
153
|
+
interface RlsResult {
|
|
154
|
+
object: string;
|
|
155
|
+
status: 'rls-consistent' | 'rls-hole' | 'member-visible' | 'skipped';
|
|
156
|
+
detail?: string;
|
|
157
|
+
}
|
|
158
|
+
interface RlsReport {
|
|
159
|
+
app: string;
|
|
160
|
+
results: RlsResult[];
|
|
161
|
+
summary: {
|
|
162
|
+
objects: number;
|
|
163
|
+
consistent: number;
|
|
164
|
+
holes: number;
|
|
165
|
+
memberVisible: number;
|
|
166
|
+
skipped: number;
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
declare function runRlsProofs(stack: VerifyStack, adminToken: string, memberToken: string, config: any): Promise<RlsReport>;
|
|
170
|
+
declare function formatRlsReport(report: RlsReport): string;
|
|
171
|
+
|
|
172
|
+
export { type AssertKind, type BootOptions, type CrudCase, type DerivedAssert, type ObjectVerifyResult, type RelationalRef, type RlsReport, type RlsResult, type VerifyReport, type VerifyStack, bootStack, deriveCrudCases, fillRelationalRefs, formatReport, formatRlsReport, runCrudVerification, runRlsProofs };
|