slotlock 0.9.0 → 0.10.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 (61) hide show
  1. package/CHANGELOG.md +305 -0
  2. package/NOTICE +2 -0
  3. package/README.md +4 -2
  4. package/agent-store-backend.js +209 -15
  5. package/application-api.d.ts +10 -1
  6. package/application-api.js +213 -2
  7. package/availability-store.js +13 -5
  8. package/booking-rules.d.ts +11 -0
  9. package/booking-rules.js +21 -1
  10. package/config.d.ts +50 -0
  11. package/config.js +107 -0
  12. package/dashboard-scope.d.ts +7 -0
  13. package/dashboard-scope.js +21 -0
  14. package/dashboard.d.ts +97 -2
  15. package/dashboard.js +3068 -284
  16. package/ddl-identity.d.ts +25 -0
  17. package/ddl-identity.js +881 -0
  18. package/ddl-scim.d.ts +5 -0
  19. package/ddl-scim.js +740 -0
  20. package/ddl-sso.d.ts +15 -0
  21. package/ddl-sso.js +1033 -0
  22. package/ddl.d.ts +40 -9
  23. package/ddl.js +2123 -49
  24. package/domains.d.ts +117 -0
  25. package/domains.js +340 -0
  26. package/event-feed.d.ts +1 -1
  27. package/event-feed.js +6 -34
  28. package/holiday-import.d.ts +1 -1
  29. package/holiday-import.js +2 -3
  30. package/holidays.js +70 -11
  31. package/index.d.ts +12 -4
  32. package/index.js +8 -2
  33. package/invitation-email.d.ts +26 -0
  34. package/invitation-email.js +73 -0
  35. package/maintenance.d.ts +7 -2
  36. package/maintenance.js +15 -1
  37. package/oidc.d.ts +135 -0
  38. package/oidc.js +452 -0
  39. package/openapi.json +1886 -6
  40. package/package.json +2 -1
  41. package/scim-server.d.ts +19 -0
  42. package/scim-server.js +243 -0
  43. package/scim.d.ts +180 -0
  44. package/scim.js +416 -0
  45. package/self-host.d.ts +19 -6
  46. package/self-host.js +262 -7
  47. package/slotlock.manifest.json +1 -1
  48. package/store.d.ts +273 -2
  49. package/store.js +984 -72
  50. package/types.d.ts +33 -1
  51. package/types.js +68 -1
  52. package/workspace-domains.d.ts +146 -0
  53. package/workspace-domains.js +249 -0
  54. package/workspace-roles.d.ts +47 -0
  55. package/workspace-roles.js +304 -0
  56. package/workspace-scim.d.ts +89 -0
  57. package/workspace-scim.js +157 -0
  58. package/workspace-sso.d.ts +131 -0
  59. package/workspace-sso.js +180 -0
  60. package/workspaces.d.ts +240 -0
  61. package/workspaces.js +433 -0
@@ -0,0 +1,131 @@
1
+ import type { SlotlockOidcAlgorithm } from './oidc.js';
2
+ import type { SlotlockSql } from './store.js';
3
+ import { type SlotlockWorkspaceRole } from './workspace-roles.js';
4
+ /** The providers a team may connect: their own rules apply (oidc.ts, dashboard). */
5
+ export type SlotlockSsoKind = 'okta' | 'entra' | 'google_workspace' | 'oidc';
6
+ /** Why a change to a team's single sign-on, or a sign-in through it, was refused. */
7
+ export type SlotlockSsoRefusal = 'not_found' | 'not_allowed' | 'no_verified_domain' | 'secret_required' | 'conflict' | 'stale_config' | 'untested' | 'no_breakglass_owner' | 'role_not_allowed' | 'owner_not_mappable' | 'invalid_value' | 'mapping_limit' | 'connection_inactive' | 'guest_refused' | 'not_member' | 'identity_taken' | 'identity_untrusted' | 'hd_mismatch' | 'member_cap' | 'invalid_max_age' | 'no_active_connection' | 'stale_sso_proof';
8
+ /** A team's connection, as saved. */
9
+ export interface SlotlockSsoConnection {
10
+ id: string;
11
+ kind: SlotlockSsoKind;
12
+ status: 'draft' | 'tested' | 'active' | 'disabled';
13
+ issuer: string;
14
+ clientId: string;
15
+ /** The client secret as sealed (null for Google Workspace, which uses Slotlock's own client). */
16
+ secretSealed: string | null;
17
+ secretLast4: string | null;
18
+ authorizationEndpoint: string;
19
+ tokenEndpoint: string;
20
+ jwksUri: string;
21
+ algorithms: SlotlockOidcAlgorithm[];
22
+ issParameter: boolean;
23
+ entraTenant: string | null;
24
+ groupsClaim: string;
25
+ jitEnabled: boolean;
26
+ defaultRole: 'viewer' | 'member' | 'developer';
27
+ configVersion: number;
28
+ testedVersion: number | null;
29
+ lastTestedAt: Date | null;
30
+ lastTestClaims: Record<string, unknown> | null;
31
+ requireSso: boolean;
32
+ maxAgeHours: number;
33
+ }
34
+ /** A connection as its callback reads it: with its team. */
35
+ export interface SlotlockSsoConnectionForSignIn {
36
+ id: string;
37
+ tenantRef: string;
38
+ handle: string;
39
+ name: string;
40
+ kind: SlotlockSsoKind;
41
+ status: SlotlockSsoConnection['status'];
42
+ issuer: string;
43
+ clientId: string;
44
+ secretSealed: string | null;
45
+ tokenEndpoint: string;
46
+ jwksUri: string;
47
+ algorithms: SlotlockOidcAlgorithm[];
48
+ issParameter: boolean;
49
+ entraTenant: string | null;
50
+ groupsClaim: string;
51
+ configVersion: number;
52
+ }
53
+ /** What a sign-in through a team's provider gave: who, and what happened. */
54
+ export interface SlotlockSsoSignIn {
55
+ person: string | null;
56
+ /** `joined`, `signed_in`, `role_changed` (alone); `linked`, `proof` (signed in already). */
57
+ outcome: 'joined' | 'signed_in' | 'role_changed' | 'linked' | 'proof' | null;
58
+ refusal: SlotlockSsoRefusal | null;
59
+ role: SlotlockWorkspaceRole | null;
60
+ }
61
+ export interface SlotlockWorkspaceSso {
62
+ connection(tenantRef: string): Promise<SlotlockSsoConnection | null>;
63
+ connectionById(id: string): Promise<SlotlockSsoConnectionForSignIn | null>;
64
+ /** A team's active connection, by its handle or a domain it holds verified. */
65
+ route(by: {
66
+ handle?: string;
67
+ domain?: string;
68
+ }): Promise<{
69
+ id: string;
70
+ tenantRef: string;
71
+ handle: string;
72
+ name: string;
73
+ authorizationEndpoint: string;
74
+ clientId: string;
75
+ kind: SlotlockSsoKind;
76
+ entraTenant: string | null;
77
+ } | null>;
78
+ save(params: {
79
+ tenantRef: string;
80
+ actor: string;
81
+ id: string;
82
+ kind: SlotlockSsoKind;
83
+ issuer: string;
84
+ clientId: string;
85
+ secretSealed: string | null;
86
+ secretLast4: string | null;
87
+ authorizationEndpoint: string;
88
+ tokenEndpoint: string;
89
+ jwksUri: string;
90
+ algorithms: readonly SlotlockOidcAlgorithm[];
91
+ issParameter: boolean;
92
+ entraTenant: string | null;
93
+ }): Promise<{
94
+ id: string;
95
+ enforcementOff: boolean;
96
+ } | {
97
+ refusal: SlotlockSsoRefusal;
98
+ id?: string;
99
+ }>;
100
+ recordTest(tenantRef: string, actor: string, id: string, version: number, claims: Record<string, unknown>): Promise<SlotlockSsoRefusal | null>;
101
+ setStatus(tenantRef: string, actor: string, id: string, status: 'active' | 'disabled'): Promise<SlotlockSsoRefusal | null>;
102
+ remove(tenantRef: string, actor: string, id: string): Promise<SlotlockSsoRefusal | null>;
103
+ setProvisioning(tenantRef: string, actor: string, jit: boolean, defaultRole: string, groupsClaim: string): Promise<SlotlockSsoRefusal | null>;
104
+ setMapping(tenantRef: string, actor: string, value: string, role: string | null): Promise<SlotlockSsoRefusal | null>;
105
+ mappings(tenantRef: string): Promise<{
106
+ value: string;
107
+ role: SlotlockWorkspaceRole;
108
+ }[]>;
109
+ signIn(params: {
110
+ connectionId: string;
111
+ issuer: string;
112
+ subject: string;
113
+ entraTid: string | null;
114
+ entraOid: string | null;
115
+ email: string | null;
116
+ emailOk: boolean;
117
+ groups: readonly string[];
118
+ overage: boolean;
119
+ guest: boolean;
120
+ sessionPerson: string | null;
121
+ memberCap: number | null;
122
+ }): Promise<SlotlockSsoSignIn>;
123
+ /** A session's proof from the connection, now; `scoped` for a session begun through it alone. */
124
+ /** `configVersion`: the connection's configuration the sign-in went through, where known. */
125
+ recordProof(sid: string, person: string, connectionId: string, scoped: boolean, configVersion?: number): Promise<boolean>;
126
+ requireSso(tenantRef: string, actor: string, sid: string, on: boolean, maxAgeHours: number): Promise<SlotlockSsoRefusal | null>;
127
+ bypass(tenantRef: string, person: string, sid: string): Promise<SlotlockSsoRefusal | null>;
128
+ prune(): Promise<number>;
129
+ }
130
+ /** Through the functions of ddl-sso.ts. */
131
+ export declare function createSlotlockWorkspaceSso(sql: SlotlockSql): SlotlockWorkspaceSso;
@@ -0,0 +1,180 @@
1
+ // A team's single sign-on as the dashboard reaches it (teams phase 5.3–5.5): through the SECURITY
2
+ // DEFINER functions of ddl-sso.ts, which check the actor's role, lock the team and decide whom a
3
+ // provider's sign-in is. Tokens are checked before (oidc.ts); secrets are sealed before (oidc.ts
4
+ // createSlotlockSecretBox), so only a sealed secret ever reaches the database.
5
+ import { createHash } from 'node:crypto';
6
+ import { isSlotlockWorkspaceRole } from './workspace-roles.js';
7
+ const TEAM = /^team:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
8
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
9
+ const sessionDigest = (sid) => createHash('sha256').update(sid, 'utf8').digest();
10
+ const refusalOf = (value) => value === null || value === undefined ? null : value;
11
+ /** Through the functions of ddl-sso.ts. */
12
+ export function createSlotlockWorkspaceSso(sql) {
13
+ return {
14
+ async connection(tenantRef) {
15
+ if (!TEAM.test(tenantRef))
16
+ return null;
17
+ const [row] = await sql `SELECT * FROM slotlock.sso_connection_of(${tenantRef}::text)`;
18
+ if (!row)
19
+ return null;
20
+ return {
21
+ id: row.connection_id,
22
+ kind: row.kind,
23
+ status: row.status,
24
+ issuer: row.issuer,
25
+ clientId: row.client_id,
26
+ secretSealed: row.secret_sealed,
27
+ secretLast4: row.secret_last4,
28
+ authorizationEndpoint: row.authorization_endpoint,
29
+ tokenEndpoint: row.token_endpoint,
30
+ jwksUri: row.jwks_uri,
31
+ algorithms: row.algorithms,
32
+ issParameter: row.iss_parameter,
33
+ entraTenant: row.entra_tenant,
34
+ groupsClaim: row.groups_claim,
35
+ jitEnabled: row.jit_enabled,
36
+ defaultRole: row.default_role,
37
+ configVersion: Number(row.config_version),
38
+ testedVersion: row.tested_version === null ? null : Number(row.tested_version),
39
+ lastTestedAt: row.last_tested_at,
40
+ lastTestClaims: row.last_test_claims,
41
+ requireSso: row.require_sso,
42
+ maxAgeHours: Number(row.sso_max_age_hours),
43
+ };
44
+ },
45
+ async connectionById(id) {
46
+ if (!UUID.test(id))
47
+ return null;
48
+ const [row] = await sql `SELECT * FROM slotlock.sso_connection_by_id(${id}::uuid)`;
49
+ if (!row)
50
+ return null;
51
+ return {
52
+ id,
53
+ tenantRef: row.team_ref,
54
+ handle: row.handle,
55
+ name: row.name,
56
+ kind: row.kind,
57
+ status: row.status,
58
+ issuer: row.issuer,
59
+ clientId: row.client_id,
60
+ secretSealed: row.secret_sealed,
61
+ tokenEndpoint: row.token_endpoint,
62
+ jwksUri: row.jwks_uri,
63
+ algorithms: row.algorithms,
64
+ issParameter: row.iss_parameter,
65
+ entraTenant: row.entra_tenant,
66
+ groupsClaim: row.groups_claim,
67
+ configVersion: Number(row.config_version),
68
+ };
69
+ },
70
+ async route(by) {
71
+ const handle = by.handle?.trim().toLowerCase() || null;
72
+ const domain = by.domain?.trim().toLowerCase() || null;
73
+ if (handle === null && domain === null)
74
+ return null;
75
+ const [row] = await sql `SELECT * FROM slotlock.sso_connection_route(${handle}::text, ${domain}::text)`;
76
+ return row
77
+ ? {
78
+ id: row.connection_id,
79
+ tenantRef: row.team_ref,
80
+ handle: row.handle,
81
+ name: row.name,
82
+ authorizationEndpoint: row.authorization_endpoint,
83
+ clientId: row.client_id,
84
+ kind: row.kind,
85
+ entraTenant: row.entra_tenant,
86
+ }
87
+ : null;
88
+ },
89
+ async save(params) {
90
+ if (!TEAM.test(params.tenantRef) || !UUID.test(params.id))
91
+ return { refusal: 'not_found' };
92
+ const [row] = await sql `
93
+ SELECT connection_id, refusal, enforcement_off FROM slotlock.save_sso_connection(
94
+ ${params.tenantRef}::text, ${params.actor}::text, ${params.id}::uuid, ${params.kind}::text, ${params.issuer}::text,
95
+ ${params.clientId}::text, ${params.secretSealed}::text, ${params.secretLast4}::text,
96
+ ${params.authorizationEndpoint}::text, ${params.tokenEndpoint}::text, ${params.jwksUri}::text,
97
+ ${[...params.algorithms]}::text[], ${params.issParameter}::boolean, ${params.entraTenant}::text)`;
98
+ if (row?.refusal)
99
+ return { refusal: refusalOf(row.refusal) ?? 'not_allowed', ...(row.connection_id ? { id: row.connection_id } : {}) };
100
+ return row?.connection_id ? { id: row.connection_id, enforcementOff: row.enforcement_off } : { refusal: 'not_allowed' };
101
+ },
102
+ async recordTest(tenantRef, actor, id, version, claims) {
103
+ if (!UUID.test(id))
104
+ return 'not_found';
105
+ const [row] = await sql `
106
+ SELECT slotlock.record_sso_test(${tenantRef}::text, ${actor}::text, ${id}::uuid, ${version}::integer,
107
+ ${JSON.stringify(claims)}::text::jsonb) AS refusal`;
108
+ return refusalOf(row?.refusal);
109
+ },
110
+ async setStatus(tenantRef, actor, id, status) {
111
+ if (!UUID.test(id))
112
+ return 'not_found';
113
+ const [row] = await sql `
114
+ SELECT slotlock.set_sso_connection_status(${tenantRef}::text, ${actor}::text, ${id}::uuid, ${status}::text) AS refusal`;
115
+ return refusalOf(row?.refusal);
116
+ },
117
+ async remove(tenantRef, actor, id) {
118
+ if (!UUID.test(id))
119
+ return 'not_found';
120
+ const [row] = await sql `
121
+ SELECT slotlock.delete_sso_connection(${tenantRef}::text, ${actor}::text, ${id}::uuid) AS refusal`;
122
+ return refusalOf(row?.refusal);
123
+ },
124
+ async setProvisioning(tenantRef, actor, jit, defaultRole, groupsClaim) {
125
+ const [row] = await sql `
126
+ SELECT slotlock.set_sso_provisioning(${tenantRef}::text, ${actor}::text, ${jit}::boolean, ${defaultRole}::text,
127
+ ${groupsClaim}::text) AS refusal`;
128
+ return refusalOf(row?.refusal);
129
+ },
130
+ async setMapping(tenantRef, actor, value, role) {
131
+ const [row] = await sql `
132
+ SELECT slotlock.set_sso_role_mapping(${tenantRef}::text, ${actor}::text, ${value}::text, ${role}::text) AS refusal`;
133
+ return refusalOf(row?.refusal);
134
+ },
135
+ async mappings(tenantRef) {
136
+ if (!TEAM.test(tenantRef))
137
+ return [];
138
+ const rows = await sql `SELECT * FROM slotlock.sso_role_mappings_of(${tenantRef}::text)`;
139
+ return rows.flatMap((row) => (isSlotlockWorkspaceRole(row.role) ? [{ value: row.idp_value, role: row.role }] : []));
140
+ },
141
+ async signIn(params) {
142
+ if (!UUID.test(params.connectionId))
143
+ return { person: null, outcome: null, refusal: 'connection_inactive', role: null };
144
+ const [row] = await sql `
145
+ SELECT person, outcome, refusal, member_role FROM slotlock.sso_sign_in(
146
+ ${params.connectionId}::uuid, ${params.issuer}::text, ${params.subject}::text, ${params.entraTid}::text,
147
+ ${params.entraOid}::text, ${params.email}::text, ${params.emailOk}::boolean, ${[...params.groups].slice(0, 200)}::text[],
148
+ ${params.overage}::boolean, ${params.guest}::boolean, ${params.sessionPerson}::text, ${params.memberCap}::integer)`;
149
+ return {
150
+ person: row?.person ?? null,
151
+ outcome: row?.outcome ?? null,
152
+ refusal: refusalOf(row?.refusal),
153
+ role: isSlotlockWorkspaceRole(row?.member_role) ? row?.member_role : null,
154
+ };
155
+ },
156
+ async recordProof(sid, person, connectionId, scoped, configVersion) {
157
+ if (!UUID.test(connectionId))
158
+ return false;
159
+ const [row] = await sql `
160
+ SELECT slotlock.record_sso_proof(${sessionDigest(sid)}::bytea, ${person}::text, ${connectionId}::uuid, ${scoped}::boolean,
161
+ ${configVersion ?? null}::integer) AS recorded`;
162
+ return row?.recorded === true;
163
+ },
164
+ async requireSso(tenantRef, actor, sid, on, maxAgeHours) {
165
+ const [row] = await sql `
166
+ SELECT slotlock.set_require_sso(${tenantRef}::text, ${actor}::text, ${sessionDigest(sid)}::bytea, ${on}::boolean,
167
+ ${maxAgeHours}::integer) AS refusal`;
168
+ return refusalOf(row?.refusal);
169
+ },
170
+ async bypass(tenantRef, person, sid) {
171
+ const [row] = await sql `
172
+ SELECT slotlock.sso_owner_bypass(${tenantRef}::text, ${person}::text, ${sessionDigest(sid)}::bytea) AS refusal`;
173
+ return refusalOf(row?.refusal);
174
+ },
175
+ async prune() {
176
+ const [row] = await sql `SELECT slotlock.prune_sso() AS pruned`;
177
+ return Number(row?.pruned ?? 0);
178
+ },
179
+ };
180
+ }
@@ -0,0 +1,240 @@
1
+ import type { SlotlockSql } from './store.js';
2
+ import { type SlotlockWorkspaceDomains } from './workspace-domains.js';
3
+ import { type SlotlockWorkspaceSso } from './workspace-sso.js';
4
+ import { type SlotlockWorkspaceScim } from './workspace-scim.js';
5
+ import { type SlotlockWorkspaceRole } from './workspace-roles.js';
6
+ import { type SlotlockResourceScope } from './resource-profile.js';
7
+ /** A GitHub organization the person is an active owner (`admin`) or member of, as GitHub lists it. */
8
+ export interface SlotlockGitHubMembership {
9
+ id: string;
10
+ role: 'admin' | 'member';
11
+ }
12
+ /** A person's standing in one workspace now. */
13
+ export interface SlotlockWorkspaceStanding {
14
+ /** Their role, or null when they do not belong to it (any longer). */
15
+ role: SlotlockWorkspaceRole | null;
16
+ /**
17
+ * The epoch their sessions must carry, or null when the person has no record (never signed in
18
+ * with this store, or erased): then no session passes. "Sign out everywhere" draws a new one.
19
+ */
20
+ sessionEpoch: number | null;
21
+ /**
22
+ * Why there is no role though the person is a member: the team requires single sign-on and this
23
+ * session holds no recent proof (`sso_required`), or the session was signed in through another
24
+ * team's provider alone (`sso_scope`). Null otherwise.
25
+ */
26
+ refusal?: 'sso_required' | 'sso_scope' | null;
27
+ /** With sso_required: whether this person (an Owner who signs in with GitHub or Google) may bypass. */
28
+ mayBypass?: boolean;
29
+ }
30
+ /** What the dashboard reads and records of people and their memberships. */
31
+ export interface SlotlockDashboardWorkspaces {
32
+ /**
33
+ * Record a sign-in, returning the epoch to seal into the session. Signing in with GitHub, pass
34
+ * the organizations GitHub lists the person in: they become memberships (owners Owner, members
35
+ * Developer), and one GitHub no longer lists ends.
36
+ */
37
+ signedIn(person: string, githubOrganizations?: readonly SlotlockGitHubMembership[]): Promise<number>;
38
+ /**
39
+ * The person's role in a tenant (its test environment is its live tenant's) and their epoch, for
40
+ * this session (its id, as the cookie holds it, or null where there is none): a team that requires
41
+ * single sign-on gives a session without a recent proof no role, and a session scoped to a team's
42
+ * provider has none elsewhere. Required, so no caller with a session leaves its scope unchecked.
43
+ */
44
+ standing(tenantRef: string, person: string, sid: string | null): Promise<SlotlockWorkspaceStanding>;
45
+ /**
46
+ * The locations and groups the person is limited to in the tenant (RFC 0004 D1), or null for all
47
+ * of it. An Owner is never limited.
48
+ */
49
+ scope?(tenantRef: string, person: string): Promise<SlotlockResourceScope | null>;
50
+ /** End every session of the person: a new epoch is drawn, and those sealed under the old are refused. */
51
+ signOutEverywhere(person: string): Promise<number>;
52
+ /** Team workspaces, their members and invitations: only with the database (createSlotlockDashboardWorkspaces). */
53
+ teams?: SlotlockWorkspaceTeams;
54
+ /** Each session a person signed in with, to list and sign out one by one: only with the database. */
55
+ sessions?: SlotlockDashboardSessions;
56
+ /** Teams' verified domains and requests to join through them (workspace-domains.ts): only with the database. */
57
+ domains?: SlotlockWorkspaceDomains;
58
+ /** Teams' single sign-on (workspace-sso.ts): only with the database. */
59
+ sso?: SlotlockWorkspaceSso;
60
+ /** Teams' SCIM provisioning (workspace-scim.ts): only with the database. */
61
+ scim?: SlotlockWorkspaceScim;
62
+ }
63
+ /** A session a person signed in with, as their Account page lists it. */
64
+ export interface SlotlockDashboardSession {
65
+ /** The SHA-256 of the session's id, in hex: names it without revealing it. */
66
+ id: string;
67
+ startedAt: Date;
68
+ lastSeenAt: Date;
69
+ expiresAt: Date;
70
+ userAgent: string | null;
71
+ ipAddress: string | null;
72
+ }
73
+ /** Sessions by person (slotlock.dashboard_sessions): only a digest of each session's id is kept. */
74
+ export interface SlotlockDashboardSessions {
75
+ started(person: string, sid: string, expiresAt: Date, from: {
76
+ userAgent?: string;
77
+ ipAddress?: string;
78
+ }): Promise<void>;
79
+ /** Whether the session may go on: false once signed out from another device. Notes it seen. */
80
+ seen(person: string, sid: string): Promise<boolean>;
81
+ list(person: string): Promise<SlotlockDashboardSession[]>;
82
+ /** Sign out one session by its id (SlotlockDashboardSession.id), or every one with null. */
83
+ end(person: string, id: string | null): Promise<boolean>;
84
+ /** The id a session is listed under. */
85
+ idOf(sid: string): string;
86
+ }
87
+ /** A team workspace (team:<uuid>) as a person sees it in their list. */
88
+ export interface SlotlockTeamWorkspace {
89
+ tenantRef: string;
90
+ handle: string;
91
+ name: string;
92
+ role: SlotlockWorkspaceRole;
93
+ }
94
+ /** An active member of a workspace. */
95
+ export interface SlotlockWorkspaceMember {
96
+ person: string;
97
+ role: SlotlockWorkspaceRole;
98
+ source: 'github_org' | 'invitation' | 'created';
99
+ /** Their GitHub login or verified Google email as they last signed in; null before they have. */
100
+ display: string | null;
101
+ joinedAt: Date;
102
+ }
103
+ /** An invitation not accepted or revoked: pending, or expired in the last 30 days. */
104
+ export interface SlotlockWorkspaceInvitation {
105
+ id: string;
106
+ email: string | null;
107
+ githubLogin: string | null;
108
+ role: SlotlockWorkspaceRole;
109
+ inviterDisplay: string;
110
+ sentAt: Date;
111
+ expiresAt: Date;
112
+ }
113
+ /** What the holder of an invitation's link sees before joining. */
114
+ export interface SlotlockInvitationPreview {
115
+ id: string;
116
+ tenantRef: string;
117
+ workspaceName: string;
118
+ workspaceHandle: string;
119
+ inviterDisplay: string;
120
+ role: SlotlockWorkspaceRole;
121
+ email: string | null;
122
+ githubRef: string | null;
123
+ githubLogin: string | null;
124
+ state: 'pending' | 'accepted' | 'revoked' | 'expired';
125
+ }
126
+ /** Why a team change was refused. */
127
+ export type SlotlockTeamRefusal = 'invalid_handle' | 'invalid_name' | 'handle_taken' | 'team_limit' | 'not_found' | 'not_allowed' | 'already_member' | 'invitation_limit'
128
+ /** The address was sent SLOTLOCK_INVITATIONS_PER_ADDRESS invitations in the last day. */
129
+ | 'recipient_limit' | 'member_cap' | 'revoked' | 'accepted' | 'expired' | 'not_for_you' | 'last_owner' | 'sso_owner' | 'confirm' | 'subscription_live' | 'domain_restricted'
130
+ /** An Owner reaches the whole workspace: never limited. */
131
+ | 'owner';
132
+ /**
133
+ * Team workspaces through their SECURITY DEFINER functions: each checks the actor's role and
134
+ * ceiling, keeps an Owner, and serialises changes to one workspace's people.
135
+ */
136
+ export interface SlotlockWorkspaceTeams {
137
+ /** How the person is shown to others: their GitHub login or verified Google email. */
138
+ setDisplay(person: string, display: string | null): Promise<void>;
139
+ /** A new team workspace with the person its Owner; `cap` free team workspaces a person may create. */
140
+ create(person: string, name: string, handle: string, cap: number | null): Promise<{
141
+ tenantRef: string;
142
+ } | {
143
+ refusal: SlotlockTeamRefusal;
144
+ }>;
145
+ info(tenantRef: string): Promise<{
146
+ handle: string;
147
+ name: string;
148
+ } | null>;
149
+ /** The team workspaces the person is an active member of. */
150
+ workspacesOf(person: string): Promise<SlotlockTeamWorkspace[]>;
151
+ members(tenantRef: string): Promise<SlotlockWorkspaceMember[]>;
152
+ invitations(tenantRef: string): Promise<SlotlockWorkspaceInvitation[]>;
153
+ invite(params: {
154
+ tenantRef: string;
155
+ inviter: string;
156
+ to: {
157
+ email: string;
158
+ } | {
159
+ github: string;
160
+ login: string;
161
+ };
162
+ role: SlotlockWorkspaceRole;
163
+ /** SHA-256 of the token; the token itself is never given. */
164
+ tokenSha256: Buffer;
165
+ days: number;
166
+ dailyLimit: number | null;
167
+ memberCap: number | null;
168
+ }): Promise<{
169
+ id: string;
170
+ } | {
171
+ refusal: SlotlockTeamRefusal;
172
+ }>;
173
+ preview(tokenSha256: Buffer): Promise<SlotlockInvitationPreview | null>;
174
+ /** Accept by the token's digest or, one found for the person, by id. `emails` are those the person holds verified. */
175
+ accept(params: {
176
+ tokenSha256?: Buffer;
177
+ id?: string;
178
+ person: string;
179
+ emails: readonly string[];
180
+ memberCap: number | null;
181
+ }): Promise<{
182
+ tenantRef: string;
183
+ } | {
184
+ refusal: SlotlockTeamRefusal;
185
+ }>;
186
+ revoke(tenantRef: string, actor: string, id: string): Promise<boolean>;
187
+ /** Send again with a new token (the old stops working), held to the limits a new one is. */
188
+ resend(tenantRef: string, actor: string, id: string, tokenSha256: Buffer, days: number, dailyLimit: number | null, memberCap: number | null): Promise<SlotlockTeamRefusal | null>;
189
+ changeRole(tenantRef: string, actor: string, person: string, role: SlotlockWorkspaceRole): Promise<SlotlockTeamRefusal | null>;
190
+ /**
191
+ * Limit a member to locations and groups, or give them the whole workspace again (null): by an
192
+ * Owner or Admin not limited themselves, of a member not an Owner (`owner`) whose role they may
193
+ * grant.
194
+ */
195
+ setScope(tenantRef: string, actor: string, person: string, scope: SlotlockResourceScope | null): Promise<SlotlockTeamRefusal | null>;
196
+ /** Each member limited to part of the workspace, by person, with what they are limited to. */
197
+ scopes(tenantRef: string): Promise<ReadonlyMap<string, SlotlockResourceScope>>;
198
+ /** Remove a member; the actor removing themselves leaves. */
199
+ remove(tenantRef: string, actor: string, person: string): Promise<SlotlockTeamRefusal | null>;
200
+ /** Pending invitations meant for the person (their GitHub account, or an email they hold verified). */
201
+ pendingFor(person: string, emails: readonly string[]): Promise<{
202
+ id: string;
203
+ tenantRef: string;
204
+ workspaceName: string;
205
+ workspaceHandle: string;
206
+ inviterDisplay: string;
207
+ role: SlotlockWorkspaceRole;
208
+ }[]>;
209
+ /** Whether a team workspace exists and serves its keys. */
210
+ isActive(tenantRef: string): Promise<boolean>;
211
+ /** Delete invitations finished more than 30 days ago; how many went. */
212
+ prune(): Promise<number>;
213
+ /** Delete the team in seven days, by an Owner who typed its handle; its keys stop at once. */
214
+ scheduleDeletion(tenantRef: string, actor: string, handle: string): Promise<SlotlockTeamRefusal | null>;
215
+ /** Keep a team being deleted, by any of its Owners. */
216
+ cancelDeletion(tenantRef: string, actor: string): Promise<SlotlockTeamRefusal | null>;
217
+ /** When a team being deleted goes, or null. */
218
+ deleteAfter(tenantRef: string): Promise<Date | null>;
219
+ }
220
+ /** Through slotlock.dashboard_sign_in, dashboard_standing and dashboard_sign_out_everywhere. */
221
+ export declare function createSlotlockDashboardWorkspaces(sql: SlotlockSql): SlotlockDashboardWorkspaces;
222
+ /**
223
+ * Whether a team workspace serves its keys, for `serve`: read through slotlock.workspace_active and
224
+ * kept for `ttlMs` (30 seconds), so deleting one stops its keys on every server within that.
225
+ */
226
+ export declare function createSlotlockWorkspaceCheck(sql: SlotlockSql, options?: {
227
+ ttlMs?: number;
228
+ now?: () => number;
229
+ maxEntries?: number;
230
+ }): (tenantRef: string) => Promise<boolean>;
231
+ /**
232
+ * The same in memory, for tests and for a library deployment with a single server. `setRole` and
233
+ * `remove` stand in for what the Members page will do.
234
+ */
235
+ export declare function createSlotlockMemoryDashboardWorkspaces(): SlotlockDashboardWorkspaces & {
236
+ setRole(tenantRef: string, person: string, role: SlotlockWorkspaceRole, source?: 'github_org' | 'invitation' | 'created'): void;
237
+ remove(tenantRef: string, person: string): void;
238
+ /** Limit a member to locations and groups, or (null) give them all of the workspace again. */
239
+ limit(tenantRef: string, person: string, scope: SlotlockResourceScope | null): void;
240
+ };