@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
package/dist/policy.js ADDED
@@ -0,0 +1,78 @@
1
+ import { core, defineResourceCatalogue, } from '@wtfalch/authz';
2
+ /**
3
+ * Builds a `StorePolicy` from a host's own `policy-catalogue.json` data and its
4
+ * `{ applicationId, platformId }`. Generic over the host's data because every host's catalogue
5
+ * differs; this function is the one piece of that shape both applications wrote identically.
6
+ *
7
+ * `binding.hostEvents` (0.4.0), when given, is carried onto the returned `StorePolicy` unchanged
8
+ * so `record`/`recordAs`/`writeServiceEvent` can validate a host's own audit actions against it.
9
+ * A name in `hostEvents` that repeats a core event name is a configuration error — it would make
10
+ * an audit row ambiguous about which vocabulary it came from — so this throws rather than let it
11
+ * pass silently.
12
+ */
13
+ export function definePolicy(data, binding) {
14
+ const coreEvents = new Set(core.events);
15
+ for (const name of binding.hostEvents ?? []) {
16
+ if (coreEvents.has(name)) {
17
+ throw new Error(`definePolicy: hostEvents "${name}" collides with a core event name; a host event must not reuse one`);
18
+ }
19
+ }
20
+ const namespaces = [
21
+ ...new Set(Object.keys(data.permissions).map((key) => key.split(/[.:]/)[0])),
22
+ ];
23
+ const catalogue = defineResourceCatalogue(namespaces.map((namespace) => ({
24
+ namespace,
25
+ permissions: Object.values(data.permissions).filter((p) => p.id.split(/[.:]/)[0] === namespace),
26
+ })));
27
+ // A tenant kind this host has no roles for is a configuration error. Seeding it
28
+ // with nothing would leave the tenant without an owner seat, so fail loudly,
29
+ // as the hosts' own catalogue did.
30
+ const rolesFor = (kind) => {
31
+ const roles = data.systemRoles[kind];
32
+ if (!roles)
33
+ throw new Error(`the policy catalogue has no system roles for tenant kind "${kind}"`);
34
+ return roles;
35
+ };
36
+ function roleFrom(organisationId, role, builtIn) {
37
+ return {
38
+ key: role.key,
39
+ applicationId: binding.applicationId,
40
+ platformId: binding.platformId,
41
+ organisationId,
42
+ label: role.label,
43
+ description: role.description,
44
+ revision: role.revision,
45
+ builtIn,
46
+ guards: [...role.guards],
47
+ entries: role.entries.map((entry) => ({
48
+ permission: entry.permission,
49
+ boundary: (entry.boundary === 'platform'
50
+ ? { kind: 'platform' }
51
+ : { kind: 'organisation', organisationId }),
52
+ scope: ('scope' in entry ? entry.scope : { kind: 'organisation' }),
53
+ relation: ('relation' in entry ? entry.relation : 'any'),
54
+ })),
55
+ };
56
+ }
57
+ return {
58
+ applicationId: binding.applicationId,
59
+ platformId: binding.platformId,
60
+ catalogue,
61
+ hostEvents: binding.hostEvents,
62
+ defaultCeiling: data.defaultCeiling,
63
+ maxDepth: data.maxDepth,
64
+ elevated: new Set(data.elevated),
65
+ requiresCoApproval: new Set(data.coApproval),
66
+ offered: Object.keys(catalogue).filter((key) => catalogue[key]?.offered),
67
+ builtInRoles(organisationId, kind) {
68
+ return Object.values(rolesFor(kind))
69
+ .filter((role) => role.key === 'self_service')
70
+ .map((role) => roleFrom(organisationId, role, true));
71
+ },
72
+ starterRoles(organisationId, kind) {
73
+ return Object.values(rolesFor(kind))
74
+ .filter((role) => role.key !== 'self_service')
75
+ .map((role) => roleFrom(organisationId, role, false));
76
+ },
77
+ };
78
+ }
@@ -0,0 +1,43 @@
1
+ import type { StoreBinding } from './binding.js';
2
+ import type { DbOrTx } from './scoped.js';
3
+ import type { Access, Context, Principal } from './types.js';
4
+ /**
5
+ * D19's propagation, in one place: after a direct membership row is written
6
+ * in a tenant, make every derived row that depended on it match again, and
7
+ * write one audit row per change.
8
+ *
9
+ * Every direct write goes through here. There are five and they arrive from
10
+ * four files: the three writes in `memberships.ts`, an accepted invitation
11
+ * in `invitations.ts`, an installed owner in `install-owner.ts`, and a
12
+ * credential minted or revoked in `credentials.ts`. Item 1e left two private
13
+ * copies of this and 1f would have made a third, which is precisely how the
14
+ * five stop agreeing about what a propagating write does.
15
+ *
16
+ * Two rules worth restating here, because both were bugs before they were
17
+ * rules. It runs INSIDE the caller's own transaction, never after it commits,
18
+ * so a membership and the rows it implies are one atomic change. And the
19
+ * tenant itself is in the recomputed set, not only its descendants: removing
20
+ * a direct row at a nested tenant does not leave that person with nothing
21
+ * there, it leaves them with whatever an ancestor still grants.
22
+ */
23
+ /** How the audit rows get signed: from a resolved `Access`, or from a `Principal` for a write that has none yet (an acceptance, before the accepter is a member). */
24
+ export type PropagationWriter = {
25
+ readonly kind: 'access';
26
+ readonly access: Access;
27
+ } | {
28
+ readonly kind: 'principal';
29
+ readonly principal: Principal;
30
+ readonly context: Context;
31
+ };
32
+ /** Signs an audit row as whichever of the two the caller had. Neither path lets a caller name an actor of its own choosing; that is `audit.ts`'s rule and this only passes it through. */
33
+ export declare function writerFromAccess(access: Access): PropagationWriter;
34
+ export declare function writerFromPrincipal(principal: Principal, context: Context): PropagationWriter;
35
+ /**
36
+ * Recomputes the derived rows this write can have moved, and returns the
37
+ * cache tags for every tenant it touched, for the caller to add to its own.
38
+ *
39
+ * A tenant with nothing above it and nothing below it does no work at all,
40
+ * which is every tenant in an app that never attaches anything: the two
41
+ * reads below are the whole cost of nesting for an app that does not use it.
42
+ */
43
+ export declare function propagateMembershipChange(tx: DbOrTx, binding: StoreBinding, writer: PropagationWriter, tenantId: string, principalId: string): Promise<readonly string[]>;
@@ -0,0 +1,47 @@
1
+ import { record, recordAs } from './audit.js';
2
+ import { recomputeDerived } from './tree-writes.js';
3
+ import { derivedEventAction, descendantsOf, nodeById } from './tree.js';
4
+ import { tenantTag } from './types.js';
5
+ /** Signs an audit row as whichever of the two the caller had. Neither path lets a caller name an actor of its own choosing; that is `audit.ts`'s rule and this only passes it through. */
6
+ export function writerFromAccess(access) {
7
+ return { kind: 'access', access };
8
+ }
9
+ export function writerFromPrincipal(principal, context) {
10
+ return { kind: 'principal', principal, context };
11
+ }
12
+ /**
13
+ * Recomputes the derived rows this write can have moved, and returns the
14
+ * cache tags for every tenant it touched, for the caller to add to its own.
15
+ *
16
+ * A tenant with nothing above it and nothing below it does no work at all,
17
+ * which is every tenant in an app that never attaches anything: the two
18
+ * reads below are the whole cost of nesting for an app that does not use it.
19
+ */
20
+ export async function propagateMembershipChange(tx, binding, writer, tenantId, principalId) {
21
+ const node = await nodeById(tx, tenantId);
22
+ const descendants = await descendantsOf(tx, tenantId);
23
+ if (descendants.length === 0 && !node?.parentId)
24
+ return [];
25
+ const changes = await recomputeDerived(tx, binding, [tenantId, ...descendants.map((d) => d.id)], [principalId]);
26
+ const tags = new Set();
27
+ for (const change of changes) {
28
+ tags.add(tenantTag(change.tenantId));
29
+ const input = {
30
+ action: derivedEventAction(change.kind),
31
+ tenantId: change.tenantId,
32
+ targetType: 'membership',
33
+ targetId: change.principalId,
34
+ before: change.before
35
+ ? { role: change.before.role, viaTenantId: change.before.viaTenantId }
36
+ : undefined,
37
+ after: change.after
38
+ ? { role: change.after.role, viaTenantId: change.after.viaTenantId }
39
+ : undefined,
40
+ };
41
+ if (writer.kind === 'access')
42
+ await record(tx, writer.access, input);
43
+ else
44
+ await recordAs(tx, writer.principal, writer.context, input);
45
+ }
46
+ return [...tags];
47
+ }
@@ -0,0 +1,78 @@
1
+ import type { StoreBinding } from './binding.js';
2
+ import type { DbOrTx } from './scoped.js';
3
+ /**
4
+ * D19's drift check: every derived membership row compared against what the
5
+ * tenant tree actually calls for, read only.
6
+ *
7
+ * It reports a difference and never fixes one. D19's failure mode is drift, a
8
+ * row that should have gone and did not or one that never got written, and a
9
+ * check that silently repairs a wrong row hides the bug that let it drift.
10
+ * See `tree.ts`'s header for the derived rule this compares against; it asks
11
+ * that file the same question the writers answer, rather than keeping a
12
+ * second copy of the rule that could be wrong in the same way.
13
+ *
14
+ * `reconcileDerived` takes whatever handle its caller already has and opens
15
+ * no connection of its own, so a nightly job, a route and a test all reach it
16
+ * the same way.
17
+ */
18
+ /**
19
+ * A derived row that is present but that the tree no longer calls for: the
20
+ * parent link was cleared without `detachInTx` running, the ancestor's own
21
+ * direct row went without `recomputeDerived` running, or the row was written
22
+ * by hand. Nothing about the tenant that held it is assumed still valid, so
23
+ * `viaTenantId` is reported exactly as stored, including null.
24
+ */
25
+ export interface ReconcileOrphan {
26
+ readonly tenantId: string;
27
+ readonly principalId: string;
28
+ readonly role: string;
29
+ readonly viaTenantId: string | null;
30
+ }
31
+ /** A derived row the tree calls for that is not there. */
32
+ export interface ReconcileMissing {
33
+ readonly tenantId: string;
34
+ readonly principalId: string;
35
+ readonly role: string;
36
+ readonly viaTenantId: string;
37
+ }
38
+ /** A derived row that is present for the right principal but does not match what the tree calls for. */
39
+ export interface ReconcileWrongRole {
40
+ readonly tenantId: string;
41
+ readonly principalId: string;
42
+ readonly actual: {
43
+ readonly role: string;
44
+ readonly viaTenantId: string | null;
45
+ };
46
+ readonly expected: {
47
+ readonly role: string;
48
+ readonly viaTenantId: string;
49
+ };
50
+ }
51
+ /** What one reconciliation run found. Every array is empty on a healthy tree. */
52
+ export interface ReconcileReport {
53
+ readonly orphans: readonly ReconcileOrphan[];
54
+ readonly missing: readonly ReconcileMissing[];
55
+ readonly wrongRole: readonly ReconcileWrongRole[];
56
+ /** How many tenants were asked the question, whether or not they turned up a difference. */
57
+ readonly checkedTenants: number;
58
+ /** How many stored `inherited` rows were read across those tenants. */
59
+ readonly checkedRows: number;
60
+ }
61
+ /**
62
+ * Whether the `inherited` membership rows in the database match what
63
+ * `expectedDerivedFor` (tree.ts) says they should be, for every tenant a
64
+ * derived row could possibly belong to. Read-only: nothing here writes.
65
+ *
66
+ * A tenant is worth asking about in one of two ways: it has a `parent_id`,
67
+ * so the rule may call for rows in it, or it currently holds at least one
68
+ * `inherited` row, so a difference can exist even if its `parent_id` was
69
+ * cleared without the recomputation that should have gone with it (which is
70
+ * exactly the corruption an orphan row represents). Checking the union
71
+ * rather than only the first is what lets a hand-cleared or hand-inserted
72
+ * row be caught rather than silently skipped.
73
+ *
74
+ * Pure over the handle it is given: a host's own nightly job builds a plain
75
+ * handle from its own `DATABASE_URL`, and a test passes a transaction
76
+ * directly.
77
+ */
78
+ export declare function reconcileDerived(handle: DbOrTx, binding: StoreBinding): Promise<ReconcileReport>;
@@ -0,0 +1,94 @@
1
+ import { and, eq } from 'drizzle-orm';
2
+ import { primaryRoleKey } from './assignments.js';
3
+ import { memberships, tenants } from './schema.js';
4
+ import { expectedDerivedFor } from './tree.js';
5
+ /**
6
+ * Whether the `inherited` membership rows in the database match what
7
+ * `expectedDerivedFor` (tree.ts) says they should be, for every tenant a
8
+ * derived row could possibly belong to. Read-only: nothing here writes.
9
+ *
10
+ * A tenant is worth asking about in one of two ways: it has a `parent_id`,
11
+ * so the rule may call for rows in it, or it currently holds at least one
12
+ * `inherited` row, so a difference can exist even if its `parent_id` was
13
+ * cleared without the recomputation that should have gone with it (which is
14
+ * exactly the corruption an orphan row represents). Checking the union
15
+ * rather than only the first is what lets a hand-cleared or hand-inserted
16
+ * row be caught rather than silently skipped.
17
+ *
18
+ * Pure over the handle it is given: a host's own nightly job builds a plain
19
+ * handle from its own `DATABASE_URL`, and a test passes a transaction
20
+ * directly.
21
+ */
22
+ export async function reconcileDerived(handle, binding) {
23
+ const tenantRows = await handle
24
+ .select({ id: tenants.id, parentId: tenants.parentId })
25
+ .from(tenants);
26
+ const toCheck = new Set();
27
+ for (const row of tenantRows) {
28
+ if (row.parentId !== null)
29
+ toCheck.add(row.id);
30
+ }
31
+ const inheritedRows = await handle
32
+ .select({ tenantId: memberships.tenantId })
33
+ .from(memberships)
34
+ .where(eq(memberships.source, 'inherited'));
35
+ for (const row of inheritedRows)
36
+ toCheck.add(row.tenantId);
37
+ const orphans = [];
38
+ const missing = [];
39
+ const wrongRole = [];
40
+ let checkedRows = 0;
41
+ for (const tenantId of [...toCheck].sort()) {
42
+ const expected = await expectedDerivedFor(handle, binding, tenantId);
43
+ const identity = (row) => `${row.principalClass}:${row.principalId}`;
44
+ const expectedById = new Map(expected.map((row) => [identity(row), row]));
45
+ const actual = await handle
46
+ .select({
47
+ principalId: memberships.principalId,
48
+ principalClass: memberships.principalClass,
49
+ role: primaryRoleKey(binding),
50
+ viaTenantId: memberships.viaTenantId,
51
+ })
52
+ .from(memberships)
53
+ .where(and(eq(memberships.tenantId, tenantId), eq(memberships.source, 'inherited')));
54
+ checkedRows += actual.length;
55
+ const actualById = new Map(actual.map((row) => [identity(row), row]));
56
+ for (const row of actual) {
57
+ const want = expectedById.get(identity(row));
58
+ if (!want) {
59
+ orphans.push({
60
+ tenantId,
61
+ principalId: row.principalId,
62
+ role: row.role,
63
+ viaTenantId: row.viaTenantId,
64
+ });
65
+ continue;
66
+ }
67
+ if (want.role !== row.role || want.viaTenantId !== row.viaTenantId) {
68
+ wrongRole.push({
69
+ tenantId,
70
+ principalId: row.principalId,
71
+ actual: { role: row.role, viaTenantId: row.viaTenantId },
72
+ expected: { role: want.role, viaTenantId: want.viaTenantId },
73
+ });
74
+ }
75
+ }
76
+ for (const row of expected) {
77
+ if (!actualById.has(identity(row))) {
78
+ missing.push({
79
+ tenantId,
80
+ principalId: row.principalId,
81
+ role: row.role,
82
+ viaTenantId: row.viaTenantId,
83
+ });
84
+ }
85
+ }
86
+ }
87
+ return {
88
+ orphans,
89
+ missing,
90
+ wrongRole,
91
+ checkedTenants: toCheck.size,
92
+ checkedRows,
93
+ };
94
+ }
@@ -0,0 +1,344 @@
1
+ import type { DbOrTx, Scope } from './scoped.js';
2
+ import { type Access, type Result } from './types.js';
3
+ /** Every surface consumes the same request-time evaluator and canonical stored assignments. */
4
+ export declare function resourceAccessFor(access: Access, tx: DbOrTx, db: DbOrTx, scopeColumn: Scope['column']): Promise<{
5
+ foreignPolicy: boolean;
6
+ evaluator: import("@wtfalch/authz").ResourceAccess<string>;
7
+ teams: {
8
+ id: string;
9
+ tenantId: string;
10
+ parentId: string | null;
11
+ name: string;
12
+ archivedAt: Date | null;
13
+ createdAt: Date;
14
+ }[];
15
+ participants: {
16
+ tenantId: string;
17
+ teamId: string;
18
+ principalId: string;
19
+ principalClass: string;
20
+ }[];
21
+ roleRows: {
22
+ permissions: {
23
+ permission: string;
24
+ relation: "any" | "owner" | "actor" | "subject";
25
+ }[];
26
+ id: string;
27
+ tenantId: string;
28
+ applicationId: string;
29
+ platformId: string;
30
+ key: string;
31
+ name: string;
32
+ description: string;
33
+ builtIn: boolean;
34
+ revision: number;
35
+ entries: {
36
+ permission: string;
37
+ boundary: {
38
+ kind: "organisation";
39
+ organisationId: string;
40
+ descendants?: boolean | undefined;
41
+ } | {
42
+ kind: "organisations";
43
+ organisationIds: string[];
44
+ } | {
45
+ kind: "platform";
46
+ };
47
+ scope: {
48
+ kind: "organisation";
49
+ } | {
50
+ kind: "team";
51
+ teamId: string;
52
+ descendants: boolean;
53
+ } | {
54
+ kind: "own_teams";
55
+ descendants: boolean;
56
+ } | {
57
+ kind: "resource";
58
+ resourceType: string;
59
+ resourceId: string;
60
+ };
61
+ relation: "any" | "owner" | "actor" | "subject";
62
+ }[];
63
+ guards: string[];
64
+ createdBy: string | null;
65
+ createdAt: Date;
66
+ updatedAt: Date;
67
+ updatedBy: string | null;
68
+ }[];
69
+ assignmentRows: {
70
+ id: string;
71
+ tenantId: string;
72
+ applicationId: string;
73
+ platformId: string;
74
+ roleId: string;
75
+ principalId: string | null;
76
+ principalClass: string | null;
77
+ teamId: string | null;
78
+ recipientDescendants: boolean;
79
+ boundary: {
80
+ kind: "organisation";
81
+ organisationId: string;
82
+ descendants?: boolean | undefined;
83
+ } | {
84
+ kind: "organisations";
85
+ organisationIds: string[];
86
+ } | {
87
+ kind: "platform";
88
+ };
89
+ scope: {
90
+ kind: "organisation";
91
+ } | {
92
+ kind: "team";
93
+ teamId: string;
94
+ descendants: boolean;
95
+ } | {
96
+ kind: "own_teams";
97
+ descendants: boolean;
98
+ } | {
99
+ kind: "resource";
100
+ resourceType: string;
101
+ resourceId: string;
102
+ };
103
+ source: string;
104
+ viaTenantId: string | null;
105
+ primary: boolean;
106
+ expiresAt: Date | null;
107
+ createdBy: string | null;
108
+ createdAt: Date;
109
+ }[];
110
+ grants: {
111
+ id: string;
112
+ applicationId: string;
113
+ platformId: string;
114
+ boundary: {
115
+ kind: "organisation";
116
+ organisationId: string;
117
+ descendants?: boolean | undefined;
118
+ } | {
119
+ kind: "organisations";
120
+ organisationIds: string[];
121
+ } | {
122
+ kind: "platform";
123
+ };
124
+ permission: string;
125
+ recipient: {
126
+ kind: "principal";
127
+ principal: {
128
+ id: string;
129
+ class: "human" | "api_key" | "agent" | "service";
130
+ };
131
+ } | {
132
+ kind: "team";
133
+ teamId: string;
134
+ descendants: boolean;
135
+ };
136
+ scope: {
137
+ kind: "organisation";
138
+ } | {
139
+ kind: "team";
140
+ teamId: string;
141
+ descendants: boolean;
142
+ } | {
143
+ kind: "own_teams";
144
+ descendants: boolean;
145
+ } | {
146
+ kind: "resource";
147
+ resourceType: string;
148
+ resourceId: string;
149
+ };
150
+ relation: "any" | "owner" | "actor" | "subject";
151
+ constraints?: ({
152
+ kind: "organisation";
153
+ } | {
154
+ kind: "team";
155
+ teamId: string;
156
+ descendants: boolean;
157
+ } | {
158
+ kind: "own_teams";
159
+ descendants: boolean;
160
+ } | {
161
+ kind: "resource";
162
+ resourceType: string;
163
+ resourceId: string;
164
+ })[] | undefined;
165
+ startsAt?: number | undefined;
166
+ expiresAt?: number | undefined;
167
+ conditions?: ({
168
+ kind: "attribute";
169
+ attribute: string;
170
+ in: string[];
171
+ } | {
172
+ kind: "schedule";
173
+ timeZone: string;
174
+ windows: {
175
+ days: ("mon" | "tue" | "wed" | "thu" | "fri" | "sat" | "sun")[];
176
+ from: string;
177
+ to: string;
178
+ }[];
179
+ })[] | undefined;
180
+ guest?: true | undefined;
181
+ budget?: {
182
+ limit: number;
183
+ windowSeconds: number;
184
+ } | undefined;
185
+ }[];
186
+ identities: Map<string, {
187
+ id: string;
188
+ class: "human";
189
+ }>;
190
+ }>;
191
+ export declare function resourceOverview(access: Access, db: DbOrTx, scopeColumn: Scope['column']): Promise<{
192
+ teams: {
193
+ parentId: string | null;
194
+ canUpdate: boolean;
195
+ canCreateChild: boolean;
196
+ canAddPeople: boolean;
197
+ canRemovePeople: boolean;
198
+ canArchive: boolean;
199
+ canReadPeople: boolean;
200
+ id: string;
201
+ tenantId: string;
202
+ name: string;
203
+ archivedAt: Date | null;
204
+ createdAt: Date;
205
+ }[];
206
+ participants: {
207
+ tenantId: string;
208
+ teamId: string;
209
+ principalId: string;
210
+ principalClass: string;
211
+ }[];
212
+ roles: {
213
+ permissions: {
214
+ permission: string;
215
+ relation: "any" | "owner" | "actor" | "subject";
216
+ }[];
217
+ id: string;
218
+ tenantId: string;
219
+ applicationId: string;
220
+ platformId: string;
221
+ key: string;
222
+ name: string;
223
+ description: string;
224
+ builtIn: boolean;
225
+ revision: number;
226
+ entries: {
227
+ permission: string;
228
+ boundary: {
229
+ kind: "organisation";
230
+ organisationId: string;
231
+ descendants?: boolean | undefined;
232
+ } | {
233
+ kind: "organisations";
234
+ organisationIds: string[];
235
+ } | {
236
+ kind: "platform";
237
+ };
238
+ scope: {
239
+ kind: "organisation";
240
+ } | {
241
+ kind: "team";
242
+ teamId: string;
243
+ descendants: boolean;
244
+ } | {
245
+ kind: "own_teams";
246
+ descendants: boolean;
247
+ } | {
248
+ kind: "resource";
249
+ resourceType: string;
250
+ resourceId: string;
251
+ };
252
+ relation: "any" | "owner" | "actor" | "subject";
253
+ }[];
254
+ guards: string[];
255
+ createdBy: string | null;
256
+ createdAt: Date;
257
+ updatedAt: Date;
258
+ updatedBy: string | null;
259
+ }[];
260
+ assignments: {
261
+ id: string;
262
+ tenantId: string;
263
+ applicationId: string;
264
+ platformId: string;
265
+ roleId: string;
266
+ principalId: string | null;
267
+ principalClass: string | null;
268
+ teamId: string | null;
269
+ recipientDescendants: boolean;
270
+ boundary: {
271
+ kind: "organisation";
272
+ organisationId: string;
273
+ descendants?: boolean | undefined;
274
+ } | {
275
+ kind: "organisations";
276
+ organisationIds: string[];
277
+ } | {
278
+ kind: "platform";
279
+ };
280
+ scope: {
281
+ kind: "organisation";
282
+ } | {
283
+ kind: "team";
284
+ teamId: string;
285
+ descendants: boolean;
286
+ } | {
287
+ kind: "own_teams";
288
+ descendants: boolean;
289
+ } | {
290
+ kind: "resource";
291
+ resourceType: string;
292
+ resourceId: string;
293
+ };
294
+ source: string;
295
+ viaTenantId: string | null;
296
+ primary: boolean;
297
+ expiresAt: Date | null;
298
+ createdBy: string | null;
299
+ createdAt: Date;
300
+ }[];
301
+ canReadRoles: boolean;
302
+ canReadTeams: boolean;
303
+ canCreateTeam: boolean;
304
+ canCreateRole: boolean;
305
+ canAssign: boolean;
306
+ canUnassign: boolean;
307
+ canDeleteRoles: boolean;
308
+ canEditTeams: boolean;
309
+ canEditRoles: boolean;
310
+ canReadAudit: boolean;
311
+ }>;
312
+ export declare function saveAppRole(access: Access, db: DbOrTx, scopeColumn: Scope['column'], input: unknown): Promise<Result<{
313
+ id: string;
314
+ }>>;
315
+ export declare function assignAppRole(access: Access, db: DbOrTx, scopeColumn: Scope['column'], input: unknown): Promise<Result<{
316
+ id: string;
317
+ }>>;
318
+ export declare function removeAppAssignment(access: Access, db: DbOrTx, scopeColumn: Scope['column'], id: string): Promise<Result<void>>;
319
+ export declare function deleteAppRole(access: Access, db: DbOrTx, scopeColumn: Scope['column'], id: string): Promise<Result<void>>;
320
+ export declare function saveTeam(access: Access, db: DbOrTx, scopeColumn: Scope['column'], input: unknown): Promise<Result<{
321
+ id: string;
322
+ }>>;
323
+ export declare function archiveTeam(access: Access, db: DbOrTx, scopeColumn: Scope['column'], id: string): Promise<Result<void>>;
324
+ export declare function changeTeamParticipant(access: Access, db: DbOrTx, scopeColumn: Scope['column'], input: unknown): Promise<Result<void>>;
325
+ export declare function scopedAuditLog(access: Access, db: DbOrTx, scopeColumn: Scope['column'], options?: {
326
+ id?: string;
327
+ limit?: number;
328
+ after?: {
329
+ occurredAt: Date;
330
+ id: number;
331
+ };
332
+ }): Promise<{
333
+ items: {
334
+ id: number;
335
+ occurredAt: Date;
336
+ action: string;
337
+ outcome: string;
338
+ teamId: string | null;
339
+ }[];
340
+ next: {
341
+ occurredAt: Date;
342
+ id: number;
343
+ } | null;
344
+ } | null>;