@wtfalch/authz-store 0.2.1 → 0.4.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.
Files changed (81) hide show
  1. package/README.md +393 -5
  2. package/dist/activations.d.ts +60 -0
  3. package/dist/activations.js +516 -0
  4. package/dist/alerts.d.ts +86 -0
  5. package/dist/alerts.js +132 -0
  6. package/dist/assignments.d.ts +71 -0
  7. package/dist/assignments.js +103 -0
  8. package/dist/audit.d.ts +135 -0
  9. package/dist/audit.js +231 -0
  10. package/dist/binding.d.ts +26 -0
  11. package/dist/binding.js +8 -0
  12. package/dist/boot.d.ts +54 -0
  13. package/dist/boot.js +143 -0
  14. package/dist/bootstrap.d.ts +28 -0
  15. package/dist/bootstrap.js +83 -0
  16. package/dist/break-glass.d.ts +61 -0
  17. package/dist/break-glass.js +247 -0
  18. package/dist/credentials.d.ts +94 -0
  19. package/dist/credentials.js +230 -0
  20. package/dist/denial.d.ts +9 -0
  21. package/dist/denial.js +49 -0
  22. package/dist/erase.d.ts +58 -0
  23. package/dist/erase.js +108 -0
  24. package/dist/events.d.ts +77 -0
  25. package/dist/events.js +107 -0
  26. package/dist/export.d.ts +161 -0
  27. package/dist/export.js +293 -0
  28. package/dist/grants.d.ts +2 -0
  29. package/dist/index.d.ts +34 -1
  30. package/dist/index.js +34 -1
  31. package/dist/install-owner.d.ts +25 -0
  32. package/dist/install-owner.js +159 -0
  33. package/dist/invitations.d.ts +160 -0
  34. package/dist/invitations.js +685 -0
  35. package/dist/membership-rows.d.ts +206 -0
  36. package/dist/membership-rows.js +271 -0
  37. package/dist/memberships.d.ts +87 -0
  38. package/dist/memberships.js +272 -0
  39. package/dist/nesting.d.ts +124 -0
  40. package/dist/nesting.js +515 -0
  41. package/dist/person-records.d.ts +186 -0
  42. package/dist/person-records.js +263 -0
  43. package/dist/platform.d.ts +20 -0
  44. package/dist/platform.js +65 -0
  45. package/dist/policy-access.d.ts +260 -0
  46. package/dist/policy-access.js +357 -0
  47. package/dist/policy-entry.d.ts +11 -0
  48. package/dist/policy-entry.js +10 -0
  49. package/dist/policy-resources.d.ts +5 -0
  50. package/dist/policy-resources.js +46 -0
  51. package/dist/policy-schema.d.ts +449 -0
  52. package/dist/policy-schema.js +63 -0
  53. package/dist/policy.d.ts +71 -0
  54. package/dist/policy.js +78 -0
  55. package/dist/propagate.d.ts +43 -0
  56. package/dist/propagate.js +47 -0
  57. package/dist/reconcile.d.ts +78 -0
  58. package/dist/reconcile.js +94 -0
  59. package/dist/resource-access.d.ts +344 -0
  60. package/dist/resource-access.js +656 -0
  61. package/dist/role-keys.d.ts +9 -0
  62. package/dist/role-keys.js +9 -0
  63. package/dist/roles.d.ts +36 -0
  64. package/dist/roles.js +191 -0
  65. package/dist/schema.d.ts +18 -1
  66. package/dist/schema.js +8 -1
  67. package/dist/startup.d.ts +57 -0
  68. package/dist/startup.js +113 -0
  69. package/dist/tenants.d.ts +213 -0
  70. package/dist/tenants.js +808 -0
  71. package/dist/tree-writes.d.ts +65 -0
  72. package/dist/tree-writes.js +201 -0
  73. package/dist/tree.d.ts +272 -0
  74. package/dist/tree.js +565 -0
  75. package/dist/types.d.ts +87 -0
  76. package/dist/types.js +15 -0
  77. package/migrations/0003_product_tenant_kind.sql +14 -0
  78. package/migrations/0004_credential_keys_issued_id.sql +33 -0
  79. package/migrations/0005_activations.sql +71 -0
  80. package/migrations/0006_erase_person.sql +143 -0
  81. package/package.json +13 -4
