/** * How the `ifc` labels of one value combine when more than one declaration * gives it some. * * A value is labeled more than once when CFC wrappers nest * (`Confidential, B>`), and when a wrapper goes around a * named type whose definition is itself labeled: the wrapper's label is then * written beside the `$ref` to that definition. Confidentiality labels join, * since the value is confidential under every label any wrapper gave it. The * other `ifc` keys have no agreed way to combine yet, so two declarations of * one must agree, or generation fails; keeping one and dropping the other * would be silent. * * The label beside a `$ref` states the labels of the definitions it reaches as * well as its own. Resolving a reference lets each keyword beside it replace * the definition's, and `ifc` is one keyword, so a label left out there is a * label the resolved schema does not have. */ import type { MutableJSONSchema, MutableJSONSchemaObj, } from "@commonfabric/api"; import { debugStr, type FabricValue, valueEqual, } from "@commonfabric/data-model"; import { forEachSubschema } from "@commonfabric/data-model-schema/schema-walk"; import { isObjectOrArray } from "@commonfabric/utils/types"; import { dedupeByValueEqual } from "./value-equality.ts"; type IfcLabels = Readonly>; const LOCAL_DEFINITION_PREFIX = "#/$defs/"; /** * The labels of one value that `inner` and then `outer` both declared: * confidentiality joined, every other key declared by one of them or alike by * both. A key declared as `undefined` (a label the lowering could not read) * counts as not declared. */ export const combineIfcLabels = ( inner: IfcLabels, outer: IfcLabels, ): Record => { const combined: Record = { ...inner }; for (const key of Object.keys(outer)) { const declared = outer[key]; if (declared === undefined) continue; const existing = combined[key]; if (existing === undefined) { combined[key] = declared; } else if ( key === "confidentiality" && Array.isArray(existing) && Array.isArray(declared) ) { combined[key] = dedupeByValueEqual( [...existing, ...declared] as FabricValue[], ); } else if ( !valueEqual(existing as FabricValue, declared as FabricValue) ) { throw new Error( debugStr`One value declares \`ifc.${key}\` twice, as ` + debugStr`$quote${existing} and as $quote${declared}. Only ` + `confidentiality labels combine; declare this one once.`, ); } } return combined; }; /** * Whether `held` already declares every label `labels` does: each * confidentiality atom, and every other key alike. */ export const holdsIfcLabels = ( held: IfcLabels, labels: IfcLabels, ): boolean => Object.entries(labels).every(([key, declared]) => { if (declared === undefined) return true; const existing = held[key]; return key === "confidentiality" && Array.isArray(existing) && Array.isArray(declared) ? declared.every((atom) => existing.some((have) => valueEqual(have as FabricValue, atom as FabricValue) ) ) : existing !== undefined && valueEqual(existing as FabricValue, declared as FabricValue); }); /** * The labels of a value that may be any one of `members`, a union's, under * the union's own `labels`: every confidentiality atom any of them declares, * the union's other labels, and each other label every member declares alike. * A member's other labels bind only while the value is that member. */ export const joinMemberIfcLabels = ( labels: IfcLabels, members: readonly IfcLabels[], ): Record | undefined => { const joined: Record = { ...labels }; const atoms = [labels, ...members].flatMap(({ confidentiality }) => Array.isArray(confidentiality) ? confidentiality : [] ); if (atoms.length > 0) { joined.confidentiality = dedupeByValueEqual(atoms as FabricValue[]); } const [first, ...rest] = members; for (const [key, declared] of Object.entries(first ?? {})) { if (key === "confidentiality" || declared === undefined || key in joined) { continue; } if ( rest.every((member) => member[key] !== undefined && valueEqual(member[key] as FabricValue, declared as FabricValue) ) ) { joined[key] = declared; } } return Object.keys(joined).length > 0 ? joined : undefined; }; /** * `schema` with `labels` combined into its own, as an outer declaration's. */ export const withIfcLabels = ( schema: MutableJSONSchema, labels: IfcLabels, ): MutableJSONSchema => { if (typeof schema === "boolean") { return schema === false ? { not: true, ifc: labels } : { ifc: labels }; } const existing = isObjectOrArray(schema.ifc) ? schema.ifc : {}; return { ...schema, ifc: combineIfcLabels(existing, labels) }; }; /** * `position` and the definitions its chain of local references reaches in * `definitions`, nearest first. A chain that comes back to a definition * already on it ends there: walking it again would add nothing. */ export const referenceChain = ( position: MutableJSONSchemaObj, definitions: Readonly>, ): MutableJSONSchemaObj[] => { const chain = [position]; const reached = new Set(); for (let at = position;;) { const ref = at.$ref; if (typeof ref !== "string" || !ref.startsWith(LOCAL_DEFINITION_PREFIX)) { return chain; } const name = ref.slice(LOCAL_DEFINITION_PREFIX.length); const definition = definitions[name]; if (reached.has(name) || !isObjectOrArray(definition)) return chain; reached.add(name); chain.push(definition); at = definition; } }; /** * The labels of every declaration along `position`'s reference chain in * `definitions`, combined, or `undefined` where none declares any. The * farthest definition's labels are the innermost declaration. */ export const declaredIfcLabels = ( position: MutableJSONSchema, definitions: Readonly>, ): Record | undefined => { if (!isObjectOrArray(position)) return undefined; let labels: Record | undefined; for (const declaring of referenceChain(position, definitions).reverse()) { if (isObjectOrArray(declaring.ifc)) { labels = combineIfcLabels(labels ?? {}, declaring.ifc); } } return labels; }; /** * Whether `schema` accepts only `null`, `undefined`, or both: written so, or a * bare local reference whose definition in `definitions`, through any further * bare references, is. */ const isNullishSchema = ( schema: MutableJSONSchema, definitions: Readonly>, ): boolean => { if (!isObjectOrArray(schema)) return false; const chain = referenceChain(schema, definitions); const last = chain[chain.length - 1]!; return chain.every((link) => Object.keys(link).length === 1) && (Array.isArray(last.type) ? last.type : [last.type]).every((type) => type === "null" || type === "undefined" ); }; /** * The value member of `position`, a union of one value with `null` or * `undefined`, where the member declares labels, as the labels in * `definitions` its references reach count, and the union declares none of * its own: a value that may be missing, whose labels formatting put on its * value member. `undefined` for any other position. */ export const labeledValueMember = ( position: MutableJSONSchema, definitions: Readonly>, ): MutableJSONSchemaObj | undefined => { if ( !isObjectOrArray(position) || !Array.isArray(position.anyOf) || isObjectOrArray(position.ifc) ) { return undefined; } const values = position.anyOf.filter((member) => !isNullishSchema(member, definitions) ); const [member] = values; return values.length === 1 && values.length < position.anyOf.length && isObjectOrArray(member) && declaredIfcLabels(member, definitions) !== undefined ? member : undefined; }; /** * The key a view's `ifc` holds, during one generation, where it read a writer * policy without its writer (`settlePoliciesReadInPart()`). The generation * removes it before it returns the schema. */ export const POLICY_READ_IN_PART = "__ctPolicyReadInPart"; /** * Whether `value`, a label atom or a part of one, names the current principal * (`CurrentPrincipal`, lowered as `{ __ctCurrentPrincipal: true }`). */ const namesCurrentPrincipal = (value: unknown): boolean => isObjectOrArray(value) && ((!Array.isArray(value) && value.__ctCurrentPrincipal === true) || Object.values(value).some(namesCurrentPrincipal)); /** * `labels` without the principal claims the runtime enforces only beside a * writer: an `ownerPrincipal`, and each `integrity` or `addIntegrity` atom * naming the current principal. A write against either, with no writer * declared beside it, is refused. `undefined` where nothing else is left. */ const withoutWriterBoundClaims = ( labels: Readonly>, ): Record | undefined => { const kept: Record = { ...labels }; delete kept.ownerPrincipal; for (const key of ["integrity", "addIntegrity"] as const) { const atoms = kept[key]; if (!Array.isArray(atoms)) continue; const rest = atoms.filter((atom) => !namesCurrentPrincipal(atom)); if (rest.length > 0) kept[key] = rest; else delete kept[key]; } return Object.keys(kept).length > 0 ? kept : undefined; }; /** * Settles each writer policy a view read in part (`POLICY_READ_IN_PART`), * once the whole schema is generated, so that the view holds the policy whole * or not at all. Where the writer arrived since, as a label the value's * declaration states merged in beside the part read, the policy is whole and * stays. Where none did, its principal claims are left out with the writer * (`withoutWriterBoundClaims()`), since either alone refuses every write * against it. * * It rewrites `ifc` in place, on positions the formatter built for this * generation, as `stateReferencedIfcLabels()` does. */ export const settlePoliciesReadInPart = (schema: MutableJSONSchema): void => { if (!isObjectOrArray(schema)) return; const visited = new Set(); const visit = (node: MutableJSONSchema): void => { if (!isObjectOrArray(node) || visited.has(node)) return; visited.add(node); if (isObjectOrArray(node.ifc) && POLICY_READ_IN_PART in node.ifc) { const { [POLICY_READ_IN_PART]: _, ...labels } = node.ifc as Record< string, unknown >; const kept = labels.writeAuthorizedBy !== undefined || labels.writePolicyAnyOf !== undefined ? labels : withoutWriterBoundClaims(labels); if (kept) node.ifc = kept as NonNullable; else delete node.ifc; } forEachSubschema(node, (child) => { visit(child as MutableJSONSchema); return false; }, { includeDefs: true, includeUnused: true }); }; visit(schema); }; /** * Writes beside each local `$ref` that carries `ifc` the labels of the * definitions it reaches, combined with its own, so that resolving the * reference keeps all of them. A definition keeps its own labels, which are * all that a reference with no label of its own resolves to. * * It rewrites `ifc` in place, on positions the formatter built for this * generation: a label beside a reference is only ever written there. */ export const stateReferencedIfcLabels = (schema: MutableJSONSchema): void => { if (!isObjectOrArray(schema)) return; const definitions = isObjectOrArray(schema.$defs) ? schema.$defs : {}; const visited = new Set(); const visit = (node: MutableJSONSchema): void => { if (!isObjectOrArray(node) || visited.has(node)) return; visited.add(node); if (typeof node.$ref === "string" && isObjectOrArray(node.ifc)) { node.ifc = declaredIfcLabels(node, definitions) as NonNullable< MutableJSONSchemaObj["ifc"] >; } // Every keyword the walk reads as holding schemas, `$defs` included (this // generator emits no `definitions`); `default`, `const` and `enum` hold // data, however it is shaped. forEachSubschema(node, (child) => { visit(child as MutableJSONSchema); return false; }, { includeDefs: true, includeUnused: true }); }; visit(schema); };