@reventlessdev/reventless-spec 3.0.0-alpha.134 → 3.0.0-alpha.136

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.
Files changed (36) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/package.json +4 -3
  3. package/run-check-lifecycle.mjs +2 -1
  4. package/run-prepare-accounts.mjs +3 -0
  5. package/src/components/CapabilityManifest.res +4 -0
  6. package/src/components/CapabilityManifest.res.mjs +13 -2
  7. package/src/components/DcbScopeInference.res +224 -73
  8. package/src/components/DcbScopeInference.res.mjs +187 -49
  9. package/src/components/DcbTag.res +142 -186
  10. package/src/components/DcbTag.res.mjs +137 -128
  11. package/src/components/DcbValidation.res +50 -0
  12. package/src/components/DcbValidation.res.mjs +51 -0
  13. package/src/generator/Codegen.res +2 -1
  14. package/src/generator/Codegen.res.mjs +1 -1
  15. package/src/generator/PlatformCodegen.res +1 -0
  16. package/src/generator/PlatformCodegen.res.mjs +5 -0
  17. package/src/lifecycle/CheckLifecycleModel.res +148 -8
  18. package/src/lifecycle/CheckLifecycleModel.res.mjs +154 -7
  19. package/src/semantic/Capabilities.res +17 -0
  20. package/src/semantic/Capabilities.res.mjs +9 -1
  21. package/src/semantic/CapabilityNeed.res +12 -0
  22. package/src/semantic/CapabilityNeed.res.mjs +9 -4
  23. package/src/semantic/IdentityProvider.res +127 -0
  24. package/src/semantic/IdentityProvider.res.mjs +65 -0
  25. package/src/semantic/Secrets.res +76 -0
  26. package/src/semantic/Secrets.res.mjs +49 -0
  27. package/src/types/AccountsManifest.res +287 -0
  28. package/src/types/AccountsManifest.res.mjs +312 -0
  29. package/src/types/AdminGroup.res +27 -0
  30. package/src/types/AdminGroup.res.mjs +9 -0
  31. package/src/types/OwnerScope.res +27 -0
  32. package/src/types/OwnerScope.res.mjs +12 -0
  33. package/src/types/PrepareAccounts.res +119 -0
  34. package/src/types/PrepareAccounts.res.mjs +148 -0
  35. package/src/util/Util_Password.res +60 -0
  36. package/src/util/Util_Password.res.mjs +53 -0
@@ -550,6 +550,18 @@ type derivedCommand = {
550
550
  accepted and silent. Kept separately because it is what turns a declared
551
551
  state the corpus disagrees with into a contradiction rather than a gap. */
552
552
  inertStates: array<string>,
553
+ /** The strict subset of [inertStates] where the command was *refused* — an
554
+ error, not an accepted no-op.
555
+
556
+ Separate because the two refute different claims. A named from-set says the
557
+ command **takes effect** from those states, so anything inert there
558
+ contradicts it. `Unrestricted` says only that the command is **never
559
+ refused**, which an accepted `Ok([])` agrees with rather than contradicts —
560
+ returning no events for a command that would change nothing is this
561
+ codebase's idempotency convention, not evidence the state was illegal.
562
+ Reading one field for both questions made every idempotent
563
+ legal-in-every-state command look like a contradiction. */
564
+ refusedStates: array<string>,
553
565
  /** Where the edges land. A relation, not a single value: a command observed
554
566
  landing in two states is the signal that the published `targetState` cannot
555
567
  carry the model, and that is worth seeing before spending the change. */
@@ -651,6 +663,9 @@ let deriveCommands = (
651
663
  inertStates: sortedUnique(
652
664
  mine->Array.filterMap(o => o.outcome == Emitted || o.from == noRow ? None : Some(o.from)),
653
665
  ),
666
+ refusedStates: sortedUnique(
667
+ mine->Array.filterMap(o => o.outcome == Refused && o.from != noRow ? Some(o.from) : None),
668
+ ),
654
669
  targets: sortedUnique(
655
670
  effective->Array.filterMap(o => o.to == o.from || o.to == noRow ? None : Some(o.to)),
656
671
  ),
@@ -666,6 +681,90 @@ let deriveCommands = (
666
681
  })
667
682
  }
668
683
 
