@wtfalch/authz-store 0.2.0 → 0.3.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.
- package/README.md +335 -5
- package/dist/activations.d.ts +60 -0
- package/dist/activations.js +516 -0
- package/dist/alerts.d.ts +86 -0
- package/dist/alerts.js +132 -0
- package/dist/assignments.d.ts +71 -0
- package/dist/assignments.js +103 -0
- package/dist/audit.d.ts +122 -0
- package/dist/audit.js +194 -0
- package/dist/binding.d.ts +17 -0
- package/dist/binding.js +8 -0
- package/dist/boot.d.ts +54 -0
- package/dist/boot.js +143 -0
- package/dist/bootstrap.d.ts +28 -0
- package/dist/bootstrap.js +83 -0
- package/dist/break-glass.d.ts +61 -0
- package/dist/break-glass.js +247 -0
- package/dist/credentials.d.ts +94 -0
- package/dist/credentials.js +230 -0
- package/dist/denial.d.ts +9 -0
- package/dist/denial.js +49 -0
- package/dist/erase.d.ts +58 -0
- package/dist/erase.js +108 -0
- package/dist/events.d.ts +77 -0
- package/dist/events.js +107 -0
- package/dist/export.d.ts +161 -0
- package/dist/export.js +293 -0
- package/dist/index.d.ts +33 -1
- package/dist/index.js +33 -1
- package/dist/install-owner.d.ts +25 -0
- package/dist/install-owner.js +159 -0
- package/dist/invitations.d.ts +160 -0
- package/dist/invitations.js +685 -0
- package/dist/membership-rows.d.ts +206 -0
- package/dist/membership-rows.js +271 -0
- package/dist/memberships.d.ts +87 -0
- package/dist/memberships.js +272 -0
- package/dist/migrate.js +15 -4
- package/dist/nesting.d.ts +124 -0
- package/dist/nesting.js +515 -0
- package/dist/owners.d.ts +6 -1
- package/dist/owners.js +7 -3
- package/dist/person-records.d.ts +186 -0
- package/dist/person-records.js +263 -0
- package/dist/platform.d.ts +20 -0
- package/dist/platform.js +65 -0
- package/dist/policy-access.d.ts +260 -0
- package/dist/policy-access.js +348 -0
- package/dist/policy-resources.d.ts +5 -0
- package/dist/policy-resources.js +46 -0
- package/dist/policy-schema.d.ts +445 -0
- package/dist/policy-schema.js +63 -0
- package/dist/policy.d.ts +64 -0
- package/dist/policy.js +65 -0
- package/dist/propagate.d.ts +43 -0
- package/dist/propagate.js +47 -0
- package/dist/reconcile.d.ts +78 -0
- package/dist/reconcile.js +94 -0
- package/dist/resource-access.d.ts +344 -0
- package/dist/resource-access.js +656 -0
- package/dist/role-keys.d.ts +9 -0
- package/dist/role-keys.js +9 -0
- package/dist/roles.d.ts +36 -0
- package/dist/roles.js +191 -0
- package/dist/schema.d.ts +18 -1
- package/dist/schema.js +8 -1
- package/dist/startup.d.ts +57 -0
- package/dist/startup.js +113 -0
- package/dist/tenants.d.ts +213 -0
- package/dist/tenants.js +808 -0
- package/dist/tree-writes.d.ts +65 -0
- package/dist/tree-writes.js +201 -0
- package/dist/tree.d.ts +272 -0
- package/dist/tree.js +565 -0
- package/dist/types.d.ts +87 -0
- package/dist/types.js +15 -0
- package/migrations/0003_product_tenant_kind.sql +14 -0
- package/migrations/0004_credential_keys_issued_id.sql +33 -0
- package/migrations/0005_activations.sql +71 -0
- package/migrations/0006_erase_person.sql +134 -0
- package/package.json +9 -4
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { DbOrTx, Scope } from './scoped.js';
|
|
2
|
+
import type { Access, Result } from './types.js';
|
|
3
|
+
export interface InstallOwnerOptions {
|
|
4
|
+
readonly tenantId: string;
|
|
5
|
+
readonly principalId: string;
|
|
6
|
+
/** A verified reference (the tenant's billing contact confirming by mail) that bypasses the freshness check. */
|
|
7
|
+
readonly reference?: string;
|
|
8
|
+
/** Days without any owner sign-in before the seat counts as inactive. */
|
|
9
|
+
readonly periodDays?: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* What a host must supply about a person's own sign-in history. That table
|
|
13
|
+
* (`profiles`) is the host's, not the store's, so the two reads Boule made
|
|
14
|
+
* directly against it become this lookup instead: `personKnown` answers "has
|
|
15
|
+
* this id ever signed in here" for the person being installed, and
|
|
16
|
+
* `lastSeenAt` answers it for a batch of a tenant's current direct owners at
|
|
17
|
+
* once, so `installOwner` can compare every owner's activity against
|
|
18
|
+
* `periodDays` in one pass. A person missing from the returned map, or
|
|
19
|
+
* mapped to `null`, counts as never signed in.
|
|
20
|
+
*/
|
|
21
|
+
export interface OwnerActivityLookup {
|
|
22
|
+
personKnown(principalId: string): Promise<boolean>;
|
|
23
|
+
lastSeenAt(principalIds: readonly string[]): Promise<ReadonlyMap<string, Date | null>>;
|
|
24
|
+
}
|
|
25
|
+
export declare function installOwner(operatorAccess: Access, db: DbOrTx, scopeColumn: Scope['column'], activity: OwnerActivityLookup, options: InstallOwnerOptions): Promise<Result<void>>;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { and, eq, sql } from 'drizzle-orm';
|
|
2
|
+
import { writeParticipationPolicy, writePrimaryAssignment } from './assignments.js';
|
|
3
|
+
import { record } from './audit.js';
|
|
4
|
+
import { permitsPlatform, refreshAccess } from './policy-access.js';
|
|
5
|
+
import { roleDef } from './roles.js';
|
|
6
|
+
import { memberships, tenants } from './schema.js';
|
|
7
|
+
import { recomputeDerived } from './tree-writes.js';
|
|
8
|
+
import { derivedEventAction, descendantsOf } from './tree.js';
|
|
9
|
+
import { done, isUuid, refused, tenantTag } from './types.js';
|
|
10
|
+
/**
|
|
11
|
+
* The one exception to D8's rule that no session, and no standing operator
|
|
12
|
+
* reach, ever changes who belongs to a tenant. A tenant whose owner seat is
|
|
13
|
+
* gone or compromised has nobody left inside it who could appoint a
|
|
14
|
+
* replacement, which is exactly what the last-owner guard elsewhere is
|
|
15
|
+
* designed to prevent working around, so this is an ordinary operator
|
|
16
|
+
* action, gated by its own permission, never a break-glass session (M1).
|
|
17
|
+
*/
|
|
18
|
+
const DEFAULT_PERIOD_DAYS = 30;
|
|
19
|
+
export async function installOwner(operatorAccess, db, scopeColumn, activity, options) {
|
|
20
|
+
if (operatorAccess.context === 'break_glass') {
|
|
21
|
+
return refused('break_glass', 'A support session never installs an owner.');
|
|
22
|
+
}
|
|
23
|
+
if (operatorAccess.tenant.kind !== 'operator' ||
|
|
24
|
+
!permitsPlatform(operatorAccess, 'owners:install')) {
|
|
25
|
+
return refused('no_permission', 'You do not have permission to install an owner.');
|
|
26
|
+
}
|
|
27
|
+
if (!isUuid(options.tenantId)) {
|
|
28
|
+
return refused('tenant_not_found', 'This organisation no longer exists.');
|
|
29
|
+
}
|
|
30
|
+
if (options.tenantId === operatorAccess.tenant.id) {
|
|
31
|
+
return refused('operator_tenant', 'The operator organisation is not what this action is for.');
|
|
32
|
+
}
|
|
33
|
+
if (!(await activity.personKnown(options.principalId)))
|
|
34
|
+
return refused('unknown_person', 'This person has never signed in here.');
|
|
35
|
+
return db.transaction(async (tx) => {
|
|
36
|
+
const fresh = await refreshAccess(tx, db, scopeColumn, operatorAccess);
|
|
37
|
+
if (!fresh || fresh.context !== 'operator' || !permitsPlatform(fresh, 'owners:install'))
|
|
38
|
+
return refused('no_permission', 'Your authority changed.');
|
|
39
|
+
const currentAccess = fresh;
|
|
40
|
+
const [tenantRow] = await tx
|
|
41
|
+
.select({ id: tenants.id, name: tenants.name })
|
|
42
|
+
.from(tenants)
|
|
43
|
+
.where(eq(tenants.id, options.tenantId))
|
|
44
|
+
.for('update');
|
|
45
|
+
if (!tenantRow)
|
|
46
|
+
return refused('tenant_not_found', 'This organisation no longer exists.');
|
|
47
|
+
const owners = await tx
|
|
48
|
+
.select({ principalId: memberships.principalId })
|
|
49
|
+
.from(memberships)
|
|
50
|
+
.where(and(eq(memberships.tenantId, tenantRow.id), eq(memberships.principalClass, 'human'),
|
|
51
|
+
// Not `and r.built_in`: a starter owner role (ADR 0016) is
|
|
52
|
+
// `built_in: false` for a tenant created after that change, but
|
|
53
|
+
// only the app's own owner role can ever hold the literal key
|
|
54
|
+
// `owner` (custom role keys always start with `c_`).
|
|
55
|
+
sql `exists(select 1 from authz_assignments a join authz_roles r on r.id=a.role_id where a.tenant_id=${memberships.tenantId} and a.principal_id=${memberships.principalId} and a.principal_class='human' and a.source='direct' and a.expires_at is null and r.key='owner')`, eq(memberships.source, 'direct')));
|
|
56
|
+
const lastSeen = await activity.lastSeenAt(owners.map((owner) => owner.principalId));
|
|
57
|
+
const periodMs = (options.periodDays ?? DEFAULT_PERIOD_DAYS) * 24 * 60 * 60 * 1000;
|
|
58
|
+
const cutoff = Date.now() - periodMs;
|
|
59
|
+
// Null (or missing from the map) counts as never signed in: this asks
|
|
60
|
+
// whether SOME owner is still within the period, not whether every
|
|
61
|
+
// owner has a timestamp at all.
|
|
62
|
+
const anOwnerIsActive = owners.some((owner) => {
|
|
63
|
+
const seenAt = lastSeen.get(owner.principalId) ?? null;
|
|
64
|
+
return seenAt !== null && seenAt.getTime() >= cutoff;
|
|
65
|
+
});
|
|
66
|
+
let reason;
|
|
67
|
+
if (owners.length === 0) {
|
|
68
|
+
reason = 'no_owner';
|
|
69
|
+
}
|
|
70
|
+
else if (!anOwnerIsActive) {
|
|
71
|
+
reason = 'owners_inactive';
|
|
72
|
+
}
|
|
73
|
+
else if (options.reference) {
|
|
74
|
+
// An owner is still within the period; only a verified reference
|
|
75
|
+
// overrides that, which is why this is checked last rather than first.
|
|
76
|
+
reason = 'reference';
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
return refused('owners_active', 'An owner has signed in recently. A verified reference is needed to install another.');
|
|
80
|
+
}
|
|
81
|
+
const [existing] = await tx
|
|
82
|
+
.select({ principalId: memberships.principalId, source: memberships.source })
|
|
83
|
+
.from(memberships)
|
|
84
|
+
.where(and(eq(memberships.tenantId, tenantRow.id), eq(memberships.principalId, options.principalId), eq(memberships.principalClass, 'human')))
|
|
85
|
+
.limit(1);
|
|
86
|
+
if (existing) {
|
|
87
|
+
// grantedBy moves to the operator too: this is the operator's own act
|
|
88
|
+
// right now, not a continuation of whoever originally added the row.
|
|
89
|
+
//
|
|
90
|
+
// `source` moves to direct as well, and `via_tenant_id` goes with it.
|
|
91
|
+
// The point of this function is that the tenant gains an owner seat of
|
|
92
|
+
// its own, and `ownerCoverage` counts direct rows only (D19), so
|
|
93
|
+
// promoting a row that arrived from a parent and leaving it derived
|
|
94
|
+
// would satisfy nothing: the tenant would still read as ownerless, and
|
|
95
|
+
// the next propagation would recompute the role straight back off it.
|
|
96
|
+
await tx
|
|
97
|
+
.update(memberships)
|
|
98
|
+
.set({
|
|
99
|
+
source: 'direct',
|
|
100
|
+
viaTenantId: null,
|
|
101
|
+
grantedBy: currentAccess.actor.id,
|
|
102
|
+
updatedAt: new Date(),
|
|
103
|
+
})
|
|
104
|
+
.where(and(eq(memberships.tenantId, tenantRow.id), eq(memberships.principalId, options.principalId), eq(memberships.principalClass, 'human')));
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
await tx.insert(memberships).values({
|
|
108
|
+
tenantId: tenantRow.id,
|
|
109
|
+
principalId: options.principalId,
|
|
110
|
+
principalClass: 'human',
|
|
111
|
+
source: 'direct',
|
|
112
|
+
tenantName: tenantRow.name,
|
|
113
|
+
grantedBy: currentAccess.actor.id,
|
|
114
|
+
});
|
|
115
|
+
await writeParticipationPolicy(tx, currentAccess.binding, tenantRow.id, {
|
|
116
|
+
id: options.principalId,
|
|
117
|
+
class: 'human',
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
const owner = await roleDef(tx, currentAccess.binding, tenantRow, 'owner');
|
|
121
|
+
if (!owner)
|
|
122
|
+
throw new Error('Missing owner role');
|
|
123
|
+
await writePrimaryAssignment(tx, currentAccess.binding, owner, { id: options.principalId, class: 'human' }, { createdBy: currentAccess.actor.id });
|
|
124
|
+
await record(tx, currentAccess, {
|
|
125
|
+
action: 'owner.installed',
|
|
126
|
+
tenantId: tenantRow.id,
|
|
127
|
+
targetType: 'membership',
|
|
128
|
+
targetId: options.principalId,
|
|
129
|
+
after: { role: 'owner' },
|
|
130
|
+
reason,
|
|
131
|
+
reference: options.reference,
|
|
132
|
+
});
|
|
133
|
+
// D19: the row just written is a direct membership, so a tenant holding
|
|
134
|
+
// children owes them the derived rows it implies, the same as every
|
|
135
|
+
// other direct write. An installed owner who did not reach the children
|
|
136
|
+
// would be an owner of a parent with no authority below it.
|
|
137
|
+
const descendants = await descendantsOf(tx, tenantRow.id);
|
|
138
|
+
const tags = [tenantTag(tenantRow.id)];
|
|
139
|
+
if (descendants.length > 0) {
|
|
140
|
+
const changes = await recomputeDerived(tx, currentAccess.binding, descendants.map((node) => node.id), [options.principalId]);
|
|
141
|
+
for (const change of changes) {
|
|
142
|
+
tags.push(tenantTag(change.tenantId));
|
|
143
|
+
await record(tx, currentAccess, {
|
|
144
|
+
action: derivedEventAction(change.kind),
|
|
145
|
+
tenantId: change.tenantId,
|
|
146
|
+
targetType: 'membership',
|
|
147
|
+
targetId: change.principalId,
|
|
148
|
+
before: change.before
|
|
149
|
+
? { role: change.before.role, viaTenantId: change.before.viaTenantId }
|
|
150
|
+
: undefined,
|
|
151
|
+
after: change.after
|
|
152
|
+
? { role: change.after.role, viaTenantId: change.after.viaTenantId }
|
|
153
|
+
: undefined,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return done(undefined, tags);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import type { StoreBinding } from './binding.js';
|
|
2
|
+
import type { OwnerActivityLookup } from './install-owner.js';
|
|
3
|
+
import type { StorePolicy } from './policy.js';
|
|
4
|
+
import type { DbOrTx, Scope } from './scoped.js';
|
|
5
|
+
import { type Access, type Principal, type Result } from './types.js';
|
|
6
|
+
/**
|
|
7
|
+
* Invitations (D10): a pending offer of membership, its own row rather than
|
|
8
|
+
* a pending membership, so it can expire, be revoked, or sit visibly as
|
|
9
|
+
* "mail failed" until someone retries. Every write here locks the tenant row
|
|
10
|
+
* first, refuses under a break-glass context (D8: no session ever changes
|
|
11
|
+
* who belongs), and records exactly one audit row through the `Access` or
|
|
12
|
+
* `Principal` it was handed.
|
|
13
|
+
*
|
|
14
|
+
* Wave 8a (authz#83) is the sending and management half:
|
|
15
|
+
* `generateInvitationToken`, `invite`, `markInvitationMail`,
|
|
16
|
+
* `resendInvitation`, `revokeInvitation`, `pendingInvitations`,
|
|
17
|
+
* `invitationPreview` and `invitationTenantId`. Wave 8b adds
|
|
18
|
+
* `acceptInvitation` and the born-attach savepoint it runs.
|
|
19
|
+
*
|
|
20
|
+
* `acceptInvitationForUser` does not move: it is the one caller of Boule's
|
|
21
|
+
* own `requireUser`, `ensureProfile` and `principalFromUser` from
|
|
22
|
+
* `access.ts`, none of which this package can reach, and stays in Boule as
|
|
23
|
+
* a non-goal (issue #98 decision 1).
|
|
24
|
+
*/
|
|
25
|
+
export interface InviteOptions {
|
|
26
|
+
readonly email: string;
|
|
27
|
+
readonly role: string;
|
|
28
|
+
/** The host's own per-hour send limit (authz#83 wave 8 decision 2: no longer a package constant). */
|
|
29
|
+
readonly capPerHour: number;
|
|
30
|
+
/** The host's own acceptance route (decision 3): the store never hard-codes one. */
|
|
31
|
+
readonly link: (token: string) => string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* What a successful send hands back. The token is returned here once and
|
|
35
|
+
* never stored or logged in clear. `email` is the row's own, normalised
|
|
36
|
+
* address -- on `resendInvitation` this is the only place a caller who
|
|
37
|
+
* supplied nothing but an id can read it back, so the mail it sends targets
|
|
38
|
+
* the invitation's real address rather than whatever a form happened to
|
|
39
|
+
* carry.
|
|
40
|
+
*/
|
|
41
|
+
export interface InvitationHandle {
|
|
42
|
+
readonly id: string;
|
|
43
|
+
readonly token: string;
|
|
44
|
+
readonly link: string;
|
|
45
|
+
readonly email: string;
|
|
46
|
+
}
|
|
47
|
+
export interface GeneratedInvitationToken {
|
|
48
|
+
readonly token: string;
|
|
49
|
+
readonly tokenHash: string;
|
|
50
|
+
readonly expiresAt: Date;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The token every invitation row uses: 32 random bytes (the package's own
|
|
54
|
+
* `core.invitation.tokenBytes`), hex-encoded for the link the mail carries,
|
|
55
|
+
* with its sha256 hex stored on the row. Exported so a host's own
|
|
56
|
+
* `createTenantAsOperator` builds its own pending invitation with exactly
|
|
57
|
+
* the same rule, rather than a second copy of it.
|
|
58
|
+
*/
|
|
59
|
+
export declare function generateInvitationToken(): GeneratedInvitationToken;
|
|
60
|
+
/**
|
|
61
|
+
* Sends an invitation, or re-sends one: a pending row already sitting on
|
|
62
|
+
* `(tenant, email)` has its token and expiry rotated in place rather than a
|
|
63
|
+
* second row being created, which is also what the unique index on that
|
|
64
|
+
* pair enforces. The role on a rotated row is replaced by whatever this call
|
|
65
|
+
* asked for, since that is the role the grant check below just verified;
|
|
66
|
+
* leaving the old role in place would store something nobody re-checked.
|
|
67
|
+
*
|
|
68
|
+
* There is no check here for "this email already belongs to a member":
|
|
69
|
+
* a membership carries a principal id and a class, never an email (the
|
|
70
|
+
* import rule scans for exactly that), so there is no predicate to write.
|
|
71
|
+
* An address that already has an account simply accepts and finds it is
|
|
72
|
+
* already a member (see `acceptInvitation`, wave 8b).
|
|
73
|
+
*/
|
|
74
|
+
export declare function invite(access: Access, db: DbOrTx, scopeColumn: Scope['column'], options: InviteOptions): Promise<Result<InvitationHandle>>;
|
|
75
|
+
/** `members:grant`; sets whether the mail for one invitation went out. Never audited: this is delivery bookkeeping, not an authorisation event. */
|
|
76
|
+
export declare function markInvitationMail(access: Access, db: DbOrTx, invitationId: string, state: 'sent' | 'failed'): Promise<Result<void>>;
|
|
77
|
+
/**
|
|
78
|
+
* A re-invite: reads the row's current email and role and sends through the
|
|
79
|
+
* same path `invite` uses, which is what rotates its token and expiry.
|
|
80
|
+
* Gated on `members:grant` before the row is even read, the same as
|
|
81
|
+
* `markInvitationMail`: without that, a member who cannot see invitations at
|
|
82
|
+
* all could still learn that a given invitation id exists here, and whether
|
|
83
|
+
* it is still pending, just from which refusal comes back.
|
|
84
|
+
*/
|
|
85
|
+
export declare function resendInvitation(access: Access, db: DbOrTx, scopeColumn: Scope['column'], invitationId: string, options: Pick<InviteOptions, 'capPerHour' | 'link'>): Promise<Result<InvitationHandle>>;
|
|
86
|
+
/**
|
|
87
|
+
* Withdraws a pending invitation. Guarded the same way removing a member is:
|
|
88
|
+
* `members:revoke` and the guard-permission test on the role being taken
|
|
89
|
+
* away, and, for an owner invitation specifically, the same coverage rule
|
|
90
|
+
* a host's own membership removal enforces for a membership row: revoking it
|
|
91
|
+
* must not leave the tenant with no owner and no other pending owner
|
|
92
|
+
* invitation.
|
|
93
|
+
*/
|
|
94
|
+
export declare function revokeInvitation(access: Access, db: DbOrTx, scopeColumn: Scope['column'], invitationId: string): Promise<Result<void>>;
|
|
95
|
+
/** What a born-attached invitation reports when the attachment itself could not be completed. The new owner stands either way; see `attemptBornAttach`. */
|
|
96
|
+
export interface AttachRefusal {
|
|
97
|
+
readonly reason: string;
|
|
98
|
+
readonly message: string;
|
|
99
|
+
}
|
|
100
|
+
export interface AcceptInvitationResult {
|
|
101
|
+
readonly tenantId: string;
|
|
102
|
+
/** Set only on a born-attached invitation: whether `attemptBornAttach` actually completed the attachment. */
|
|
103
|
+
readonly attached?: boolean;
|
|
104
|
+
/** Set only when a born-attached invitation's attachment was refused. The membership this function just wrote stands regardless. */
|
|
105
|
+
readonly attachRefusal?: AttachRefusal;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Accepts an invitation. There is no `Access` yet: the person is not a
|
|
109
|
+
* member until this function decides they may become one, so every check
|
|
110
|
+
* here reads rows directly rather than resolving one. Order follows D10 and
|
|
111
|
+
* D3 exactly: not pending, expired, unverified email, mismatched email,
|
|
112
|
+
* tenant state, then the inviter's authority re-checked as it stands now
|
|
113
|
+
* (not as it stood when they sent the invitation), then the profile, then
|
|
114
|
+
* the membership itself.
|
|
115
|
+
*
|
|
116
|
+
* `binding` and `policy` are threaded through to `roleForInvitation`,
|
|
117
|
+
* `loadAccess`/`operatorMayStillOnboard` and `attemptBornAttach`, the same
|
|
118
|
+
* as every other write this campaign has moved. `profiles` is the host's own
|
|
119
|
+
* port over its `profiles` table's one question this function asks --
|
|
120
|
+
* reusing `Pick<OwnerActivityLookup, 'personKnown'>` from install-owner.ts
|
|
121
|
+
* rather than declaring a second port for the same "has this id ever signed
|
|
122
|
+
* in" question.
|
|
123
|
+
*/
|
|
124
|
+
export declare function acceptInvitation(principal: Principal, db: DbOrTx, scopeColumn: Scope['column'], binding: StoreBinding, policy: Pick<StorePolicy, 'maxDepth' | 'offered'>, profiles: Pick<OwnerActivityLookup, 'personKnown'>, token: string): Promise<Result<AcceptInvitationResult>>;
|
|
125
|
+
export interface PendingInvitationSummary {
|
|
126
|
+
readonly id: string;
|
|
127
|
+
readonly email: string;
|
|
128
|
+
readonly role: string;
|
|
129
|
+
readonly status: string;
|
|
130
|
+
readonly mailState: string;
|
|
131
|
+
readonly invitedBy: string | null;
|
|
132
|
+
readonly expiresAt: Date;
|
|
133
|
+
readonly createdAt: Date;
|
|
134
|
+
/** The organisation this invitation would attach to on acceptance, born-attached only (D19). */
|
|
135
|
+
readonly parentName: string | null;
|
|
136
|
+
}
|
|
137
|
+
/** `members:read`; every pending invitation for the members page, with `token_hash` never among the selected columns. */
|
|
138
|
+
export declare function pendingInvitations(access: Access, db: DbOrTx): Promise<readonly PendingInvitationSummary[]>;
|
|
139
|
+
export interface InvitationPreview {
|
|
140
|
+
readonly tenantName: string;
|
|
141
|
+
readonly role: string;
|
|
142
|
+
readonly status: string;
|
|
143
|
+
readonly expiresAt: Date;
|
|
144
|
+
/** The organisation this invitation would attach to on acceptance, born-attached only (D19). Never the parent's id: this page is reachable before anyone is signed in. */
|
|
145
|
+
readonly parentName: string | null;
|
|
146
|
+
}
|
|
147
|
+
/** For the acceptance page, reachable before anyone is signed in: what the invitation offers, never the hash, the inviter's address, or the parent's id. */
|
|
148
|
+
export declare function invitationPreview(token: string, db: DbOrTx): Promise<InvitationPreview | null>;
|
|
149
|
+
/**
|
|
150
|
+
* The tenant a token's invitation belongs to, regardless of its status --
|
|
151
|
+
* pending, accepted, revoked or expired. Exists for one caller only: the
|
|
152
|
+
* accept page, so that a `not_pending` refusal (the row has already been
|
|
153
|
+
* used) can be told apart from every other refusal by asking a second
|
|
154
|
+
* question -- is this signed-in person already a member there -- without
|
|
155
|
+
* touching `acceptInvitation` itself. Unlike `invitationPreview`, this may
|
|
156
|
+
* only be called once a principal is established: it hands back the raw
|
|
157
|
+
* tenant id, which `invitationPreview`'s own docblock deliberately never
|
|
158
|
+
* does for a visitor who has not signed in yet.
|
|
159
|
+
*/
|
|
160
|
+
export declare function invitationTenantId(token: string, db: DbOrTx): Promise<string | null>;
|