@adminiumjs/manifest 0.3.38 → 0.3.40

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.
@@ -31,12 +31,29 @@ import { publicConsentSchema } from './people.js';
31
31
  import { kindOf, rulesOf } from './capacity.js';
32
32
  import { formulaColumns } from './formula.js';
33
33
  import { refSchema, scalarSchema, settingRefSchema, valueFits, } from './refs.js';
34
+ import { FORMS_BECAME_TABLE_MAX } from './forms.js';
34
35
  import { momentIssues, momentOffsetSchema, plainMomentSchema, wallTimeSchema } from './refs.js';
35
36
  import { conditionIssues, linkedConditionSchema, reachedOnlyByUndo, stateConditionSchema } from './states.js';
36
37
  /** The fewest characters (its prefix apart) of a code that opens its own row (`unlockBy.self`). */
37
38
  export const SELF_UNLOCK_MIN_LENGTH = 10;
38
39
  /** The key every entry uses unless it names another. */
39
40
  export const CUSTOMER_KEY = 'customer';
41
+ /**
42
+ * The reserved key name of the entries that open a table to FORMS: an answer
43
+ * to a form may become a new row of the table, through this entry's
44
+ * `writable` columns, `defaults` and `anonymous` limits. No browser key of
45
+ * this name is ever made, and the public API never serves such an entry: it
46
+ * is read by the forms door alone, so opening a table to forms does not open
47
+ * it to the public API.
48
+ */
49
+ export const FORMS_KEY = 'forms';
50
+ /**
51
+ * What a forms entry may say. A form only ever creates a row (it never reads
52
+ * one back, changes one or signs anyone in), so every word about reading,
53
+ * claims, sessions, children, files and the entry's own confirmation mail is
54
+ * left out: forms send their own.
55
+ */
56
+ export const FORMS_ENTRY_WORDS = ['table', 'methods', 'key', 'writable', 'writableValues', 'defaults', 'requires', 'agrees', 'anonymous', 'requireSetting', 'module'];
40
57
  /** The longest a plain-text value may be unless its column says longer: a name. */
41
58
  export const PLAIN_TEXT_MAX = 80;
42
59
  /** The most digits a plain-text column may take in all: "table 12", "2 without onions", never a phone number. */
@@ -699,6 +716,9 @@ export const publicKeysSchema = z
699
716
  .record(keyNameSchema, publicKeySchema)
700
717
  .refine((keys) => !(CUSTOMER_KEY in keys) || Object.keys(keys[CUSTOMER_KEY] ?? {}).every((field) => field === 'peak'), {
701
718
  message: '"customer" is the app\'s own key: only its peak is said here',
719
+ })
720
+ .refine((keys) => !(FORMS_KEY in keys), {
721
+ message: '"forms" is the name of the entries forms write through, and no browser key: name the key otherwise',
702
722
  });
