@rawdash/connector-workos 0.0.1

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/dist/index.js ADDED
@@ -0,0 +1,610 @@
1
+ // ../../connector-shared/dist/index.js
2
+ var HTTP_CLIENT_VERSION = "0.0.0";
3
+ var DEFAULT_USER_AGENT = `rawdash-connector/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;
4
+ function connectorUserAgent(connectorId) {
5
+ return `rawdash-connector-${connectorId}/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;
6
+ }
7
+ function parseEpoch(value, unit) {
8
+ if (value === null || value === void 0) {
9
+ return null;
10
+ }
11
+ if (unit === "iso") {
12
+ if (typeof value !== "string") {
13
+ return null;
14
+ }
15
+ const ms = new Date(value).getTime();
16
+ return Number.isFinite(ms) ? ms : null;
17
+ }
18
+ if (typeof value === "string" && value.trim() === "") {
19
+ return null;
20
+ }
21
+ const n = typeof value === "number" ? value : Number(value);
22
+ if (!Number.isFinite(n)) {
23
+ return null;
24
+ }
25
+ const result = unit === "s" ? n * 1e3 : n;
26
+ return Number.isFinite(result) ? result : null;
27
+ }
28
+
29
+ // src/workos.ts
30
+ import {
31
+ BaseConnector,
32
+ defineConfigFields,
33
+ defineConnectorDoc,
34
+ defineResources,
35
+ makeChunkedCursorGuard,
36
+ paginateChunked,
37
+ schemasFromResources,
38
+ selectActivePhases
39
+ } from "@rawdash/core";
40
+ import { z } from "zod";
41
+ var configFields = defineConfigFields(
42
+ z.object({
43
+ apiKey: z.object({ $secret: z.string().min(1) }).meta({
44
+ label: "API key",
45
+ description: "WorkOS API key (server-side, starts with `sk_`). Used as a bearer token on every request. Read-only access is sufficient for sync.",
46
+ placeholder: "WORKOS_API_KEY",
47
+ secret: true
48
+ }),
49
+ resources: z.array(
50
+ z.enum(["organizations", "connections", "directories", "auth_events"])
51
+ ).nonempty().optional().meta({
52
+ label: "Resources",
53
+ description: "Which WorkOS resources to sync. Omit to sync all of them."
54
+ }),
55
+ authEventsLookbackDays: z.number().int().positive().max(90).optional().meta({
56
+ label: "Auth events lookback (days)",
57
+ description: "On a full sync (and when no incremental cursor is available), how many days of authentication events to fetch. Defaults to 30. Caps at 90.",
58
+ placeholder: "30"
59
+ })
60
+ })
61
+ );
62
+ var doc = defineConnectorDoc({
63
+ displayName: "WorkOS",
64
+ category: "security",
65
+ brandColor: "#6363F1",
66
+ tagline: "Sync organizations, SSO connections, directory-sync directories, and authentication events from a WorkOS workspace for B2B SaaS onboarding and SSO-activity dashboards.",
67
+ vendor: {
68
+ name: "WorkOS",
69
+ domain: "workos.com",
70
+ apiDocs: "https://workos.com/docs/reference",
71
+ website: "https://workos.com"
72
+ },
73
+ auth: {
74
+ summary: "A WorkOS API key (server-side, starts with `sk_`) is required. It is sent as a bearer token on every request and never leaves the workspace.",
75
+ setup: [
76
+ "Sign in to the WorkOS Dashboard and switch to the environment (Sandbox or Production) you want to sync.",
77
+ "Open API Keys in the left navigation.",
78
+ "Create a new secret key (or copy an existing one). WorkOS only shows the secret once on creation.",
79
+ 'Store it as a rawdash secret and reference it from the connector config as `apiKey: secret("WORKOS_API_KEY")`.'
80
+ ]
81
+ },
82
+ rateLimit: "WorkOS list endpoints return X-RateLimit-Remaining and X-RateLimit-Reset (Unix seconds) headers when throttling kicks in; the shared HTTP client falls back to Retry-After on 429.",
83
+ limitations: [
84
+ "Authentication events use the WorkOS Events API filtered to authentication.* event types (sign-in success and failure across SSO, OAuth, password, magic auth, MFA). Other event categories (dsync.*, organization.*) are not synced.",
85
+ "Organizations, connections, and directories are fetched in full on every sync; the WorkOS list endpoints do not expose a server-side updated_at filter, so the scope is cleared and rewritten on full syncs and left untouched on incremental syncs.",
86
+ "Directory-sync user and group rows are out of scope; this connector tracks the directory entities themselves, not their imported memberships."
87
+ ]
88
+ });
89
+ var workosCredentials = {
90
+ apiKey: {
91
+ description: "WorkOS API key",
92
+ auth: "required"
93
+ }
94
+ };
95
+ var PHASE_ORDER = [
96
+ "organizations",
97
+ "connections",
98
+ "directories",
99
+ "auth_events"
100
+ ];
101
+ var isWorkOSSyncCursor = makeChunkedCursorGuard(PHASE_ORDER);
102
+ var ORGANIZATION_ENTITY = "workos_organization";
103
+ var CONNECTION_ENTITY = "workos_connection";
104
+ var DIRECTORY_ENTITY = "workos_directory";
105
+ var AUTH_EVENT = "workos_auth_event";
106
+ var PAGE_SIZE = 100;
107
+ var DEFAULT_AUTH_LOOKBACK_DAYS = 30;
108
+ var BASE_URL = "https://api.workos.com";
109
+ var AUTH_EVENT_TYPES = [
110
+ "authentication.email_verification_succeeded",
111
+ "authentication.magic_auth_succeeded",
112
+ "authentication.magic_auth_failed",
113
+ "authentication.mfa_succeeded",
114
+ "authentication.mfa_failed",
115
+ "authentication.oauth_succeeded",
116
+ "authentication.oauth_failed",
117
+ "authentication.password_succeeded",
118
+ "authentication.password_failed",
119
+ "authentication.sso_succeeded",
120
+ "authentication.sso_failed"
121
+ ];
122
+ var idString = z.string().min(1);
123
+ var listMetadataSchema = z.object({
124
+ before: z.string().nullish(),
125
+ after: z.string().nullish()
126
+ });
127
+ var organizationDomainSchema = z.object({
128
+ domain: z.string(),
129
+ state: z.string().nullish()
130
+ });
131
+ var organizationSchema = z.object({
132
+ id: idString,
133
+ name: z.string(),
134
+ domains: z.array(organizationDomainSchema).nullish(),
135
+ allow_profiles_outside_organization: z.boolean().nullish(),
136
+ created_at: z.string(),
137
+ updated_at: z.string().nullish()
138
+ });
139
+ var organizationsResponseSchema = z.object({
140
+ data: z.array(organizationSchema),
141
+ list_metadata: listMetadataSchema
142
+ });
143
+ var connectionSchema = z.object({
144
+ id: idString,
145
+ name: z.string(),
146
+ organization_id: z.string().nullish(),
147
+ connection_type: z.string(),
148
+ state: z.string().nullish(),
149
+ status: z.string().nullish(),
150
+ created_at: z.string(),
151
+ updated_at: z.string().nullish()
152
+ });
153
+ var connectionsResponseSchema = z.object({
154
+ data: z.array(connectionSchema),
155
+ list_metadata: listMetadataSchema
156
+ });
157
+ var directorySchema = z.object({
158
+ id: idString,
159
+ name: z.string(),
160
+ organization_id: z.string().nullish(),
161
+ type: z.string(),
162
+ state: z.string().nullish(),
163
+ created_at: z.string(),
164
+ updated_at: z.string().nullish()
165
+ });
166
+ var directoriesResponseSchema = z.object({
167
+ data: z.array(directorySchema),
168
+ list_metadata: listMetadataSchema
169
+ });
170
+ var eventSchema = z.object({
171
+ id: idString,
172
+ event: z.string(),
173
+ created_at: z.string(),
174
+ data: z.object({
175
+ organization_id: z.string().nullish(),
176
+ user_id: z.string().nullish(),
177
+ email: z.string().nullish(),
178
+ ip_address: z.string().nullish(),
179
+ connection_id: z.string().nullish(),
180
+ connection_type: z.string().nullish()
181
+ }).passthrough().nullish()
182
+ });
183
+ var eventsResponseSchema = z.object({
184
+ data: z.array(eventSchema),
185
+ list_metadata: listMetadataSchema
186
+ });
187
+ var workosResources = defineResources({
188
+ [ORGANIZATION_ENTITY]: {
189
+ shape: "entity",
190
+ filterable: [],
191
+ description: "WorkOS organizations (tenants) with their display name, domains, and creation timestamp.",
192
+ endpoint: "GET /organizations",
193
+ fields: [
194
+ { name: "name", description: "Organization display name." },
195
+ {
196
+ name: "domains",
197
+ description: "Comma-separated list of domains attached to the organization."
198
+ },
199
+ {
200
+ name: "createdAt",
201
+ description: "When the organization was created (Unix ms)."
202
+ }
203
+ ],
204
+ responses: { organizations: organizationsResponseSchema }
205
+ },
206
+ [CONNECTION_ENTITY]: {
207
+ shape: "entity",
208
+ filterable: [
209
+ {
210
+ field: "state",
211
+ ops: ["eq"],
212
+ values: ["active", "inactive", "draft", "linked", "unlinked"]
213
+ },
214
+ { field: "connectionType", ops: ["eq"] }
215
+ ],
216
+ description: "WorkOS SSO connections (one per identity provider per organization) with their type, state, and parent organization.",
217
+ endpoint: "GET /connections",
218
+ fields: [
219
+ {
220
+ name: "connectionType",
221
+ description: "Connection type (e.g. OktaSAML, AzureSAML, GoogleOAuth)."
222
+ },
223
+ {
224
+ name: "organizationId",
225
+ description: "WorkOS organization that owns the connection."
226
+ },
227
+ {
228
+ name: "state",
229
+ description: "Lifecycle state (active, inactive, draft, linked, unlinked)."
230
+ },
231
+ { name: "name", description: "Connection display name." },
232
+ {
233
+ name: "createdAt",
234
+ description: "When the connection was created (Unix ms)."
235
+ }
236
+ ],
237
+ responses: { connections: connectionsResponseSchema }
238
+ },
239
+ [DIRECTORY_ENTITY]: {
240
+ shape: "entity",
241
+ filterable: [
242
+ {
243
+ field: "state",
244
+ ops: ["eq"],
245
+ values: ["active", "inactive", "validating", "linked", "unlinked"]
246
+ },
247
+ { field: "directoryType", ops: ["eq"] }
248
+ ],
249
+ description: "WorkOS directory-sync directories (SCIM/HRIS feeds) with their type, state, and parent organization.",
250
+ endpoint: "GET /directories",
251
+ fields: [
252
+ {
253
+ name: "directoryType",
254
+ description: "Directory provider type (e.g. okta scim v2.0, azure scim v2.0, bamboohr)."
255
+ },
256
+ {
257
+ name: "organizationId",
258
+ description: "WorkOS organization that owns the directory."
259
+ },
260
+ {
261
+ name: "state",
262
+ description: "Lifecycle state (active, inactive, validating, linked, unlinked)."
263
+ },
264
+ { name: "name", description: "Directory display name." },
265
+ {
266
+ name: "createdAt",
267
+ description: "When the directory was created (Unix ms)."
268
+ }
269
+ ],
270
+ responses: { directories: directoriesResponseSchema }
271
+ },
272
+ [AUTH_EVENT]: {
273
+ shape: "event",
274
+ filterable: [
275
+ { field: "eventType", ops: ["eq"], values: [...AUTH_EVENT_TYPES] }
276
+ ],
277
+ description: "Authentication events from the WorkOS Events API (SSO, OAuth, password, magic auth, and MFA sign-in successes and failures).",
278
+ endpoint: "GET /events",
279
+ notes: "Filtered to the authentication.* event family. Incremental syncs pass `range_start` so only events newer than the watermark are returned.",
280
+ fields: [
281
+ {
282
+ name: "eventType",
283
+ description: "WorkOS event name (authentication.sso_succeeded, etc)."
284
+ },
285
+ {
286
+ name: "outcome",
287
+ description: '"succeeded" or "failed" derived from the event suffix.'
288
+ },
289
+ {
290
+ name: "method",
291
+ description: "Authentication method (sso, oauth, password, magic_auth, mfa, email_verification)."
292
+ },
293
+ {
294
+ name: "organizationId",
295
+ description: "WorkOS organization the event belongs to (may be null)."
296
+ },
297
+ {
298
+ name: "userId",
299
+ description: "WorkOS user id involved in the event (may be null)."
300
+ },
301
+ {
302
+ name: "connectionId",
303
+ description: "WorkOS connection id used for the event (may be null)."
304
+ },
305
+ {
306
+ name: "connectionType",
307
+ description: "Connection type used for the event (may be null for non-SSO methods)."
308
+ },
309
+ {
310
+ name: "ipAddress",
311
+ description: "Client IP captured by WorkOS (may be null)."
312
+ }
313
+ ],
314
+ responses: { auth_events: eventsResponseSchema }
315
+ }
316
+ });
317
+ var id = "workos";
318
+ function isoToMs(value) {
319
+ if (!value) {
320
+ return null;
321
+ }
322
+ return parseEpoch(value, "iso");
323
+ }
324
+ function isoToMsOrZero(value) {
325
+ return isoToMs(value) ?? 0;
326
+ }
327
+ function summarizeDomains(domains) {
328
+ if (!domains || domains.length === 0) {
329
+ return null;
330
+ }
331
+ return domains.map((d) => d.domain).join(", ");
332
+ }
333
+ function deriveOutcome(eventType) {
334
+ if (eventType.endsWith("_succeeded")) {
335
+ return "succeeded";
336
+ }
337
+ if (eventType.endsWith("_failed")) {
338
+ return "failed";
339
+ }
340
+ return null;
341
+ }
342
+ function deriveMethod(eventType) {
343
+ const m = /^authentication\.(.+?)_(?:succeeded|failed)$/.exec(eventType);
344
+ return m ? m[1] : null;
345
+ }
346
+ function isAuthEventType(value) {
347
+ return AUTH_EVENT_TYPES.includes(value);
348
+ }
349
+ function lookbackStartIso(days) {
350
+ const ts = Date.now() - days * 24 * 60 * 60 * 1e3;
351
+ return new Date(ts).toISOString();
352
+ }
353
+ var WorkOSConnector = class _WorkOSConnector extends BaseConnector {
354
+ static id = id;
355
+ static resources = workosResources;
356
+ static schemas = schemasFromResources(workosResources);
357
+ static create(input, ctx) {
358
+ const parsed = configFields.parse(input);
359
+ return new _WorkOSConnector(
360
+ {
361
+ resources: parsed.resources,
362
+ authEventsLookbackDays: parsed.authEventsLookbackDays
363
+ },
364
+ { apiKey: parsed.apiKey },
365
+ ctx
366
+ );
367
+ }
368
+ id = id;
369
+ credentials = workosCredentials;
370
+ buildHeaders() {
371
+ return {
372
+ Authorization: `Bearer ${this.creds.apiKey}`,
373
+ Accept: "application/json",
374
+ "User-Agent": connectorUserAgent("workos")
375
+ };
376
+ }
377
+ apiGet(url, resource, signal) {
378
+ return this.get(url, {
379
+ resource,
380
+ headers: this.buildHeaders(),
381
+ signal
382
+ });
383
+ }
384
+ buildListUrl(path, after, extra = {}) {
385
+ const url = new URL(`${BASE_URL}${path}`);
386
+ url.searchParams.set("limit", String(PAGE_SIZE));
387
+ url.searchParams.set("order", "asc");
388
+ if (after) {
389
+ url.searchParams.set("after", after);
390
+ }
391
+ for (const [k, v] of Object.entries(extra)) {
392
+ url.searchParams.append(k, v);
393
+ }
394
+ return url.toString();
395
+ }
396
+ buildEventsUrl(after, options) {
397
+ const url = new URL(`${BASE_URL}/events`);
398
+ url.searchParams.set("limit", String(PAGE_SIZE));
399
+ for (const t of AUTH_EVENT_TYPES) {
400
+ url.searchParams.append("events", t);
401
+ }
402
+ if (after) {
403
+ url.searchParams.set("after", after);
404
+ } else {
405
+ const rangeStart = options.since ?? this.defaultEventsRangeStart();
406
+ url.searchParams.set("range_start", rangeStart);
407
+ }
408
+ return url.toString();
409
+ }
410
+ defaultEventsRangeStart() {
411
+ const days = this.settings.authEventsLookbackDays ?? DEFAULT_AUTH_LOOKBACK_DAYS;
412
+ return lookbackStartIso(days);
413
+ }
414
+ async fetchOrganizationsPage(page, signal) {
415
+ const url = this.buildListUrl("/organizations", page);
416
+ const res = await this.apiGet(
417
+ url,
418
+ "organizations",
419
+ signal
420
+ );
421
+ const next = res.body.list_metadata.after ?? null;
422
+ return { items: res.body.data, next };
423
+ }
424
+ async fetchConnectionsPage(page, signal) {
425
+ const url = this.buildListUrl("/connections", page);
426
+ const res = await this.apiGet(
427
+ url,
428
+ "connections",
429
+ signal
430
+ );
431
+ const next = res.body.list_metadata.after ?? null;
432
+ return { items: res.body.data, next };
433
+ }
434
+ async fetchDirectoriesPage(page, signal) {
435
+ const url = this.buildListUrl("/directories", page);
436
+ const res = await this.apiGet(
437
+ url,
438
+ "directories",
439
+ signal
440
+ );
441
+ const next = res.body.list_metadata.after ?? null;
442
+ return { items: res.body.data, next };
443
+ }
444
+ async fetchEventsPage(page, options, signal) {
445
+ const url = this.buildEventsUrl(page, options);
446
+ const res = await this.apiGet(url, "auth_events", signal);
447
+ const next = res.body.list_metadata.after ?? null;
448
+ return { items: res.body.data, next };
449
+ }
450
+ async fetchPhasePage(phase, page, options, signal) {
451
+ switch (phase) {
452
+ case "organizations":
453
+ return this.fetchOrganizationsPage(page, signal);
454
+ case "connections":
455
+ return this.fetchConnectionsPage(page, signal);
456
+ case "directories":
457
+ return this.fetchDirectoriesPage(page, signal);
458
+ case "auth_events":
459
+ return this.fetchEventsPage(page, options, signal);
460
+ }
461
+ }
462
+ async writeOrganizations(storage, items) {
463
+ for (const org of items) {
464
+ await storage.entity({
465
+ type: ORGANIZATION_ENTITY,
466
+ id: org.id,
467
+ attributes: {
468
+ name: org.name,
469
+ domains: summarizeDomains(org.domains),
470
+ createdAt: isoToMs(org.created_at)
471
+ },
472
+ updated_at: isoToMsOrZero(org.updated_at ?? org.created_at)
473
+ });
474
+ }
475
+ }
476
+ async writeConnections(storage, items) {
477
+ for (const c of items) {
478
+ await storage.entity({
479
+ type: CONNECTION_ENTITY,
480
+ id: c.id,
481
+ attributes: {
482
+ name: c.name,
483
+ connectionType: c.connection_type,
484
+ organizationId: c.organization_id ?? null,
485
+ state: c.state ?? c.status ?? null,
486
+ createdAt: isoToMs(c.created_at)
487
+ },
488
+ updated_at: isoToMsOrZero(c.updated_at ?? c.created_at)
489
+ });
490
+ }
491
+ }
492
+ async writeDirectories(storage, items) {
493
+ for (const d of items) {
494
+ await storage.entity({
495
+ type: DIRECTORY_ENTITY,
496
+ id: d.id,
497
+ attributes: {
498
+ name: d.name,
499
+ directoryType: d.type,
500
+ organizationId: d.organization_id ?? null,
501
+ state: d.state ?? null,
502
+ createdAt: isoToMs(d.created_at)
503
+ },
504
+ updated_at: isoToMsOrZero(d.updated_at ?? d.created_at)
505
+ });
506
+ }
507
+ }
508
+ async writeAuthEvents(storage, items, sinceMs) {
509
+ for (const ev of items) {
510
+ if (!isAuthEventType(ev.event)) {
511
+ continue;
512
+ }
513
+ const ts = isoToMs(ev.created_at);
514
+ if (ts === null) {
515
+ continue;
516
+ }
517
+ if (sinceMs !== null && ts <= sinceMs) {
518
+ continue;
519
+ }
520
+ const data = ev.data ?? {};
521
+ const attributes = {
522
+ eventType: ev.event,
523
+ outcome: deriveOutcome(ev.event),
524
+ method: deriveMethod(ev.event),
525
+ organizationId: data.organization_id ?? null,
526
+ userId: data.user_id ?? null,
527
+ connectionId: data.connection_id ?? null,
528
+ connectionType: data.connection_type ?? null,
529
+ ipAddress: data.ip_address ?? null,
530
+ eventId: ev.id
531
+ };
532
+ await storage.event({
533
+ name: AUTH_EVENT,
534
+ start_ts: ts,
535
+ end_ts: null,
536
+ attributes
537
+ });
538
+ }
539
+ }
540
+ async clearScopeOnFirstPage(storage, phase, isFull) {
541
+ if (!isFull) {
542
+ return;
543
+ }
544
+ switch (phase) {
545
+ case "organizations":
546
+ await storage.entities([], { types: [ORGANIZATION_ENTITY] });
547
+ return;
548
+ case "connections":
549
+ await storage.entities([], { types: [CONNECTION_ENTITY] });
550
+ return;
551
+ case "directories":
552
+ await storage.entities([], { types: [DIRECTORY_ENTITY] });
553
+ return;
554
+ case "auth_events":
555
+ await storage.events([], { names: [AUTH_EVENT] });
556
+ return;
557
+ }
558
+ }
559
+ async writePhase(storage, phase, items, sinceMs) {
560
+ switch (phase) {
561
+ case "organizations":
562
+ return this.writeOrganizations(storage, items);
563
+ case "connections":
564
+ return this.writeConnections(storage, items);
565
+ case "directories":
566
+ return this.writeDirectories(storage, items);
567
+ case "auth_events":
568
+ return this.writeAuthEvents(storage, items, sinceMs);
569
+ }
570
+ }
571
+ resolveCursor(cursor) {
572
+ return isWorkOSSyncCursor(cursor) ? cursor : void 0;
573
+ }
574
+ async sync(options, storage, signal) {
575
+ const cursor = this.resolveCursor(options.cursor);
576
+ const isFull = options.mode === "full";
577
+ const sinceMs = options.since ? Date.parse(options.since) : null;
578
+ const sinceMsOrNull = sinceMs !== null && Number.isFinite(sinceMs) ? sinceMs : null;
579
+ const phases = selectActivePhases(
580
+ (r) => r,
581
+ PHASE_ORDER,
582
+ this.settings.resources
583
+ );
584
+ return paginateChunked({
585
+ phases,
586
+ cursor,
587
+ signal,
588
+ logger: this.logger,
589
+ fetchPage: async (phase, page, sig) => this.fetchPhasePage(phase, page, options, sig),
590
+ writeBatch: async (phase, items, page) => {
591
+ if (page === null) {
592
+ await this.clearScopeOnFirstPage(storage, phase, isFull);
593
+ }
594
+ await this.writePhase(storage, phase, items, sinceMsOrNull);
595
+ }
596
+ });
597
+ }
598
+ };
599
+
600
+ // src/index.ts
601
+ var index_default = WorkOSConnector;
602
+ export {
603
+ WorkOSConnector,
604
+ configFields,
605
+ index_default as default,
606
+ doc,
607
+ id,
608
+ workosResources as resources
609
+ };
610
+ //# sourceMappingURL=index.js.map