684
+ // ── What each command yields ────────────────────────────────────────────────
685
+
686
+ /** Which command yields which event or error, as its scenarios show it — what a
687
+ consumer draws a command's outcome arrows from, read off the corpus rather
688
+ than guessed from names. It needs no lifecycle labelling, so every corpus a
689
+ plugin has is read, though only its command scenarios count.
690
+
691
+ An empty `then` is skipped rather than read as "accepted, nothing happened".
692
+ The PPX currently writes `then: []` for `thenNoEvent`, so an empty `then` is
693
+ indistinguishable from one the sidecar could not read, and an outcome shown
694
+ here has to be one a scenario actually states. */
695
+ type shownOutcome = {
696
+ kind: string, // "event" | "error" | "noEvent"
697
+ /** The whole `then`, in order: `thenEvents([A, B])` is one outcome of two
698
+ events — both, not either. Empty for `noEvent`. */
699
+ names: array<string>,
700
+ /** The title of every scenario showing this outcome, in corpus order. */
701
+ scenarios: array<string>,
702
+ }
703
+
704
+ type commandOutcomes = {
705
+ plugin: string,
706
+ component: string,
707
+ command: string,
708
+ /** One per distinct outcome, in order of first appearance. */
709
+ outcomes: array<shownOutcome>,
710
+ }
711
+
712
+ let shownOutcomeOf = (s: scenario): option<(string, array<string>)> =>
713
+ switch s.thenKind {
714
+ | "event" | "error" => Some((s.thenKind, s.thenElements->Array.map(e => e.name)))
715
+ | "noEvent" => Some(("noEvent", []))
716
+ // The empty `then` above, and a `then` that is not about this command's
717
+ // result — a side effect, a published command.
718
+ | _ => None
719
+ }
720
+
721
+ let byPluginComponentCommand = (xs: array<commandOutcomes>): array<commandOutcomes> =>
722
+ xs->Array.toSorted((a, b) =>
723
+ switch String.compare(a.plugin, b.plugin) {
724
+ | 0. =>
725
+ switch String.compare(a.component, b.component) {
726
+ | 0. => String.compare(a.command, b.command)
727
+ | c => c
728
+ }
729
+ | c => c
730
+ }
731
+ )
732
+
733
+ /** A command with no readable outcome is left out: an entry must say something. */
734
+ let commandOutcomes = (~plugin: string, ~corpora: array<corpus>): array<commandOutcomes> => {
735
+ let entries: array<commandOutcomes> = []
736
+ corpora->Array.forEach(c =>
737
+ c.scenarios->Array.forEach(s =>
738
+ switch (s.whenKind, s.whenElements->Array.get(0), shownOutcomeOf(s)) {
739
+ | ("command", Some(command), Some((kind, names))) =>
740
+ let entry = switch entries->Array.find(
741
+ e => e.component == c.component && e.command == command.name,
742
+ ) {
743
+ | Some(e) => e
744
+ | None =>
745
+ let e: commandOutcomes = {
746
+ plugin,
747
+ component: c.component,
748
+ command: command.name,
749
+ outcomes: [],
750
+ }
751
+ entries->Array.push(e)
752
+ e
753
+ }
754
+ switch entry.outcomes->Array.find(o => o.kind == kind && o.names == names) {
755
+ | Some(o) =>
756
+ if !(o.scenarios->Array.includes(s.title)) {
757
+ o.scenarios->Array.push(s.title)
758
+ }
759
+ | None => entry.outcomes->Array.push({kind, names, scenarios: [s.title]})
760
+ }
761
+ | _ => ()
762
+ }
763
+ )
764
+ )
765
+ entries->byPluginComponentCommand
766
+ }
767
+
669
768
  // ── The three verdicts ──────────────────────────────────────────────────────
670
769
 