703
723
  /**
704
724
  * The own links of the rows a person of `identityTable` holds: each table a
@@ -765,6 +785,53 @@ export function resendAddressOf(outbox, kind, table, columnsOf) {
765
785
  return null;
766
786
  return { columns, ...(person === undefined ? {} : { person }) };
767
787
  }
788
+ /** Whether anyone at all may read rows through an entry: no claim, no session, no person, no parent row it is read with. */
789
+ function readByAnyone(entry) {
790
+ return (entry.methods.includes('GET') &&
791
+ entry.kind !== 'availability' &&
792
+ entry.claim === undefined &&
793
+ entry.claimedBy === undefined &&
794
+ entry.identity === undefined &&
795
+ entry.visibleWith === undefined &&
796
+ entry.level === undefined);
797
+ }
798
+ /**
799
+ * A forms entry's own rules: it creates (POST) and nothing else, under the
800
+ * anonymous limits every stranger's create has, with the columns a form may
801
+ * fill named; one per table; and no entry on any key lets anyone read the
802
+ * rows strangers add through it.
803
+ */
804
+ function formsEntryIssues(entry, i, entries, appKey) {
805
+ const out = [];
806
+ const at = (...rest) => ['publicAccess', i, ...rest];
807
+ // Each answer keeps the row it became by its table's stored name: it must fit an answers table, whole.
808
+ const stored = appKey === undefined ? 0 : appKey.length + 1 + entry.table.length;
809
+ if (stored > FORMS_BECAME_TABLE_MAX) {
810
+ out.push({ path: at('table'), message: `a form's answer names the row it became by "<app>:<table>", at most ${String(FORMS_BECAME_TABLE_MAX)} characters: "${appKey ?? ''}:${entry.table}" is ${String(stored)}` });
811
+ }
812
+ const allowed = FORMS_ENTRY_WORDS;
813
+ for (const word of Object.keys(entry)) {
814
+ if (!allowed.includes(word))
815
+ out.push({ path: at(word), message: `a forms entry only lets a form add a row: "${word}" is not said on it` });
816
+ }
817
+ if (entry.methods.length !== 1 || entry.methods[0] !== 'POST')
818
+ out.push({ path: at('methods'), message: 'a forms entry adds a row and does nothing else: its methods are ["POST"]' });
819
+ if (entry.anonymous === undefined)
820
+ out.push({ path: at('anonymous'), message: 'a form is sent by strangers: a forms entry says its anonymous limits' });
821
+ if ((entry.writable ?? []).length === 0)
822
+ out.push({ path: at('writable'), message: 'a forms entry names the columns a form may fill (writable)' });
823
+ if (entries.findIndex((other) => other.key === FORMS_KEY && other.table === entry.table) !== i) {
824
+ out.push({ path: at('table'), message: `"${entry.table}" is opened to forms by another entry already: one forms entry per table` });
825
+ }
826
+ const reader = entries.findIndex((other) => other !== entry && other.table === entry.table && readByAnyone(other));
827
+ if (reader !== -1) {
828
+ out.push({
829
+ path: at('table'),
830
+ message: `strangers add rows to "${entry.table}" through forms, and entry ${String(reader)} lets anyone read its rows: everyone could read what people typed into a form`,
831
+ });
832
+ }
833
+ return out;
834
+ }
768
835
  /** Everything in `publicAccess` and `publicKeys` that names something undeclared, or breaks a rule. */
769
836
  export function publicAccessIssues(entries, ctx) {
770
837
  const out = [];
@@ -894,9 +961,11 @@ export function publicAccessIssues(entries, ctx) {
894
961
  }
895
962
  const column = (ref) => index.column(entry.table, ref);
896
963
  const key = entry.key ?? CUSTOMER_KEY;
897
- if (key !== CUSTOMER_KEY && ctx.publicKeys?.[key] === undefined) {
964
+ if (key !== CUSTOMER_KEY && key !== FORMS_KEY && ctx.publicKeys?.[key] === undefined) {
898
965
  out.push({ path: at('key'), message: `"${key}" is not one of the app's publicKeys` });
899
966
  }
967
+ if (key === FORMS_KEY)
968
+ out.push(...formsEntryIssues(entry, i, entries, ctx.appKey));
900
969
  for (const list of ['select', 'writable']) {
901
970
  for (const ref of entry[list] ?? []) {
902
971
  if (!has(entry.table, ref))
@@ -906,8 +975,8 @@ export function publicAccessIssues(entries, ctx) {
906
975
  // An entry anyone may call (no claim, no parent, no session asked) shows no personal data: the install refuses it too.
907
976
  const sessionOnly = entry.level !== undefined && entry.claim === undefined && entry.claimedBy === undefined && entry.visibleWith === undefined;
908
977
  const anyone = entry.claim === undefined && entry.visibleWith === undefined && (entry.claimedBy === undefined || entry.claimedBy.optional === true) && !sessionOnly;
909
- // An availability entry shows no row, whatever it names.
910
- if (anyone && entry.kind !== 'availability') {
978
+ // An availability entry shows no row, whatever it names; nor does a forms entry, which answers nobody with the row.
979
+ if (anyone && entry.kind !== 'availability' && key !== FORMS_KEY) {
911
980
  // No select shows every column but a code Adminium makes and a secret, as the install reads it.
912
981
  const shown = entry.select ??
913
982
  table.columns.filter((c) => c.rules?.['code'] === undefined && c.rules?.['secret'] !== true).map((c) => c.ref);