@@ -0,0 +1,46 @@
1
+ import { and, eq } from 'drizzle-orm';
2
+ import { accessTeams } from './resource-schema.js';
3
+ import { authzEvents } from './schema.js';
4
+ import { isUuid } from './types.js';
5
+ /** Authoritative resource metadata, shared by delegation and credential lineage evaluation. */
6
+ export async function policyResource(tx, binding, organisationId, type, id) {
7
+ const base = {
8
+ applicationId: binding.applicationId,
9
+ platformId: binding.platformId,
10
+ organisationId,
11
+ type,
12
+ id,
13
+ };
14
+ if (type === 'team' && isUuid(id)) {
15
+ const [row] = await tx
16
+ .select({ id: accessTeams.id })
17
+ .from(accessTeams)
18
+ .where(and(eq(accessTeams.tenantId, organisationId), eq(accessTeams.id, id)))
19
+ .limit(1);
20
+ return row ? { ...base, teamId: id } : undefined;
21
+ }
22
+ if (type === 'audit' && /^\d{1,15}$/.test(id)) {
23
+ const [row] = await tx
24
+ .select({
25
+ teamId: authzEvents.teamId,
26
+ actorId: authzEvents.actorId,
27
+ actorClass: authzEvents.actorClass,
28
+ subjectId: authzEvents.subjectId,
29
+ subjectClass: authzEvents.subjectClass,
30
+ })
31
+ .from(authzEvents)
32
+ .where(and(eq(authzEvents.tenantId, organisationId), eq(authzEvents.tenantVisible, true), eq(authzEvents.id, Number(id))))
33
+ .limit(1);
34
+ if (!row)
35
+ return undefined;
36
+ return {
37
+ ...base,
38
+ teamId: row.teamId,
39
+ actor: { id: row.actorId, class: row.actorClass },
40
+ ...(row.subjectId && row.subjectClass
41
+ ? { subject: { id: row.subjectId, class: row.subjectClass } }
42
+ : {}),
43
+ };
44
+ }
45
+ return undefined;
46
+ }
@@ -166,6 +166,7 @@ export declare const policyRoles: import("drizzle-orm/pg-core").PgTableWithColum
166
166
  boundary: {
167
167
  kind: "organisation";
168
168
  organisationId: string;
169
+ descendants?: boolean | undefined;
169
170
  } | {
170
171
  kind: "organisations";
171
172
  organisationIds: string[];
@@ -204,6 +205,7 @@ export declare const policyRoles: import("drizzle-orm/pg-core").PgTableWithColum
204
205
  boundary: {
205
206
  kind: "organisation";
206
207
  organisationId: string;
208
+ descendants?: boolean | undefined;
207
209
  } | {
208
210
  kind: "organisations";
209
211
  organisationIds: string[];
@@ -507,6 +509,7 @@ export declare const policyAssignments: import("drizzle-orm/pg-core").PgTableWit
507
509
  data: {
508
510
  kind: "organisation";
509
511
  organisationId: string;
512
+ descendants?: boolean | undefined;
510
513
  } | {
511
514
  kind: "organisations";
512
515
  organisationIds: string[];
@@ -527,6 +530,7 @@ export declare const policyAssignments: import("drizzle-orm/pg-core").PgTableWit
527
530
  $type: {
528
531
  kind: "organisation";
529
532
  organisationId: string;
533
+ descendants?: boolean | undefined;
530
534
  } | {
531
535
  kind: "organisations";
532
536
  organisationIds: string[];
@@ -684,6 +688,451 @@ export declare const policyAssignments: import("drizzle-orm/pg-core").PgTableWit
684
688
  };
685
689
  dialect: "pg";
686
690
  }>;
691
+ /**
692
+ * A human principal's time-boxed request to exercise a subset of the
693
+ * ELEVATED permissions they are otherwise only eligible for (ADR 0016, "No
694
+ * standing authority"). Ported from Boule's `drizzle/0014_activations.sql`
695
+ * (authz#83 wave 11b, `migrations/0005_activations.sql` here);
696
+ * `authz_roles.updated_by` above and the `'activation'`
697
+ * `authz_assignments.source` value that same source file added are already
698
+ * in `0001_baseline.sql`, so only these two tables land in the new file.
699
+ */
700
+ export declare const policyActivations: import("drizzle-orm/pg-core").PgTableWithColumns<{
701
+ name: "authz_activations";
702
+ schema: undefined;
703
+ columns: {
704
+ id: import("drizzle-orm/pg-core").PgColumn<{
705
+ name: "id";
706
+ tableName: "authz_activations";
707
+ dataType: "string";
708
+ columnType: "PgUUID";
709
+ data: string;
710
+ driverParam: string;
711
+ notNull: true;
712
+ hasDefault: true;
713
+ isPrimaryKey: true;
714
+ isAutoincrement: false;
715
+ hasRuntimeDefault: false;
716
+ enumValues: undefined;
717
+ baseColumn: never;
718
+ identity: undefined;
719
+ generated: undefined;
720
+ }, {}, {}>;
721
+ tenantId: import("drizzle-orm/pg-core").PgColumn<{
722
+ name: "tenant_id";
723
+ tableName: "authz_activations";
724
+ dataType: "string";
725
+ columnType: "PgUUID";
726
+ data: string;
727
+ driverParam: string;
728
+ notNull: true;
729
+ hasDefault: false;
730
+ isPrimaryKey: false;
731
+ isAutoincrement: false;
732
+ hasRuntimeDefault: false;
733
+ enumValues: undefined;
734
+ baseColumn: never;
735
+ identity: undefined;
736
+ generated: undefined;
737
+ }, {}, {}>;
738
+ applicationId: import("drizzle-orm/pg-core").PgColumn<{
739
+ name: "application_id";
740
+ tableName: "authz_activations";
741
+ dataType: "string";
742
+ columnType: "PgText";
743
+ data: string;
744
+ driverParam: string;
745
+ notNull: true;
746
+ hasDefault: false;
747
+ isPrimaryKey: false;
748
+ isAutoincrement: false;
749
+ hasRuntimeDefault: false;
750
+ enumValues: [string, ...string[]];
751
+ baseColumn: never;
752
+ identity: undefined;
753
+ generated: undefined;
754
+ }, {}, {}>;
755
+ platformId: import("drizzle-orm/pg-core").PgColumn<{
756
+ name: "platform_id";
757
+ tableName: "authz_activations";
758
+ dataType: "string";
759
+ columnType: "PgText";
760
+ data: string;
761
+ driverParam: string;
762
+ notNull: true;
763
+ hasDefault: false;
764
+ isPrimaryKey: false;
765
+ isAutoincrement: false;
766
+ hasRuntimeDefault: false;
767
+ enumValues: [string, ...string[]];
768
+ baseColumn: never;
769
+ identity: undefined;
770
+ generated: undefined;
771
+ }, {}, {}>;
772
+ principalId: import("drizzle-orm/pg-core").PgColumn<{
773
+ name: "principal_id";
774
+ tableName: "authz_activations";
775
+ dataType: "string";
776
+ columnType: "PgText";
777
+ data: string;
778
+ driverParam: string;
779
+ notNull: true;
780
+ hasDefault: false;
781
+ isPrimaryKey: false;
782
+ isAutoincrement: false;
783
+ hasRuntimeDefault: false;
784
+ enumValues: [string, ...string[]];
785
+ baseColumn: never;
786
+ identity: undefined;
787
+ generated: undefined;
788
+ }, {}, {}>;
789
+ principalClass: import("drizzle-orm/pg-core").PgColumn<{
790
+ name: "principal_class";
791
+ tableName: "authz_activations";
792
+ dataType: "string";
793
+ columnType: "PgText";
794
+ data: string;
795
+ driverParam: string;
796
+ notNull: true;
797
+ hasDefault: false;
798
+ isPrimaryKey: false;
799
+ isAutoincrement: false;
800
+ hasRuntimeDefault: false;
801
+ enumValues: [string, ...string[]];
802
+ baseColumn: never;
803
+ identity: undefined;
804
+ generated: undefined;
805
+ }, {}, {}>;
806
+ permissions: import("drizzle-orm/pg-core").PgColumn<{
807
+ name: "permissions";
808
+ tableName: "authz_activations";
809
+ dataType: "array";
810
+ columnType: "PgArray";
811
+ data: string[];
812
+ driverParam: string | string[];
813
+ notNull: true;
814
+ hasDefault: false;
815
+ isPrimaryKey: false;
816
+ isAutoincrement: false;
817
+ hasRuntimeDefault: false;
818
+ enumValues: [string, ...string[]];
819
+ baseColumn: import("drizzle-orm").Column<{
820
+ name: "permissions";
821
+ tableName: "authz_activations";
822
+ dataType: "string";
823
+ columnType: "PgText";
824
+ data: string;
825
+ driverParam: string;
826
+ notNull: false;
827
+ hasDefault: false;
828
+ isPrimaryKey: false;
829
+ isAutoincrement: false;
830
+ hasRuntimeDefault: false;
831
+ enumValues: [string, ...string[]];
832
+ baseColumn: never;
833
+ identity: undefined;
834
+ generated: undefined;
835
+ }, {}, {}>;
836
+ identity: undefined;
837
+ generated: undefined;
838
+ }, {}, {
839
+ baseBuilder: import("drizzle-orm/pg-core").PgColumnBuilder<{
840
+ name: "permissions";
841
+ dataType: "string";
842
+ columnType: "PgText";
843
+ data: string;
844
+ enumValues: [string, ...string[]];
845
+ driverParam: string;
846
+ }, {}, {}, import("drizzle-orm").ColumnBuilderExtraConfig>;
847
+ size: undefined;
848
+ }>;
849
+ reason: import("drizzle-orm/pg-core").PgColumn<{
850
+ name: "reason";
851
+ tableName: "authz_activations";
852
+ dataType: "string";
853
+ columnType: "PgText";
854
+ data: string;
855
+ driverParam: string;
856
+ notNull: true;
857
+ hasDefault: false;
858
+ isPrimaryKey: false;
859
+ isAutoincrement: false;
860
+ hasRuntimeDefault: false;
861
+ enumValues: [string, ...string[]];
862
+ baseColumn: never;
863
+ identity: undefined;
864
+ generated: undefined;
865
+ }, {}, {}>;
866
+ reference: import("drizzle-orm/pg-core").PgColumn<{
867
+ name: "reference";
868
+ tableName: "authz_activations";
869
+ dataType: "string";
870
+ columnType: "PgText";
871
+ data: string;
872
+ driverParam: string;
873
+ notNull: true;
874
+ hasDefault: false;
875
+ isPrimaryKey: false;
876
+ isAutoincrement: false;
877
+ hasRuntimeDefault: false;
878
+ enumValues: [string, ...string[]];
879
+ baseColumn: never;
880
+ identity: undefined;
881
+ generated: undefined;
882
+ }, {}, {}>;
883
+ status: import("drizzle-orm/pg-core").PgColumn<{
884
+ name: "status";
885
+ tableName: "authz_activations";
886
+ dataType: "string";
887
+ columnType: "PgText";
888
+ data: string;
889
+ driverParam: string;
890
+ notNull: true;
891
+ hasDefault: true;
892
+ isPrimaryKey: false;
893
+ isAutoincrement: false;
894
+ hasRuntimeDefault: false;
895
+ enumValues: [string, ...string[]];
896
+ baseColumn: never;
897
+ identity: undefined;
898
+ generated: undefined;
899
+ }, {}, {}>;
900
+ requestedAt: import("drizzle-orm/pg-core").PgColumn<{
901
+ name: "requested_at";
902
+ tableName: "authz_activations";
903
+ dataType: "date";
904
+ columnType: "PgTimestamp";
905
+ data: Date;
906
+ driverParam: string;
907
+ notNull: true;
908
+ hasDefault: true;
909
+ isPrimaryKey: false;
910
+ isAutoincrement: false;
911
+ hasRuntimeDefault: false;
912
+ enumValues: undefined;
913
+ baseColumn: never;
914
+ identity: undefined;
915
+ generated: undefined;
916
+ }, {}, {}>;
917
+ startsAt: import("drizzle-orm/pg-core").PgColumn<{
918
+ name: "starts_at";
919
+ tableName: "authz_activations";
920
+ dataType: "date";
921
+ columnType: "PgTimestamp";
922
+ data: Date;
923
+ driverParam: string;
924
+ notNull: true;
925
+ hasDefault: false;
926
+ isPrimaryKey: false;
927
+ isAutoincrement: false;
928
+ hasRuntimeDefault: false;
929
+ enumValues: undefined;
930
+ baseColumn: never;
931
+ identity: undefined;
932
+ generated: undefined;
933
+ }, {}, {}>;
934
+ expiresAt: import("drizzle-orm/pg-core").PgColumn<{
935
+ name: "expires_at";
936
+ tableName: "authz_activations";
937
+ dataType: "date";
938
+ columnType: "PgTimestamp";
939
+ data: Date;
940
+ driverParam: string;
941
+ notNull: true;
942
+ hasDefault: false;
943
+ isPrimaryKey: false;
944
+ isAutoincrement: false;
945
+ hasRuntimeDefault: false;
946
+ enumValues: undefined;
947
+ baseColumn: never;
948
+ identity: undefined;
949
+ generated: undefined;
950
+ }, {}, {}>;
951
+ roleId: import("drizzle-orm/pg-core").PgColumn<{
952
+ name: "role_id";
953
+ tableName: "authz_activations";
954
+ dataType: "string";
955
+ columnType: "PgUUID";
956
+ data: string;
957
+ driverParam: string;
958
+ notNull: true;
959
+ hasDefault: false;
960
+ isPrimaryKey: false;
961
+ isAutoincrement: false;
962
+ hasRuntimeDefault: false;
963
+ enumValues: undefined;
964
+ baseColumn: never;
965
+ identity: undefined;
966
+ generated: undefined;
967
+ }, {}, {}>;
968
+ endedAt: import("drizzle-orm/pg-core").PgColumn<{
969
+ name: "ended_at";
970
+ tableName: "authz_activations";
971
+ dataType: "date";
972
+ columnType: "PgTimestamp";
973
+ data: Date;
974
+ driverParam: string;
975
+ notNull: false;
976
+ hasDefault: false;
977
+ isPrimaryKey: false;
978
+ isAutoincrement: false;
979
+ hasRuntimeDefault: false;
980
+ enumValues: undefined;
981
+ baseColumn: never;
982
+ identity: undefined;
983
+ generated: undefined;
984
+ }, {}, {}>;
985
+ endedBy: import("drizzle-orm/pg-core").PgColumn<{
986
+ name: "ended_by";
987
+ tableName: "authz_activations";
988
+ dataType: "string";
989
+ columnType: "PgText";
990
+ data: string;
991
+ driverParam: string;
992
+ notNull: false;
993
+ hasDefault: false;
994
+ isPrimaryKey: false;
995
+ isAutoincrement: false;
996
+ hasRuntimeDefault: false;
997
+ enumValues: [string, ...string[]];
998
+ baseColumn: never;
999
+ identity: undefined;
1000
+ generated: undefined;
1001
+ }, {}, {}>;
1002
+ };
1003
+ dialect: "pg";
1004
+ }>;
1005
+ /**
1006
+ * One row per decision on an activation, never edited -- the two-person
1007
+ * approval and self-approve-after-wait counting is a host's own check, not a
1008
+ * DB constraint (ADR 0016).
1009
+ */
1010
+ export declare const policyActivationApprovals: import("drizzle-orm/pg-core").PgTableWithColumns<{
1011
+ name: "authz_activation_approvals";
1012
+ schema: undefined;
1013
+ columns: {
1014
+ id: import("drizzle-orm/pg-core").PgColumn<{
1015
+ name: "id";
1016
+ tableName: "authz_activation_approvals";
1017
+ dataType: "string";
1018
+ columnType: "PgUUID";
1019
+ data: string;
1020
+ driverParam: string;
1021
+ notNull: true;
1022
+ hasDefault: true;
1023
+ isPrimaryKey: true;
1024
+ isAutoincrement: false;
1025
+ hasRuntimeDefault: false;
1026
+ enumValues: undefined;
1027
+ baseColumn: never;
1028
+ identity: undefined;
1029
+ generated: undefined;
1030
+ }, {}, {}>;
1031
+ activationId: import("drizzle-orm/pg-core").PgColumn<{
1032
+ name: "activation_id";
1033
+ tableName: "authz_activation_approvals";
1034
+ dataType: "string";
1035
+ columnType: "PgUUID";
1036
+ data: string;
1037
+ driverParam: string;
1038
+ notNull: true;
1039
+ hasDefault: false;
1040
+ isPrimaryKey: false;
1041
+ isAutoincrement: false;
1042
+ hasRuntimeDefault: false;
1043
+ enumValues: undefined;
1044
+ baseColumn: never;
1045
+ identity: undefined;
1046
+ generated: undefined;
1047
+ }, {}, {}>;
1048
+ approverId: import("drizzle-orm/pg-core").PgColumn<{
1049
+ name: "approver_id";
1050
+ tableName: "authz_activation_approvals";
1051
+ dataType: "string";
1052
+ columnType: "PgText";
1053
+ data: string;
1054
+ driverParam: string;
1055
+ notNull: true;
1056
+ hasDefault: false;
1057
+ isPrimaryKey: false;
1058
+ isAutoincrement: false;
1059
+ hasRuntimeDefault: false;
1060
+ enumValues: [string, ...string[]];
1061
+ baseColumn: never;
1062
+ identity: undefined;
1063
+ generated: undefined;
1064
+ }, {}, {}>;
1065
+ approverClass: import("drizzle-orm/pg-core").PgColumn<{
1066
+ name: "approver_class";
1067
+ tableName: "authz_activation_approvals";
1068
+ dataType: "string";
1069
+ columnType: "PgText";
1070
+ data: string;
1071
+ driverParam: string;
1072
+ notNull: true;
1073
+ hasDefault: false;
1074
+ isPrimaryKey: false;
1075
+ isAutoincrement: false;
1076
+ hasRuntimeDefault: false;
1077
+ enumValues: [string, ...string[]];
1078
+ baseColumn: never;
1079
+ identity: undefined;
1080
+ generated: undefined;
1081
+ }, {}, {}>;
1082
+ decision: import("drizzle-orm/pg-core").PgColumn<{
1083
+ name: "decision";
1084
+ tableName: "authz_activation_approvals";
1085
+ dataType: "string";
1086
+ columnType: "PgText";
1087
+ data: string;
1088
+ driverParam: string;
1089
+ notNull: true;
1090
+ hasDefault: false;
1091
+ isPrimaryKey: false;
1092
+ isAutoincrement: false;
1093
+ hasRuntimeDefault: false;
1094
+ enumValues: [string, ...string[]];
1095
+ baseColumn: never;
1096
+ identity: undefined;
1097
+ generated: undefined;
1098
+ }, {}, {}>;
1099
+ reason: import("drizzle-orm/pg-core").PgColumn<{
1100
+ name: "reason";
1101
+ tableName: "authz_activation_approvals";
1102
+ dataType: "string";
1103
+ columnType: "PgText";
1104
+ data: string;
1105
+ driverParam: string;
1106
+ notNull: false;
1107
+ hasDefault: false;
1108
+ isPrimaryKey: false;
1109
+ isAutoincrement: false;
1110
+ hasRuntimeDefault: false;
1111
+ enumValues: [string, ...string[]];
1112
+ baseColumn: never;
1113
+ identity: undefined;
1114
+ generated: undefined;
1115
+ }, {}, {}>;
1116
+ decidedAt: import("drizzle-orm/pg-core").PgColumn<{
1117
+ name: "decided_at";
1118
+ tableName: "authz_activation_approvals";
1119
+ dataType: "date";
1120
+ columnType: "PgTimestamp";
1121
+ data: Date;
1122
+ driverParam: string;
1123
+ notNull: true;
1124
+ hasDefault: true;
1125
+ isPrimaryKey: false;
1126
+ isAutoincrement: false;
1127
+ hasRuntimeDefault: false;
1128
+ enumValues: undefined;
1129
+ baseColumn: never;
1130
+ identity: undefined;
1131
+ generated: undefined;
1132
+ }, {}, {}>;
1133
+ };
1134
+ dialect: "pg";
1135
+ }>;
687
1136
  export declare const policyVersion: import("drizzle-orm/pg-core").PgTableWithColumns<{
688
1137
  name: "authz_policy_version";
689
1138
  schema: undefined;
@@ -46,6 +46,69 @@ export const policyAssignments = pgTable('authz_assignments', {
46
46
  index('authz_assignments_role_idx').on(t.tenantId, t.roleId),
47
47
  index('authz_assignments_team_idx').on(t.tenantId, t.teamId),
48
48
  ]);
49
+ /**
50
+ * A human principal's time-boxed request to exercise a subset of the
51
+ * ELEVATED permissions they are otherwise only eligible for (ADR 0016, "No
52
+ * standing authority"). Ported from Boule's `drizzle/0014_activations.sql`
53
+ * (authz#83 wave 11b, `migrations/0005_activations.sql` here);
54
+ * `authz_roles.updated_by` above and the `'activation'`
55
+ * `authz_assignments.source` value that same source file added are already
56
+ * in `0001_baseline.sql`, so only these two tables land in the new file.
57
+ */
58
+ export const policyActivations = pgTable('authz_activations', {
59
+ id: uuid('id').primaryKey().defaultRandom(),
60
+ tenantId: uuid('tenant_id').notNull(),
61
+ applicationId: text('application_id').notNull(),
62
+ platformId: text('platform_id').notNull(),
63
+ // Agents and services can never open an activation (no inbox for the
64
+ // self-approve-wait email, nothing eligible to approve on their behalf),
65
+ // so unlike memberships.principal_id/authz_assignments.principal_id this
66
+ // is always a person.
67
+ principalId: text('principal_id').notNull(),
68
+ principalClass: text('principal_class').notNull(),
69
+ // The requested subset of what the principal is eligible for. Checked
70
+ // against a host's policy `elevated` set in code, not here: that set is
71
+ // a host's own catalogue data, not the database's.
72
+ permissions: text('permissions').array().notNull(),
73
+ reason: text('reason').notNull(),
74
+ reference: text('reference').notNull(),
75
+ status: text('status').notNull().default('pending'),
76
+ requestedAt: timestamp('requested_at', { withTimezone: true }).notNull().defaultNow(),
77
+ startsAt: timestamp('starts_at', { withTimezone: true }).notNull(),
78
+ expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
79
+ // The non-primary authz_roles row minted with exactly the requested
80
+ // entries, the same indirection break_glass_sessions.role_id uses.
81
+ roleId: uuid('role_id').notNull(),
82
+ endedAt: timestamp('ended_at', { withTimezone: true }),
83
+ endedBy: text('ended_by'),
84
+ }, (t) => [
85
+ // Full definition is partial (WHERE status = 'pending') in the shipped
86
+ // migration; this version of drizzle's table builder cannot express
87
+ // that predicate, the same gap `src/testing/database.ts` documents for
88
+ // `bg_active_idx` and the other non-unique partial indexes this package
89
+ // ships.
90
+ index('authz_activations_pending_idx').on(t.tenantId, t.principalId),
91
+ index('authz_activations_tenant_idx').on(t.tenantId, t.requestedAt.desc()),
92
+ ]);
93
+ /**
94
+ * One row per decision on an activation, never edited -- the two-person
95
+ * approval and self-approve-after-wait counting is a host's own check, not a
96
+ * DB constraint (ADR 0016).
97
+ */
98
+ export const policyActivationApprovals = pgTable('authz_activation_approvals', {
99
+ id: uuid('id').primaryKey().defaultRandom(),
100
+ activationId: uuid('activation_id').notNull(),
101
+ // Same owner-column reasoning as policyActivations.principalId above: an
102
+ // approver is always a person.
103
+ approverId: text('approver_id').notNull(),
104
+ approverClass: text('approver_class').notNull(),
105
+ decision: text('decision').notNull(),
106
+ reason: text('reason'),
107
+ decidedAt: timestamp('decided_at', { withTimezone: true }).notNull().defaultNow(),
108
+ }, (t) => [
109
+ index('authz_activation_approvals_activation_idx').on(t.activationId, t.decidedAt.desc()),
110
+ index('authz_activation_approvals_approver_idx').on(t.approverId),
111
+ ]);
49
112
  export const policyVersion = pgTable('authz_policy_version', {
50
113
  id: integer('id').primaryKey(),
51
114
  version: integer('version').notNull(),
@@ -0,0 +1,71 @@
1
+ import { type ResourcePermission, type ResourceRole, type TenantKind } from '@wtfalch/authz';
2
+ import type { StoreBinding } from './binding.js';
3
+ /** One `systemRoles[kind][key]` entry of a host's policy catalogue JSON. */
4
+ export interface PolicyRoleTemplate {
5
+ readonly key: string;
6
+ readonly label: string;
7
+ readonly description: string;
8
+ readonly revision: number;
9
+ readonly guards: readonly string[];
10
+ readonly entries: readonly {
11
+ readonly permission: string;
12
+ readonly boundary?: string;
13
+ readonly scope?: unknown;
14
+ readonly relation?: string;
15
+ }[];
16
+ }
17
+ /**
18
+ * The shape a host's own `policy-catalogue.json` must have for `definePolicy` to build a
19
+ * `StorePolicy` from it. The store has no catalogue of its own — this is the host's data,
20
+ * typed so `definePolicy` can be generic over it.
21
+ */
22
+ export interface PolicyData {
23
+ readonly permissions: Record<string, ResourcePermission>;
24
+ readonly defaultCeiling: readonly string[];
25
+ readonly maxDepth: number;
26
+ /** Partial: a host's JSON need not define a role set for every `TenantKind` (Boule's has none for `product`); asking for a missing kind throws. */
27
+ readonly systemRoles: Partial<Record<TenantKind, Record<string, PolicyRoleTemplate>>>;
28
+ /** ADR 0016: the permissions that are eligible, not standingly usable, by default. */
29
+ readonly elevated: readonly string[];
30
+ /** The subset of `elevated` a second person must also approve before it activates. */
31
+ readonly coApproval: readonly string[];
32
+ }
33
+ /**
34
+ * A `StoreBinding` plus everything a host's own `policy-catalogue.ts` used to export: the
35
+ * compiled catalogue, the built-in and starter role templates, and the ADR 0016 sets. Only
36
+ * `roles.ts` and `boot.ts` need the template methods; everywhere else a plain `StoreBinding`
37
+ * (which this also is) is enough.
38
+ */
39
+ export interface StorePolicy extends StoreBinding {
40
+ readonly defaultCeiling: readonly string[];
41
+ readonly maxDepth: number;
42
+ /** ADR 0016: the permissions that are eligible, not standingly usable, by default. */
43
+ readonly elevated: ReadonlySet<string>;
44
+ /** The subset of `elevated` a second person must also approve before it activates. */
45
+ readonly requiresCoApproval: ReadonlySet<string>;
46
+ readonly offered: readonly string[];
47
+ /**
48
+ * ADR 0016: the only true built-in left is `self_service` — resynced on every boot. Every
49
+ * other `systemRoles` entry (`owner`, `admin`, `billing`, `member`) is a starter role, seeded
50
+ * once by `starterRoles` below, as an ordinary editable row nothing here resyncs again.
51
+ */
52
+ builtInRoles(organisationId: string, kind: TenantKind): Omit<ResourceRole, 'id'>[];
53
+ /** The starter role set a new tenant of this kind is seeded with once, at creation (ADR 0016). */
54
+ starterRoles(organisationId: string, kind: TenantKind): Omit<ResourceRole, 'id'>[];
55
+ }
56
+ /**
57
+ * Builds a `StorePolicy` from a host's own `policy-catalogue.json` data and its
58
+ * `{ applicationId, platformId }`. Generic over the host's data because every host's catalogue
59
+ * differs; this function is the one piece of that shape both applications wrote identically.
60
+ *
61
+ * `binding.hostEvents` (0.4.0), when given, is carried onto the returned `StorePolicy` unchanged
62
+ * so `record`/`recordAs`/`writeServiceEvent` can validate a host's own audit actions against it.
63
+ * A name in `hostEvents` that repeats a core event name is a configuration error — it would make
64
+ * an audit row ambiguous about which vocabulary it came from — so this throws rather than let it
65
+ * pass silently.
66
+ */
67
+ export declare function definePolicy(data: PolicyData, binding: {
68
+ readonly applicationId: string;
69
+ readonly platformId: string;
70
+ readonly hostEvents?: readonly string[];
71
+ }): StorePolicy;