671
770
  type finding = {
@@ -757,14 +856,24 @@ let compare = (
757
856
  // effect somewhere agrees with that rather than narrowing it — the corpus
758
857
  // covers the states somebody wrote a scenario for, and silence about the
759
858
  // rest is not refusal. What DOES refute the claim is a state the command was
760
- // exercised in and did nothing: that is the switch and the behaviour
859
+ // exercised in and **refused**: that is the switch and the behaviour
761
860
  // disagreeing about the same row.
861
+ //
862
+ // 🚨 **Refused, not merely inert.** `Unrestricted` claims the command is
863
+ // never refused — it does not claim the command always emits. A state where a
864
+ // scenario shows it accepted and silent AGREES with that: returning `Ok([])`
865
+ // for a command that would change nothing is this codebase's idempotency
866
+ // convention, required because commands arrive at least once. Reading
867
+ // `inertStates` here marked every such command contradicted, which is the
868
+ // opposite of what the corpus showed — a verdict redelivered, or landing
869
+ // after deactivation, is precisely the case the switch declared legal so that
870
+ // the reporting slice would not retry forever.
762
871
  | None if cmd.allowedStatesSource == Some("unrestricted") =>
763
- derived.inertStates->Array.forEach(state =>
872
+ derived.refusedStates->Array.forEach(state =>
764
873
  add(
765
874
  "contradicted",
766
875
  [state],
767
- `the switch declares it legal in every state, and a scenario from "${state}" ` ++ `shows it refused or producing nothing`,
876
+ `the switch declares it legal in every state, and a scenario from "${state}" ` ++ `shows it refused`,
768
877
  )
769
878
  )
770
879
  | None =>
@@ -928,6 +1037,7 @@ let runPlugin = async (
928
1037
  ~pluginDir: string,
929
1038
  ~findings: array<finding>,
930
1039
  ~opaque: array<opaque>,
1040
+ ~outcomes: array<commandOutcomes>,
931
1041
  ): result<array<derivedCommand>, string> =>
932
1042
  switch await readDeclared(~pluginDir) {
933
1043
  | Error(msg) => Error(msg)
@@ -937,6 +1047,8 @@ let runPlugin = async (
937
1047
  readCorpus,
938
1048
  )
939
1049
 
1050
+ outcomes->Array.pushMany(commandOutcomes(~plugin, ~corpora))
1051
+
940
1052
  // Every corpus, not only the ones the walk goes on to use: the kinds that
941
1053
  // are unreadable in full — extension points, automation and translation
942
1054
  // slices — are exactly the ones that sit outside the view/writable folders
@@ -1106,13 +1218,16 @@ let goldenPath = (~root: appRoot) => NodePath.join([root.dir, "schema", "lifecyc
1106
1218
  ever exercised, which is a sharper "missing scenario" than a verdict.
1107
1219
  - `opaque` — the corpora the walk cannot read, so a consumer can say "not
1108
1220
  analysed" where it would otherwise say "not covered".
1221
+ - `outcomes` — which command yields which event or error, and the scenarios
1222
+ that show it, sorted by plugin, component and command.
1109
1223
 
1110
- `str` is the schema's own version, bumped when a consumer would have to
1111
- change. */
1224
+ `version` is the schema's own version, bumped when a consumer would have to
1225
+ change. An added section is not such a change. */
1112
1226
  let reportJson = (
1113
1227
  ~findings: array<finding>,
1114
1228
  ~opaque: array<opaque>,
1115
1229
  ~derived: array<(string, derivedCommand)>,
1230
+ ~outcomes: array<commandOutcomes>,
1116
1231
  ~failures: array<string>,
1117
1232
  ): string => {
1118
1233
  let strs = xs => JSON.Encode.array(xs->Array.map(JSON.Encode.string))
@@ -1149,11 +1264,30 @@ let reportJson = (
1149
1264
  ("unreadable", JSON.Encode.int(o.unreadable)),
1150
1265
  ])
1151
1266
 
1267
+ let shownOutcomeJson = (o: shownOutcome) =>
1268
+ obj([
1269
+ ("kind", JSON.Encode.string(o.kind)),
1270
+ ("names", strs(o.names)),
1271
+ ("scenarios", strs(o.scenarios)),
1272
+ ])
1273
+
1274
+ let commandOutcomesJson = (e: commandOutcomes) =>
1275
+ obj([
1276
+ ("plugin", JSON.Encode.string(e.plugin)),
1277
+ ("component", JSON.Encode.string(e.component)),
1278
+ ("command", JSON.Encode.string(e.command)),
1279
+ ("outcomes", JSON.Encode.array(e.outcomes->Array.map(shownOutcomeJson))),
1280
+ ])
1281
+
1152
1282
  JSON.stringify(
1153
1283
  obj([
1154
1284
  ("version", JSON.Encode.int(1)),
1155
1285
  ("findings", JSON.Encode.array(findings->Array.map(findingJson))),
1156
1286
  ("commands", JSON.Encode.array(derived->Array.map(commandJson))),
1287
+ (
1288
+ "outcomes",
1289
+ JSON.Encode.array(outcomes->byPluginComponentCommand->Array.map(commandOutcomesJson)),
1290
+ ),
1157
1291
  ("opaque", JSON.Encode.array(opaque->Array.map(opaqueJson))),
1158
1292
  ("unreadable", strs(failures)),
1159
1293
  ]),
@@ -1245,6 +1379,7 @@ let writeOrCompare = (~path: string, ~actual: string, ~label: string, ~drifted:
1245
1379
  let main = async () => {
1246
1380
  let findings = []
1247
1381
  let opaque = []
1382
+ let outcomes = []
1248
1383
  let failures = []
1249
1384
  let drifted = []
1250
1385
  let allDerived = []
@@ -1302,7 +1437,7 @@ let main = async () => {
1302
1437
  | Some(pluginDir) =>
1303
1438
  let plugin = NodePath.basename(pluginDir)
1304
1439
  let qualified = `${example}/${plugin}`
1305
- switch await runPlugin(~plugin=qualified, ~pluginDir, ~findings, ~opaque) {
1440
+ switch await runPlugin(~plugin=qualified, ~pluginDir, ~findings, ~opaque, ~outcomes) {
1306
1441
  | Ok(commands) =>
1307
1442
  commands->Array.forEach(c => {
1308
1443
  derived->Array.push(c)
@@ -1345,7 +1480,7 @@ let main = async () => {
1345
1480
  let contradicted = of_("contradicted")
1346
1481
 
1347
1482
  if json {
1348
- Console.log(reportJson(~findings, ~opaque, ~derived=allDerived, ~failures))
1483
+ Console.log(reportJson(~findings, ~opaque, ~derived=allDerived, ~outcomes, ~failures))
1349
1484
  } else {
1350
1485
  ["contradicted", "unverified", "undeclared", "level", "ambiguous"]->Array.forEach(severity => {
1351
1486
  let group = of_(severity)
@@ -1378,4 +1513,9 @@ let main = async () => {
1378
1513
  }
1379
1514
  }
1380
1515
 
1381
- let _ = main()
1516
+ // 🚨 **No top-level call.** `../../run-check-lifecycle.mjs` invokes [main]; this
1517
+ // module only defines it. While the call was here, importing the module *ran the
1518
+ // whole check* and then called `NodeProcess.exit`, so nothing in this file could
1519
+ // be reached from a test — including [deriveCommands] and [compare], which are
1520
+ // pure and are where every verdict is actually decided. That is the same trap
1521
+ // `ProvisionIdentity` records having shipped a never-matching guard through.
@@ -469,6 +469,11 @@ function deriveCommands(component, observations, labelled) {
469
469
  return o.from;
470
470
  }
471
471
  })),
472
+ refusedStates: sortedUnique(Stdlib_Array.filterMap(mine, o => {
473
+ if (o.outcome === "Refused" && o.from !== noRow) {
474
+ return o.from;
475
+ }
476
+ })),
472
477
  targets: sortedUnique(Stdlib_Array.filterMap(effective, o => {
473
478
  if (o.to === o.from || o.to === noRow) {
474
479
  return;
@@ -484,6 +489,109 @@ function deriveCommands(component, observations, labelled) {
484
489
  });
485
490
  }
486
491
 
492
+ function shownOutcomeOf(s) {
493
+ let match = s.thenKind;
494
+ switch (match) {
495
+ case "error" :
496
+ case "event" :
497
+ break;
498
+ case "noEvent" :
499
+ return [
500
+ "noEvent",
501
+ []
502
+ ];
503
+ default:
504
+ return;
505
+ }
506
+ return [
507
+ s.thenKind,
508
+ s.thenElements.map(e => e.name)
509
+ ];
510
+ }
511
+
512
+ function byPluginComponentCommand(xs) {
513
+ return xs.toSorted((a, b) => {
514
+ let c = Primitive_string.compare(a.plugin, b.plugin);
515
+ if (c !== 0) {
516
+ return c;
517
+ }
518
+ let c$1 = Primitive_string.compare(a.component, b.component);
519
+ if (c$1 !== 0) {
520
+ return c$1;
521
+ } else {
522
+ return Primitive_string.compare(a.command, b.command);
523
+ }
524
+ });
525
+ }
526
+
527
+ function commandOutcomes(plugin, corpora) {
528
+ let entries = [];
529
+ corpora.forEach(c => {
530
+ c.scenarios.forEach(s => {
531
+ let match = s.whenKind;
532
+ let match$1 = s.whenElements[0];
533
+ let match$2 = shownOutcomeOf(s);
534
+ if (match !== "command") {
535
+ return;
536
+ }
537
+ if (match$1 === undefined) {
538
+ return;
539
+ }
540
+ if (match$2 === undefined) {
541
+ return;
542
+ }
543
+ let names = match$2[1];
544
+ let kind = match$2[0];
545
+ let e = entries.find(e => {
546
+ if (e.component === c.component) {
547
+ return e.command === match$1.name;
548
+ } else {
549
+ return false;
550
+ }
551
+ });
552
+ let entry;
553
+ if (e !== undefined) {
554
+ entry = e;
555
+ } else {
556
+ let e_component = c.component;
557
+ let e_command = match$1.name;
558
+ let e_outcomes = [];
559
+ let e$1 = {
560
+ plugin: plugin,
561
+ component: e_component,
562
+ command: e_command,
563
+ outcomes: e_outcomes
564
+ };
565
+ entries.push(e$1);
566
+ entry = e$1;
567
+ }
568
+ let o = entry.outcomes.find(o => {
569
+ if (o.kind === kind) {
570
+ return Primitive_object.equal(o.names, names);
571
+ } else {
572
+ return false;
573
+ }
574
+ });
575
+ if (o !== undefined) {
576
+ if (!o.scenarios.includes(s.title)) {
577
+ o.scenarios.push(s.title);
578
+ return;
579
+ } else {
580
+ return;
581
+ }
582
+ } else {
583
+ entry.outcomes.push({
584
+ kind: kind,
585
+ names: names,
586
+ scenarios: [s.title]
587
+ });
588
+ return;
589
+ }
590
+ });
591
+ });
592
+ return byPluginComponentCommand(entries);
593
+ }
594
+
487
595
  function allUnverified(cmd, add, why) {
488
596
  let states = cmd.allowedStates;
489
597
  if (states !== undefined && states.length !== 0) {
@@ -531,7 +639,7 @@ function compare(plugin, writable, derived, findings) {
531
639
  add("unverified", states, `the switch declares ` + states.length.toString() + ` state(s) and no scenario shows the command taking effect anywhere`);
532
640
  }
533
641
  } else if (Primitive_object.equal(declared.allowedStatesSource, "unrestricted")) {
534
- derived.inertStates.forEach(state => add("contradicted", [state], `the switch declares it legal in every state, and a scenario from "` + state + `" shows it refused or producing nothing`));
642
+ derived.refusedStates.forEach(state => add("contradicted", [state], `the switch declares it legal in every state, and a scenario from "` + state + `" shows it refused`));
535
643
  } else if (derived.allowedStates.length !== 0) {
536
644
  add("undeclared", derived.allowedStates, `scenarios show it taking effect from ` + derived.allowedStates.join(", ") + `, and it declares no edge`);
537
645
  }
@@ -654,7 +762,7 @@ function isWritablePath(path) {
654
762
  }
655
763
  }
656
764
 
657
- async function runPlugin(plugin, pluginDir, findings, opaque) {
765
+ async function runPlugin(plugin, pluginDir, findings, opaque, outcomes) {
658
766
  let msg = await readDeclared(pluginDir);
659
767
  if (msg.TAG !== "Ok") {
660
768
  return {
@@ -664,6 +772,7 @@ async function runPlugin(plugin, pluginDir, findings, opaque) {
664
772
  }
665
773
  let declared = msg._0;
666
774
  let corpora = Stdlib_Array.filterMap(filesUnder(Nodepath.join(pluginDir, "tests"), ".gwt.json"), readCorpus);
775
+ outcomes.push(...commandOutcomes(plugin, corpora));
667
776
  corpora.forEach(c => {
668
777
  let unreadable = c.scenarios.filter(s => s.whenElements.length === 0);
669
778
  if (unreadable.length !== 0) {
@@ -800,7 +909,7 @@ function goldenPath(root) {
800
909
  return Nodepath.join(root.dir, "schema", "lifecycle-model.json");
801
910
  }
802
911
 
803
- function reportJson(findings, opaque, derived, failures) {
912
+ function reportJson(findings, opaque, derived, outcomes, failures) {
804
913
  let findingJson = f => Object.fromEntries([
805
914
  [
806
915
  "verdict",
@@ -886,6 +995,38 @@ function reportJson(findings, opaque, derived, failures) {
886
995
  o.unreadable
887
996
  ]
888
997
  ]);
998
+ let shownOutcomeJson = o => Object.fromEntries([
999
+ [
1000
+ "kind",
1001
+ o.kind
1002
+ ],
1003
+ [
1004
+ "names",
1005
+ o.names.map(prim => prim)
1006
+ ],
1007
+ [
1008
+ "scenarios",
1009
+ o.scenarios.map(prim => prim)
1010
+ ]
1011
+ ]);
1012
+ let commandOutcomesJson = e => Object.fromEntries([
1013
+ [
1014
+ "plugin",
1015
+ e.plugin
1016
+ ],
1017
+ [
1018
+ "component",
1019
+ e.component
1020
+ ],
1021
+ [
1022
+ "command",
1023
+ e.command
1024
+ ],
1025
+ [
1026
+ "outcomes",
1027
+ e.outcomes.map(shownOutcomeJson)
1028
+ ]
1029
+ ]);
889
1030
  return JSON.stringify(Object.fromEntries([
890
1031
  [
891
1032
  "version",
@@ -899,6 +1040,10 @@ function reportJson(findings, opaque, derived, failures) {
899
1040
  "commands",
900
1041
  derived.map(commandJson)
901
1042
  ],
1043
+ [
1044
+ "outcomes",
1045
+ byPluginComponentCommand(outcomes).map(commandOutcomesJson)
1046
+ ],
902
1047
  [
903
1048
  "opaque",
904
1049
  opaque.map(opaqueJson)
@@ -990,6 +1135,7 @@ function writeOrCompare(path, actual, label, drifted) {
990
1135
  async function main() {
991
1136
  let findings = [];
992
1137
  let opaque = [];
1138
+ let outcomes = [];
993
1139
  let failures = [];
994
1140
  let drifted = [];
995
1141
  let allDerived = [];
@@ -1019,7 +1165,7 @@ async function main() {
1019
1165
  if (pluginDir !== undefined) {
1020
1166
  let plugin = Nodepath.basename(pluginDir);
1021
1167
  let qualified = example + `/` + plugin;
1022
- let commands = await runPlugin(qualified, pluginDir, findings, opaque);
1168
+ let commands = await runPlugin(qualified, pluginDir, findings, opaque, outcomes);
1023
1169
  if (commands.TAG === "Ok") {
1024
1170
  let commands$1 = commands._0;
1025
1171
  commands$1.forEach(c => {
@@ -1052,7 +1198,7 @@ async function main() {
1052
1198
  let of_ = severity => findings.filter(f => f.severity === severity);
1053
1199
  let contradicted = of_("contradicted");
1054
1200
  if (json) {
1055
- console.log(reportJson(findings, opaque, allDerived, failures));
1201
+ console.log(reportJson(findings, opaque, allDerived, outcomes, failures));
1056
1202
  } else {
1057
1203
  [
1058
1204
  "contradicted",
@@ -1086,8 +1232,6 @@ async function main() {
1086
1232
  }
1087
1233
  }
1088
1234
 
1089
- main();
1090
-
1091
1235
  export {
1092
1236
  repoRoot,
1093
1237
  examplesDir,
@@ -1128,6 +1272,9 @@ export {
1128
1272
  sameRow,
1129
1273
  observe,
1130
1274
  deriveCommands,
1275
+ shownOutcomeOf,
1276
+ byPluginComponentCommand,
1277
+ commandOutcomes,
1131
1278
  allUnverified,
1132
1279
  compare,
1133
1280
  pluginDirsIn,
@@ -27,6 +27,15 @@ type t = {
27
27
  /** Send a message to a person, and say which channels this deployment can
28
28
  attempt at all. See `Messaging.provider`. */
29
29
  messaging: Messaging.provider,
30
+ /** Make, group and unmake principals, and say which of those this
31
+ deployment's provider can do at all. Administrative only — authenticating
32
+ one is `Auth_Adapter.Provider`'s. See `IdentityProvider.t`. */
33
+ identityProvider: IdentityProvider.t,
34
+ /** Unguessable material and a one-way function over it. Injected rather than
35
+ imported so a test can pin the secret a flow is about to use — a suite
36
+ cannot assert what a flow does with a value it cannot predict. See
37
+ `Secrets.t`. */
38
+ secrets: Secrets.t,
30
39
  }
31
40
 
32
41
  /**
@@ -54,4 +63,12 @@ let none: t = {
54
63
  ~recipient as _,
55
64
  ~message as _,
56
65
  ) => Error(Unavailable("no messaging provider is configured for this platform"))),
66
+ identityProvider: IdentityProvider.unavailable(
67
+ ~reason="no identity provider is configured for this platform",
68
+ ),
69
+ // Refusing rather than falling back to a weak source. Every runtime has a
70
+ // cryptographic one, so a platform reaching this arm has skipped wiring rather
71
+ // than declined to provision — and a guessable token would be accepted
72
+ // everywhere while looking exactly like a real one.
73
+ secrets: Secrets.unavailable(~reason="no secret source is configured for this platform"),
57
74
  }
@@ -1,6 +1,8 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
+ import * as Secrets$Reventless from "./Secrets.res.mjs";
3
4
  import * as Messaging$Reventless from "./Messaging.res.mjs";
5
+ import * as IdentityProvider$Reventless from "./IdentityProvider.res.mjs";
4
6
 
5
7
  async function none_geocode(param) {
6
8
  return {
@@ -20,9 +22,15 @@ let none_messaging = Messaging$Reventless.makeProvider([], [], async (param, par
20
22
  }
21
23
  }));
22
24
 
25
+ let none_identityProvider = IdentityProvider$Reventless.unavailable("no identity provider is configured for this platform");
26
+
27
+ let none_secrets = Secrets$Reventless.unavailable("no secret source is configured for this platform");
28
+
23
29
  let none = {
24
30
  geocode: none_geocode,
25
- messaging: none_messaging
31
+ messaging: none_messaging,
32
+ identityProvider: none_identityProvider,
33
+ secrets: none_secrets
26
34
  };
27
35
 
28
36
  export {
@@ -24,6 +24,16 @@ type t =
24
24
  declaration, because a plugin that named `Sms` would fail a deploy it
25
25
  could have run on email. */
26
26
  | Messaging
27
+ /** Making, grouping and unmaking principals, reached through
28
+ `Capabilities.identityProvider`. No payload, for the reason `Geocoding`
29
+ has none: one identity provider per deployment is a real answer, and a
30
+ deployment wanting a second pool is the `{alternatives, min}` question
31
+ `Messaging` already answers by publishing what it provisioned.
32
+
33
+ Not authentication. Token issuance and session handling are
34
+ `Auth_Adapter.Provider`'s, read at a different time — two seams over one
35
+ provider, the way this type and `Capabilities.t` are already two. */
36
+ | IdentityProvider
27
37
 
28
38
  /** The spelling carried in `pluginStructure` and in `capabilities.json`.
29
39
  Persisted structures hold strings, not enum members, so a plugin built
@@ -32,6 +42,7 @@ let toString = (need: t): string =>
32
42
  switch need {
33
43
  | Geocoding => "Geocoding"
34
44
  | Messaging => "Messaging"
45
+ | IdentityProvider => "IdentityProvider"
35
46
  }
36
47
 
37
48
  /** The inverse, for readers of a persisted structure or a committed manifest.
@@ -41,6 +52,7 @@ let fromString = (name: string): option<t> =>
41
52
  switch name {
42
53
  | "Geocoding" => Some(Geocoding)
43
54
  | "Messaging" => Some(Messaging)
55
+ | "IdentityProvider" => Some(IdentityProvider)
44
56
  | _ => None
45
57
  }
46
58
 
@@ -3,10 +3,13 @@
3
3
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
4
4
 
5
5
  function toString(need) {
6
- if (need === "Geocoding") {
7
- return "Geocoding";
8
- } else {
9
- return "Messaging";
6
+ switch (need) {
7
+ case "Geocoding" :
8
+ return "Geocoding";
9
+ case "Messaging" :
10
+ return "Messaging";
11
+ case "IdentityProvider" :
12
+ return "IdentityProvider";
10
13
  }
11
14
  }
12
15
 
@@ -14,6 +17,8 @@ function fromString(name) {
14
17
  switch (name) {
15
18
  case "Geocoding" :
16
19
  return "Geocoding";
20
+ case "IdentityProvider" :
21
+ return "IdentityProvider";
17
22
  case "Messaging" :
18
23
  return "Messaging";
19
24
  default: