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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slotlock",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "type": "module",
5
5
  "description": "Resource-first, deterministic calendar and availability engine for AI agents.",
6
6
  "license": "Apache-2.0",
@@ -59,6 +59,7 @@
59
59
  "dependencies": {
60
60
  "ical.js": "2.2.1",
61
61
  "rrule": "2.8.1",
62
+ "tldts": "7.4.19",
62
63
  "zod": "4.6.7"
63
64
  },
64
65
  "peerDependencies": {
@@ -0,0 +1,19 @@
1
+ import type { SlotlockAuditEventInput } from './store.js';
2
+ import type { SlotlockWorkspaceScim } from './workspace-scim.js';
3
+ /** What the endpoint needs from its host. */
4
+ export interface SlotlockScimServerOptions {
5
+ scim: SlotlockWorkspaceScim;
6
+ /** Where `scim/v2` lives: the dashboard's public root. */
7
+ publicRoot: string;
8
+ documentationUri: string;
9
+ /** Records an event in a team's audit log. */
10
+ record?: (tenantRef: string, event: SlotlockAuditEventInput) => Promise<void>;
11
+ /** A person's membership ended by the directory: their keys go where the team chose. */
12
+ deactivated?: (tenantRef: string, person: string, keyPolicy: 'keep' | 'revoke') => Promise<number>;
13
+ newId?: () => string;
14
+ }
15
+ /**
16
+ * One SCIM request. `path` is what follows `scim/v2/`: `<team id>/<resource>[/<id>]`. Answers 401
17
+ * without a token for the team the address names.
18
+ */
19
+ export declare function handleSlotlockScimRequest(request: Request, path: string, options: SlotlockScimServerOptions): Promise<Response>;
package/scim-server.js ADDED
@@ -0,0 +1,243 @@
1
+ // A team's SCIM 2.0 endpoint (teams phase 5.6–5.7): `<root>/scim/v2/<team id>/…`, on the dashboard's
2
+ // Worker and on the Node server alike. The token decides the team, and must be the one the address
3
+ // names; session cookies never work here, nor SCIM tokens on the dashboard. Requests are parsed and
4
+ // normalised by scim.ts, kept through workspace-scim.ts, and each write is one event in the team's
5
+ // audit log, as its token's (never a body: Okta sends a placeholder password).
6
+ import { randomUUID } from 'node:crypto';
7
+ import { SLOTLOCK_SCIM_CONTENT_TYPE, SlotlockScimError, slotlockScimErrorBody, slotlockScimFilter, slotlockScimGroupFromBody, slotlockScimGroupResource, slotlockScimList, slotlockScimPage, slotlockScimPatchGroup, slotlockScimPatchOperations, slotlockScimPatchUser, slotlockScimResourceTypes, slotlockScimSchemas, slotlockScimServiceProviderConfig, slotlockScimUserFromBody, slotlockScimUserResource, } from './scim.js';
8
+ const USER_FILTERS = ['username', 'externalid', 'id', 'emails[type eq "work"].value', 'emails.value'];
9
+ const GROUP_FILTERS = ['displayname', 'externalid', 'id'];
10
+ const MAX_BODY_BYTES = 262_144;
11
+ const json = (status, body, headers = {}) => new Response(status === 204 ? null : JSON.stringify(body), {
12
+ status,
13
+ headers: { 'content-type': `${SLOTLOCK_SCIM_CONTENT_TYPE}; charset=utf-8`, 'cache-control': 'no-store', ...headers },
14
+ });
15
+ const failure = (status, scimType, detail) => json(status, slotlockScimErrorBody(status, scimType, detail));
16
+ /** How a refusal from the database answers. */
17
+ function refused(refusal) {
18
+ switch (refusal) {
19
+ case 'uniqueness':
20
+ return failure(409, 'uniqueness', 'A resource with that name exists');
21
+ case 'domain_not_verified':
22
+ return failure(400, 'invalidValue', 'The address is not at a domain this team has verified');
23
+ case 'owner_protected':
24
+ return failure(400, 'mutability', 'Owners are managed in Slotlock, not by the directory');
25
+ case 'invalid_member':
26
+ return failure(400, 'invalidValue', 'A member is not one of this team’s users');
27
+ case 'not_found':
28
+ return failure(404, null, 'Not found');
29
+ default:
30
+ return failure(400, 'invalidValue', refusal);
31
+ }
32
+ }
33
+ /** The lookup a filter asks for, as workspace-scim.ts takes it. */
34
+ function lookupOf(query, allowed) {
35
+ const filter = query.get('filter');
36
+ if (filter === null || filter.trim() === '')
37
+ return null;
38
+ const [first, ...rest] = slotlockScimFilter(filter, allowed);
39
+ if (!first || rest.length > 0)
40
+ throw new SlotlockScimError(400, 'invalidFilter', 'Slotlock filters by one attribute');
41
+ const attribute = first.attribute.startsWith('emails') ? 'email' : first.attribute;
42
+ return { attribute, value: first.value };
43
+ }
44
+ async function bodyOf(request) {
45
+ const type = (request.headers.get('content-type') ?? '').split(';')[0]?.trim().toLowerCase();
46
+ if (type !== SLOTLOCK_SCIM_CONTENT_TYPE && type !== 'application/json')
47
+ throw new SlotlockScimError(415, null, 'Send application/scim+json');
48
+ // Counted in bytes as it arrives, never read past the cap: a token's holder cannot make the server
49
+ // buffer an endless body.
50
+ const tooLarge = () => new SlotlockScimError(413, null, 'The request is too large');
51
+ const declared = Number(request.headers.get('content-length'));
52
+ if (Number.isFinite(declared) && declared > MAX_BODY_BYTES) {
53
+ await request.body?.cancel().catch(() => undefined);
54
+ throw tooLarge();
55
+ }
56
+ const chunks = [];
57
+ let size = 0;
58
+ const reader = request.body?.getReader();
59
+ if (reader) {
60
+ for (;;) {
61
+ const { done, value } = await reader.read();
62
+ if (done)
63
+ break;
64
+ size += value.byteLength;
65
+ if (size > MAX_BODY_BYTES) {
66
+ await reader.cancel().catch(() => undefined);
67
+ throw tooLarge();
68
+ }
69
+ chunks.push(value);
70
+ }
71
+ }
72
+ const bytes = new Uint8Array(size);
73
+ let offset = 0;
74
+ for (const chunk of chunks) {
75
+ bytes.set(chunk, offset);
76
+ offset += chunk.byteLength;
77
+ }
78
+ let text;
79
+ try {
80
+ text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
81
+ }
82
+ catch {
83
+ throw new SlotlockScimError(400, 'invalidSyntax', 'The body is not UTF-8');
84
+ }
85
+ try {
86
+ return JSON.parse(text);
87
+ }
88
+ catch {
89
+ throw new SlotlockScimError(400, 'invalidSyntax', 'The body is not JSON');
90
+ }
91
+ }
92
+ /**
93
+ * One SCIM request. `path` is what follows `scim/v2/`: `<team id>/<resource>[/<id>]`. Answers 401
94
+ * without a token for the team the address names.
95
+ */
96
+ export async function handleSlotlockScimRequest(request, path, options) {
97
+ const [teamId = '', resource = '', id, ...extra] = path.split('/');
98
+ const authorization = request.headers.get('authorization') ?? '';
99
+ const token = /^Bearer\s+(\S+)$/i.exec(authorization)?.[1] ?? '';
100
+ const authenticated = await options.scim.authenticate(token, teamId);
101
+ if ('refusal' in authenticated) {
102
+ // Another team's token at this address: a security event, in the token's own team's log.
103
+ if (authenticated.refusal === 'wrong_workspace' && authenticated.tenantRef && options.record) {
104
+ await options.record(authenticated.tenantRef, {
105
+ action: 'scim.token_refused',
106
+ actorType: 'system',
107
+ actorRef: `scim_token:${authenticated.prefix ?? 'unknown'}`,
108
+ outcome: 'denied',
109
+ reason: 'wrong_workspace',
110
+ }).catch(() => undefined);
111
+ }
112
+ return json(401, slotlockScimErrorBody(401, null, 'A SCIM token for this team is needed'), { 'www-authenticate': 'Bearer' });
113
+ }
114
+ const { tenantRef, prefix, keyPolicy } = authenticated;
115
+ const base = `${options.publicRoot}/scim/v2/${teamId.toLowerCase()}`;
116
+ const actor = { actorType: 'system', actorRef: `scim_token:${prefix}` };
117
+ const record = async (event) => {
118
+ await options.record?.(tenantRef, { ...event, ...actor }).catch(() => undefined);
119
+ };
120
+ const newId = options.newId ?? randomUUID;
121
+ const url = new URL(request.url);
122
+ const method = request.method.toUpperCase();
123
+ if (extra.length > 0)
124
+ return failure(404, null, 'Not found');
125
+ try {
126
+ if (resource === 'ServiceProviderConfig' && method === 'GET')
127
+ return json(200, slotlockScimServiceProviderConfig(options.documentationUri));
128
+ if (resource === 'ResourceTypes' && method === 'GET')
129
+ return json(200, slotlockScimResourceTypes(base));
130
+ if (resource === 'Schemas' && method === 'GET')
131
+ return json(200, slotlockScimSchemas(base));
132
+ if (resource === 'Users') {
133
+ const meta = (user) => ({ created: user.createdAt, lastModified: user.updatedAt, location: `${base}/Users/${user.id}` });
134
+ const one = async (userId) => (await options.scim.users(tenantRef, null, 1, 1, userId)).users[0];
135
+ if (id === undefined && method === 'GET') {
136
+ const { startIndex, count } = slotlockScimPage(url.searchParams);
137
+ const found = await options.scim.users(tenantRef, lookupOf(url.searchParams, USER_FILTERS), startIndex, count);
138
+ return json(200, slotlockScimList(found.users.map((user) => slotlockScimUserResource(user.id, user, meta(user))), found.total, startIndex));
139
+ }
140
+ if (id === undefined && method === 'POST') {
141
+ const user = slotlockScimUserFromBody(await bodyOf(request));
142
+ const saved = await options.scim.putUser(tenantRef, null, newId(), user);
143
+ if ('refusal' in saved)
144
+ return refused(saved.refusal);
145
+ await record({ action: 'scim.user_created', targetType: 'scim_user', targetRef: saved.id });
146
+ const made = await one(saved.id);
147
+ return made ? json(201, slotlockScimUserResource(made.id, made, meta(made)), { location: `${base}/Users/${made.id}` }) : failure(404, null, 'Not found');
148
+ }
149
+ if (id === undefined)
150
+ return failure(405, null, 'Not allowed');
151
+ const current = await one(id);
152
+ if (!current)
153
+ return failure(404, null, 'Not found');
154
+ if (method === 'GET')
155
+ return json(200, slotlockScimUserResource(current.id, current, meta(current)));
156
+ if (method === 'DELETE') {
157
+ const deleted = await options.scim.deleteUser(tenantRef, id);
158
+ if (deleted.refusal)
159
+ return refused(deleted.refusal);
160
+ await record({ action: 'scim.user_deleted', targetType: 'scim_user', targetRef: id });
161
+ if (deleted.person)
162
+ await options.deactivated?.(tenantRef, deleted.person, keyPolicy);
163
+ return json(204, null);
164
+ }
165
+ if (method === 'PUT' || method === 'PATCH') {
166
+ const user = method === 'PUT'
167
+ ? slotlockScimUserFromBody(await bodyOf(request))
168
+ : slotlockScimPatchUser(current, current.id, slotlockScimPatchOperations(await bodyOf(request)));
169
+ const saved = await options.scim.putUser(tenantRef, id, newId(), user);
170
+ if ('refusal' in saved) {
171
+ if (saved.refusal === 'owner_protected') {
172
+ await record({ action: 'scim.user_refused', targetType: 'scim_user', targetRef: id, outcome: 'denied', reason: 'owner_protected' });
173
+ }
174
+ return refused(saved.refusal);
175
+ }
176
+ if (saved.membership === 'deactivated' && saved.person) {
177
+ const revoked = (await options.deactivated?.(tenantRef, saved.person, keyPolicy)) ?? 0;
178
+ await record({ action: 'scim.user_deactivated', targetType: 'person', targetRef: saved.person, metadata: { keys: keyPolicy, revoked } });
179
+ }
180
+ else if (saved.membership === 'activated' && saved.person) {
181
+ await record({ action: 'scim.user_reactivated', targetType: 'person', targetRef: saved.person });
182
+ }
183
+ else {
184
+ await record({ action: 'scim.user_updated', targetType: 'scim_user', targetRef: id });
185
+ }
186
+ const updated = await one(id);
187
+ return updated ? json(200, slotlockScimUserResource(updated.id, updated, meta(updated))) : failure(404, null, 'Not found');
188
+ }
189
+ return failure(405, null, 'Not allowed');
190
+ }
191
+ if (resource === 'Groups') {
192
+ const meta = (group) => ({ created: group.createdAt, lastModified: group.updatedAt, location: `${base}/Groups/${group.id}` });
193
+ const withMembers = !/\bmembers\b/i.test(url.searchParams.get('excludedAttributes') ?? '');
194
+ const one = async (groupId) => (await options.scim.groups(tenantRef, null, 1, 1, groupId)).groups[0];
195
+ if (id === undefined && method === 'GET') {
196
+ const { startIndex, count } = slotlockScimPage(url.searchParams);
197
+ const found = await options.scim.groups(tenantRef, lookupOf(url.searchParams, GROUP_FILTERS), startIndex, count);
198
+ return json(200, slotlockScimList(found.groups.map((group) => slotlockScimGroupResource(group.id, group, meta(group), withMembers)), found.total, startIndex));
199
+ }
200
+ if (id === undefined && method === 'POST') {
201
+ const group = slotlockScimGroupFromBody(await bodyOf(request));
202
+ const saved = await options.scim.putGroup(tenantRef, null, newId(), group);
203
+ if ('refusal' in saved)
204
+ return refused(saved.refusal);
205
+ await record({ action: 'scim.group_created', targetType: 'scim_group', targetRef: saved.id, metadata: { members: group.members.length } });
206
+ const made = await one(saved.id);
207
+ return made ? json(201, slotlockScimGroupResource(made.id, made, meta(made)), { location: `${base}/Groups/${made.id}` }) : failure(404, null, 'Not found');
208
+ }
209
+ if (id === undefined)
210
+ return failure(405, null, 'Not allowed');
211
+ const current = await one(id);
212
+ if (!current)
213
+ return failure(404, null, 'Not found');
214
+ if (method === 'GET')
215
+ return json(200, slotlockScimGroupResource(current.id, current, meta(current), withMembers));
216
+ if (method === 'DELETE') {
217
+ const refusal = await options.scim.deleteGroup(tenantRef, id);
218
+ if (refusal)
219
+ return refused(refusal);
220
+ await record({ action: 'scim.group_deleted', targetType: 'scim_group', targetRef: id });
221
+ return json(204, null);
222
+ }
223
+ if (method === 'PUT' || method === 'PATCH') {
224
+ const group = method === 'PUT'
225
+ ? slotlockScimGroupFromBody(await bodyOf(request))
226
+ : slotlockScimPatchGroup(current, current.id, slotlockScimPatchOperations(await bodyOf(request)));
227
+ const saved = await options.scim.putGroup(tenantRef, id, newId(), group);
228
+ if ('refusal' in saved)
229
+ return refused(saved.refusal);
230
+ await record({ action: 'scim.group_updated', targetType: 'scim_group', targetRef: id, metadata: { members: group.members.length } });
231
+ const updated = await one(id);
232
+ return updated ? json(200, slotlockScimGroupResource(updated.id, updated, meta(updated), withMembers)) : failure(404, null, 'Not found');
233
+ }
234
+ return failure(405, null, 'Not allowed');
235
+ }
236
+ return failure(404, null, 'Not found');
237
+ }
238
+ catch (error) {
239
+ if (error instanceof SlotlockScimError)
240
+ return failure(error.status, error.scimType, error.message);
241
+ throw error;
242
+ }
243
+ }
package/scim.d.ts ADDED
@@ -0,0 +1,180 @@
1
+ export declare const SLOTLOCK_SCIM_CONTENT_TYPE = "application/scim+json";
2
+ export declare const SCIM_USER_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:User";
3
+ export declare const SCIM_GROUP_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:Group";
4
+ export declare const SCIM_PATCH_SCHEMA = "urn:ietf:params:scim:api:messages:2.0:PatchOp";
5
+ export declare const SCIM_LIST_SCHEMA = "urn:ietf:params:scim:api:messages:2.0:ListResponse";
6
+ export declare const SCIM_ERROR_SCHEMA = "urn:ietf:params:scim:api:messages:2.0:Error";
7
+ /** A page holds at most this many resources (ServiceProviderConfig's filter.maxResults). */
8
+ export declare const SLOTLOCK_SCIM_MAX_RESULTS = 100;
9
+ /** RFC 7644 §3.12's error types Slotlock answers with. */
10
+ export type SlotlockScimErrorType = 'invalidFilter' | 'uniqueness' | 'mutability' | 'invalidSyntax' | 'invalidPath' | 'noTarget' | 'invalidValue';
11
+ /** A request refused: its status and SCIM error type. */
12
+ export declare class SlotlockScimError extends Error {
13
+ readonly status: number;
14
+ readonly scimType: SlotlockScimErrorType | null;
15
+ constructor(status: number, scimType: SlotlockScimErrorType | null, message: string);
16
+ }
17
+ /** An error body: `status` as a string, as RFC 7644 §3.12 has it. */
18
+ export declare function slotlockScimErrorBody(status: number, scimType: SlotlockScimErrorType | null, detail: string): {
19
+ schemas: string[];
20
+ status: string;
21
+ scimType?: SlotlockScimErrorType;
22
+ detail: string;
23
+ };
24
+ /** The user attributes Slotlock keeps. */
25
+ export interface SlotlockScimUserAttributes {
26
+ /** As the provider sent it; matched case-insensitively. */
27
+ userName: string;
28
+ externalId: string | null;
29
+ active: boolean;
30
+ givenName: string | null;
31
+ familyName: string | null;
32
+ displayName: string | null;
33
+ /** The work email: `emails[type eq "work"]`, else the primary one, else the first. */
34
+ workEmail: string | null;
35
+ }
36
+ /** A group's attributes, its members by their SCIM ids. */
37
+ export interface SlotlockScimGroupAttributes {
38
+ displayName: string;
39
+ externalId: string | null;
40
+ members: string[];
41
+ }
42
+ /** A SCIM boolean: true or false, or the strings Entra sends ("True", "False"). */
43
+ export declare function slotlockScimBoolean(value: unknown): boolean;
44
+ /** A user as a POST or PUT body gives it (RFC 7643 §4.1): what Slotlock keeps, the rest ignored. */
45
+ export declare function slotlockScimUserFromBody(body: unknown): SlotlockScimUserAttributes;
46
+ /** A group as a POST or PUT body gives it. */
47
+ export declare function slotlockScimGroupFromBody(body: unknown): SlotlockScimGroupAttributes;
48
+ /** One patch operation, as normalised: its op in lowercase, its path (null: the resource), its value. */
49
+ export interface SlotlockScimPatchOperation {
50
+ op: 'add' | 'replace' | 'remove';
51
+ path: string | null;
52
+ value: unknown;
53
+ }
54
+ /** A PATCH body's operations, normalised (see the top of this file). */
55
+ export declare function slotlockScimPatchOperations(body: unknown): SlotlockScimPatchOperation[];
56
+ /** A user changed by a patch, all of it or none (RFC 7644 §3.5.2: a patch is atomic). */
57
+ export declare function slotlockScimPatchUser(current: SlotlockScimUserAttributes, id: string, operations: readonly SlotlockScimPatchOperation[]): SlotlockScimUserAttributes;
58
+ /** A group changed by a patch, all of it or none. */
59
+ export declare function slotlockScimPatchGroup(current: SlotlockScimGroupAttributes, id: string, operations: readonly SlotlockScimPatchOperation[]): SlotlockScimGroupAttributes;
60
+ /** A filter Slotlock answers: `attribute eq "value"`, joined by `and`; the attributes its caller allows. */
61
+ export interface SlotlockScimFilter {
62
+ /** In lowercase, as the caller's allowed list names it. */
63
+ attribute: string;
64
+ value: string;
65
+ }
66
+ /**
67
+ * A filter (RFC 7644 §3.4.2.2) as Okta and Entra send it: `eq` comparisons of the allowed
68
+ * attributes, joined by `and`. Anything else is 400 invalidFilter.
69
+ */
70
+ export declare function slotlockScimFilter(filter: string, allowed: readonly string[]): SlotlockScimFilter[];
71
+ /** Where a resource lives, for its meta. */
72
+ export interface SlotlockScimMeta {
73
+ created: Date;
74
+ lastModified: Date;
75
+ location: string;
76
+ }
77
+ /** A user as SCIM answers it: every attribute Slotlock keeps, so a GET then PUT loses nothing. */
78
+ export declare function slotlockScimUserResource(id: string, user: SlotlockScimUserAttributes, meta: SlotlockScimMeta): {
79
+ schemas: string[];
80
+ id: string;
81
+ externalId?: string;
82
+ userName: string;
83
+ active: boolean;
84
+ name?: {
85
+ givenName?: string;
86
+ familyName?: string;
87
+ };
88
+ displayName?: string;
89
+ emails?: {
90
+ value: string;
91
+ type: string;
92
+ primary: boolean;
93
+ }[];
94
+ meta: {
95
+ resourceType: string;
96
+ created: string;
97
+ lastModified: string;
98
+ location: string;
99
+ };
100
+ };
101
+ /** A group as SCIM answers it, with its members unless asked not to. */
102
+ export declare function slotlockScimGroupResource(id: string, group: SlotlockScimGroupAttributes, meta: SlotlockScimMeta, withMembers?: boolean): {
103
+ schemas: string[];
104
+ id: string;
105
+ displayName: string;
106
+ externalId?: string;
107
+ members?: {
108
+ value: string;
109
+ }[];
110
+ meta: {
111
+ resourceType: string;
112
+ created: string;
113
+ lastModified: string;
114
+ location: string;
115
+ };
116
+ };
117
+ /** A list response; an empty one too (Entra needs it), with integer counts. */
118
+ export declare function slotlockScimList(resources: readonly unknown[], totalResults: number, startIndex: number): {
119
+ schemas: string[];
120
+ totalResults: number;
121
+ startIndex: number;
122
+ itemsPerPage: number;
123
+ Resources: readonly unknown[];
124
+ };
125
+ /** startIndex and count as a page: startIndex below 1 read as 1, count clamped to 0–100. */
126
+ export declare function slotlockScimPage(query: URLSearchParams): {
127
+ startIndex: number;
128
+ count: number;
129
+ };
130
+ /** What Slotlock supports (RFC 7643 §5): PATCH, filters up to 100 results; no bulk, sort or ETags. */
131
+ export declare function slotlockScimServiceProviderConfig(documentationUri: string): {
132
+ schemas: string[];
133
+ documentationUri: string;
134
+ patch: {
135
+ supported: boolean;
136
+ };
137
+ bulk: {
138
+ supported: boolean;
139
+ maxOperations: number;
140
+ maxPayloadSize: number;
141
+ };
142
+ filter: {
143
+ supported: boolean;
144
+ maxResults: number;
145
+ };
146
+ changePassword: {
147
+ supported: boolean;
148
+ };
149
+ sort: {
150
+ supported: boolean;
151
+ };
152
+ etag: {
153
+ supported: boolean;
154
+ };
155
+ authenticationSchemes: {
156
+ type: string;
157
+ name: string;
158
+ description: string;
159
+ primary: boolean;
160
+ }[];
161
+ meta: {
162
+ resourceType: string;
163
+ };
164
+ };
165
+ /** The resource types Slotlock serves, as a list. */
166
+ export declare function slotlockScimResourceTypes(base: string): {
167
+ schemas: string[];
168
+ totalResults: number;
169
+ startIndex: number;
170
+ itemsPerPage: number;
171
+ Resources: readonly unknown[];
172
+ };
173
+ /** The schemas of what Slotlock keeps, as a list. */
174
+ export declare function slotlockScimSchemas(base: string): {
175
+ schemas: string[];
176
+ totalResults: number;
177
+ startIndex: number;
178
+ itemsPerPage: number;
179
+ Resources: readonly unknown[];
180
+ };