@rawdash/connector-launchdarkly 0.17.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/dist/index.js ADDED
@@ -0,0 +1,731 @@
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 standardRateLimitPolicy(config) {
8
+ const { remainingHeader, resetHeader, resetUnit, resetFallbackMs } = config;
9
+ const multiplier = resetUnit === "s" ? 1e3 : 1;
10
+ return {
11
+ parse(h) {
12
+ const remainingRaw = h.get(remainingHeader);
13
+ if (remainingRaw === null || remainingRaw.trim() === "") {
14
+ return null;
15
+ }
16
+ const remaining = Number(remainingRaw);
17
+ if (!Number.isFinite(remaining)) {
18
+ return null;
19
+ }
20
+ const resetRaw = h.get(resetHeader);
21
+ if (resetRaw === null) {
22
+ if (resetFallbackMs === void 0) {
23
+ return null;
24
+ }
25
+ return {
26
+ remaining,
27
+ resetAt: new Date(Date.now() + resetFallbackMs)
28
+ };
29
+ }
30
+ if (resetRaw.trim() === "") {
31
+ return null;
32
+ }
33
+ const reset = Number(resetRaw);
34
+ if (!Number.isFinite(reset) || reset < 0) {
35
+ return null;
36
+ }
37
+ const resetMs = reset * multiplier;
38
+ if (!Number.isFinite(resetMs)) {
39
+ return null;
40
+ }
41
+ return { remaining, resetAt: new Date(resetMs) };
42
+ }
43
+ };
44
+ }
45
+ function sanitizeAllowedUrl(options) {
46
+ const { url, host, pathname, protocol = "https:" } = options;
47
+ if (url === null) {
48
+ return null;
49
+ }
50
+ try {
51
+ const u = new URL(url);
52
+ if (u.protocol !== protocol || u.host !== host || u.pathname !== pathname) {
53
+ return null;
54
+ }
55
+ return u.toString();
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+ function parseEpoch(value, unit) {
61
+ if (value === null || value === void 0) {
62
+ return null;
63
+ }
64
+ if (unit === "iso") {
65
+ if (typeof value !== "string") {
66
+ return null;
67
+ }
68
+ const ms = new Date(value).getTime();
69
+ return Number.isFinite(ms) ? ms : null;
70
+ }
71
+ if (typeof value === "string" && value.trim() === "") {
72
+ return null;
73
+ }
74
+ const n = typeof value === "number" ? value : Number(value);
75
+ if (!Number.isFinite(n)) {
76
+ return null;
77
+ }
78
+ const result = unit === "s" ? n * 1e3 : n;
79
+ return Number.isFinite(result) ? result : null;
80
+ }
81
+
82
+ // src/launchdarkly.ts
83
+ import {
84
+ BaseConnector,
85
+ defineConfigFields,
86
+ defineConnectorDoc,
87
+ defineResources,
88
+ makeChunkedCursorGuard,
89
+ paginateChunked,
90
+ schemasFromResources,
91
+ selectActivePhases
92
+ } from "@rawdash/core";
93
+ import { z } from "zod";
94
+ var configFields = defineConfigFields(
95
+ z.object({
96
+ apiToken: z.object({ $secret: z.string() }).meta({
97
+ label: "API Token",
98
+ description: "LaunchDarkly API access token with read access. Create one at LaunchDarkly -> Account settings -> Authorization -> Access tokens.",
99
+ placeholder: "api-...",
100
+ secret: true
101
+ }),
102
+ projects: z.array(z.string().min(1)).nonempty().optional().meta({
103
+ label: "Projects (optional)",
104
+ description: "Restrict the sync to specific LaunchDarkly project keys. Omit to sync every project the token can see."
105
+ }),
106
+ resources: z.array(z.enum(["projects", "feature_flags", "flag_events"])).nonempty().optional().meta({
107
+ label: "Resources",
108
+ description: "Which LaunchDarkly resources to sync. Omit to sync all of them. feature_flags depends on projects being fetched - enabling it without projects still runs the projects query, but skips writing project entities."
109
+ }),
110
+ auditLogLookbackDays: z.number().int().positive().max(90).optional().meta({
111
+ label: "Audit log lookback (days)",
112
+ description: "How many days back to fetch audit-log events on a full sync. Defaults to 30. LaunchDarkly returns audit events newest-first; this caps the backfill window.",
113
+ placeholder: "30"
114
+ })
115
+ })
116
+ );
117
+ var doc = defineConnectorDoc({
118
+ displayName: "LaunchDarkly",
119
+ category: "engineering",
120
+ brandColor: "#FFC110",
121
+ tagline: "Sync LaunchDarkly projects, feature flags, and audit-log events - including flag state per environment, kind, and recent rollout changes.",
122
+ vendor: {
123
+ name: "LaunchDarkly",
124
+ apiDocs: "https://apidocs.launchdarkly.com/",
125
+ website: "https://launchdarkly.com"
126
+ },
127
+ auth: {
128
+ summary: "A LaunchDarkly API access token with read access is required. Personal or service tokens both work; a reader-role service token is the recommended minimum.",
129
+ setup: [
130
+ "Open LaunchDarkly -> Account settings -> Authorization -> Access tokens.",
131
+ "Create an access token with the Reader role (or a custom role that grants read access to projects, flags, and the audit log).",
132
+ 'Copy the generated token and store it as a secret, referencing it from the connector config as `apiToken: secret("LD_API_TOKEN")`.'
133
+ ]
134
+ },
135
+ rateLimit: "LaunchDarkly defaults to 5 requests/second per token; X-Ratelimit-Global-Remaining and X-Ratelimit-Reset (Unix ms) headers are honored. Retry-After is honored on 429.",
136
+ limitations: [
137
+ "Flag-level served counts (Data Export) and the Experimentation API are out of scope.",
138
+ "Feature flags are fetched per project; the audit log is a single global stream filtered by created-after timestamp.",
139
+ "Custom hosts / federal instances are out of scope (pagination URLs are pinned to app.launchdarkly.com)."
140
+ ]
141
+ });
142
+ var launchDarklyCredentials = {
143
+ apiToken: {
144
+ description: "LaunchDarkly API access token",
145
+ auth: "required"
146
+ }
147
+ };
148
+ var launchDarklyRateLimit = standardRateLimitPolicy({
149
+ remainingHeader: "x-ratelimit-global-remaining",
150
+ resetHeader: "x-ratelimit-reset",
151
+ resetUnit: "ms"
152
+ });
153
+ var PHASE_ORDER = ["projects", "feature_flags", "audit_log"];
154
+ var isLaunchDarklySyncCursor = makeChunkedCursorGuard(PHASE_ORDER);
155
+ var idString = z.string().min(1);
156
+ var linksSchema = z.object({
157
+ next: z.object({ href: z.string(), type: z.string().optional() }).optional(),
158
+ self: z.object({ href: z.string(), type: z.string().optional() }).optional()
159
+ }).optional();
160
+ var projectSchema = z.object({
161
+ _id: idString,
162
+ key: z.string().min(1),
163
+ name: z.string(),
164
+ tags: z.array(z.string()).optional()
165
+ });
166
+ var projectsResponseSchema = z.object({
167
+ items: z.array(projectSchema),
168
+ _links: linksSchema,
169
+ totalCount: z.number().int().nonnegative().optional()
170
+ });
171
+ var flagEnvironmentSchema = z.object({
172
+ on: z.boolean().optional(),
173
+ archived: z.boolean().optional(),
174
+ salt: z.string().optional(),
175
+ lastModified: z.number().int().nonnegative().optional()
176
+ });
177
+ var flagSchema = z.object({
178
+ _id: z.string().optional(),
179
+ key: z.string().min(1),
180
+ name: z.string(),
181
+ description: z.string().optional(),
182
+ kind: z.string(),
183
+ archived: z.boolean().optional(),
184
+ tags: z.array(z.string()).optional(),
185
+ creationDate: z.number().int().nonnegative().optional(),
186
+ variations: z.array(
187
+ z.object({
188
+ _id: z.string().optional(),
189
+ name: z.string().nullable().optional(),
190
+ value: z.unknown()
191
+ })
192
+ ).optional(),
193
+ environments: z.record(z.string(), flagEnvironmentSchema).optional()
194
+ });
195
+ var flagsResponseSchema = z.object({
196
+ items: z.array(flagSchema),
197
+ _links: linksSchema,
198
+ totalCount: z.number().int().nonnegative().optional()
199
+ });
200
+ var auditEntrySchema = z.object({
201
+ _id: idString,
202
+ kind: z.string().optional(),
203
+ name: z.string().optional(),
204
+ description: z.string().optional(),
205
+ shortDescription: z.string().optional(),
206
+ comment: z.string().optional(),
207
+ date: z.number().int().nonnegative(),
208
+ member: z.object({
209
+ _id: z.string().optional(),
210
+ email: z.string().optional(),
211
+ firstName: z.string().optional(),
212
+ lastName: z.string().optional()
213
+ }).optional(),
214
+ target: z.object({
215
+ name: z.string().optional(),
216
+ resources: z.array(z.string()).optional()
217
+ }).optional(),
218
+ titleVerb: z.string().optional(),
219
+ title: z.string().optional()
220
+ });
221
+ var auditLogResponseSchema = z.object({
222
+ items: z.array(auditEntrySchema),
223
+ _links: linksSchema,
224
+ totalCount: z.number().int().nonnegative().optional()
225
+ });
226
+ var launchdarklyResources = defineResources({
227
+ launchdarkly_project: {
228
+ shape: "entity",
229
+ description: "LaunchDarkly projects, with their key, display name, and tags.",
230
+ endpoint: "GET /api/v2/projects",
231
+ fields: [
232
+ { name: "key", description: "Project key (stable identifier)." },
233
+ { name: "name", description: "Project display name." },
234
+ { name: "tags", description: "Project tags." }
235
+ ],
236
+ responses: { projects: projectsResponseSchema }
237
+ },
238
+ launchdarkly_feature_flag: {
239
+ shape: "entity",
240
+ description: "Feature flags across one or more projects, including kind (boolean | multivariate | other), archived state, tags, variations, and per-environment on/off + last-modified.",
241
+ endpoint: "GET /api/v2/flags/{projectKey}",
242
+ fields: [
243
+ { name: "key", description: "Flag key (stable identifier)." },
244
+ { name: "name", description: "Flag display name." },
245
+ {
246
+ name: "kind",
247
+ description: "Flag kind: boolean | multivariate | other."
248
+ },
249
+ {
250
+ name: "projectKey",
251
+ description: "Project key the flag belongs to."
252
+ },
253
+ { name: "archived", description: "Whether the flag is archived." },
254
+ { name: "tags", description: "Flag tags." },
255
+ {
256
+ name: "variationCount",
257
+ description: "Number of variations on the flag."
258
+ },
259
+ {
260
+ name: "environments",
261
+ description: "Map of envKey -> { on, archived, lastModified } summarizing flag state per environment."
262
+ },
263
+ {
264
+ name: "creationDate",
265
+ description: "Flag creation timestamp (epoch ms)."
266
+ }
267
+ ],
268
+ responses: { feature_flags: flagsResponseSchema }
269
+ },
270
+ launchdarkly_flag_event: {
271
+ shape: "event",
272
+ description: "Audit-log entries for flag-related changes (flag created / modified / toggled / archived), with the acting member and target resources.",
273
+ endpoint: "GET /api/v2/auditlog",
274
+ notes: "Filtered to entries newer than the lookback window (default 30 days) and incrementally bounded by options.since on subsequent syncs. LaunchDarkly returns events newest-first.",
275
+ fields: [
276
+ { name: "auditId", description: "LaunchDarkly audit-log entry id." },
277
+ {
278
+ name: "kind",
279
+ description: "Audit entry kind (e.g. flag, project, environment)."
280
+ },
281
+ {
282
+ name: "titleVerb",
283
+ description: 'Verb describing the action (e.g. "updated", "created").'
284
+ },
285
+ {
286
+ name: "memberEmail",
287
+ description: "Email of the member who performed the action."
288
+ },
289
+ {
290
+ name: "targetName",
291
+ description: "Name of the target resource (e.g. flag key)."
292
+ },
293
+ {
294
+ name: "targetResources",
295
+ description: "Resource paths the action touched (e.g. proj/<key>:env/<env>:flag/<key>)."
296
+ }
297
+ ],
298
+ responses: { audit_log: auditLogResponseSchema }
299
+ }
300
+ });
301
+ var LD_API_HOST = "app.launchdarkly.com";
302
+ var LD_API_BASE = `https://${LD_API_HOST}`;
303
+ var PROJECTS_PAGE_SIZE = 100;
304
+ var FLAGS_PAGE_SIZE = 100;
305
+ var AUDIT_LOG_PAGE_SIZE = 50;
306
+ var DEFAULT_AUDIT_LOOKBACK_DAYS = 30;
307
+ var MS_PER_DAY = 24 * 60 * 60 * 1e3;
308
+ var id = "launchdarkly";
309
+ var LaunchDarklyConnector = class _LaunchDarklyConnector extends BaseConnector {
310
+ static id = id;
311
+ static resources = launchdarklyResources;
312
+ static schemas = schemasFromResources(launchdarklyResources);
313
+ static create(input, ctx) {
314
+ const parsed = configFields.parse(input);
315
+ return new _LaunchDarklyConnector(
316
+ {
317
+ projects: parsed.projects,
318
+ resources: parsed.resources,
319
+ auditLogLookbackDays: parsed.auditLogLookbackDays
320
+ },
321
+ { apiToken: parsed.apiToken },
322
+ ctx
323
+ );
324
+ }
325
+ id = id;
326
+ credentials = launchDarklyCredentials;
327
+ // Project keys discovered during the projects phase, so the feature_flags
328
+ // phase can fan out to each project without re-fetching. Only trusted once
329
+ // the projects pagination has fully completed for the current sync, so a
330
+ // partial (resumed mid-phase) or stale (prior sync) list never drives the
331
+ // feature_flags fan-out.
332
+ discoveredProjectKeys = null;
333
+ discoveredProjectKeysComplete = false;
334
+ buildHeaders() {
335
+ return {
336
+ Authorization: this.creds.apiToken,
337
+ "User-Agent": connectorUserAgent("launchdarkly")
338
+ };
339
+ }
340
+ fetch(url, resource, signal) {
341
+ return this.get(url, {
342
+ resource,
343
+ headers: this.buildHeaders(),
344
+ signal,
345
+ rateLimit: launchDarklyRateLimit
346
+ });
347
+ }
348
+ // -------------------------------------------------------------------------
349
+ // Resource enablement
350
+ // -------------------------------------------------------------------------
351
+ activePhases() {
352
+ return selectActivePhases(
353
+ (r) => {
354
+ switch (r) {
355
+ case "projects":
356
+ return "projects";
357
+ case "feature_flags":
358
+ return "feature_flags";
359
+ case "flag_events":
360
+ return "audit_log";
361
+ }
362
+ },
363
+ PHASE_ORDER,
364
+ this.settings.resources
365
+ );
366
+ }
367
+ // -------------------------------------------------------------------------
368
+ // URL building + sanitization
369
+ // -------------------------------------------------------------------------
370
+ allowedPagePath(phase, page) {
371
+ switch (phase) {
372
+ case "projects":
373
+ return "/api/v2/projects";
374
+ case "audit_log":
375
+ return "/api/v2/auditlog";
376
+ case "feature_flags": {
377
+ try {
378
+ const u = new URL(page);
379
+ if (u.pathname.startsWith("/api/v2/flags/")) {
380
+ return u.pathname;
381
+ }
382
+ } catch {
383
+ return null;
384
+ }
385
+ return null;
386
+ }
387
+ }
388
+ }
389
+ sanitizePageUrl(phase, pageUrl) {
390
+ if (pageUrl === null) {
391
+ return null;
392
+ }
393
+ const allowedPath = this.allowedPagePath(phase, pageUrl);
394
+ if (allowedPath === null) {
395
+ return null;
396
+ }
397
+ return sanitizeAllowedUrl({
398
+ url: pageUrl,
399
+ host: LD_API_HOST,
400
+ pathname: allowedPath
401
+ });
402
+ }
403
+ resolveCursor(cursor) {
404
+ if (!isLaunchDarklySyncCursor(cursor)) {
405
+ return void 0;
406
+ }
407
+ return {
408
+ phase: cursor.phase,
409
+ page: this.sanitizePageUrl(cursor.phase, cursor.page)
410
+ };
411
+ }
412
+ resolveNextHref(phase, href) {
413
+ if (!href) {
414
+ return null;
415
+ }
416
+ let abs;
417
+ try {
418
+ abs = new URL(href, LD_API_BASE).toString();
419
+ } catch {
420
+ return null;
421
+ }
422
+ return this.sanitizePageUrl(phase, abs);
423
+ }
424
+ buildInitialProjectsUrl() {
425
+ const u = new URL(`${LD_API_BASE}/api/v2/projects`);
426
+ u.searchParams.set("limit", String(PROJECTS_PAGE_SIZE));
427
+ return u.toString();
428
+ }
429
+ buildInitialFlagsUrl(projectKey) {
430
+ const u = new URL(`${LD_API_BASE}/api/v2/flags/${projectKey}`);
431
+ u.searchParams.set("limit", String(FLAGS_PAGE_SIZE));
432
+ u.searchParams.set("summary", "false");
433
+ return u.toString();
434
+ }
435
+ buildInitialAuditLogUrl(options) {
436
+ const u = new URL(`${LD_API_BASE}/api/v2/auditlog`);
437
+ u.searchParams.set("limit", String(AUDIT_LOG_PAGE_SIZE));
438
+ const sinceMs = this.computeAuditSinceMs(options);
439
+ u.searchParams.set("after", String(sinceMs));
440
+ return u.toString();
441
+ }
442
+ computeAuditSinceMs(options) {
443
+ if (options.since) {
444
+ const ms = parseEpoch(options.since, "iso");
445
+ if (ms !== null) {
446
+ return ms;
447
+ }
448
+ }
449
+ const days = this.settings.auditLogLookbackDays ?? DEFAULT_AUDIT_LOOKBACK_DAYS;
450
+ return Date.now() - days * MS_PER_DAY;
451
+ }
452
+ // -------------------------------------------------------------------------
453
+ // Project enumeration for the feature_flags phase
454
+ // -------------------------------------------------------------------------
455
+ async resolveProjectKeysForFlags(signal) {
456
+ if (this.settings.projects && this.settings.projects.length > 0) {
457
+ return [...this.settings.projects];
458
+ }
459
+ if (this.discoveredProjectKeysComplete && this.discoveredProjectKeys !== null) {
460
+ return this.discoveredProjectKeys;
461
+ }
462
+ const keys = [];
463
+ let nextUrl = this.buildInitialProjectsUrl();
464
+ while (nextUrl) {
465
+ signal?.throwIfAborted();
466
+ const res = await this.fetch(
467
+ nextUrl,
468
+ "projects",
469
+ signal
470
+ );
471
+ for (const p of res.body.items) {
472
+ keys.push(p.key);
473
+ }
474
+ nextUrl = this.resolveNextHref("projects", res.body._links?.next?.href);
475
+ }
476
+ this.discoveredProjectKeys = keys;
477
+ this.discoveredProjectKeysComplete = true;
478
+ return keys;
479
+ }
480
+ // -------------------------------------------------------------------------
481
+ // Fetchers
482
+ // -------------------------------------------------------------------------
483
+ async fetchProjectsPage(page, signal) {
484
+ const url = page ?? this.buildInitialProjectsUrl();
485
+ const res = await this.fetch(url, "projects", signal);
486
+ if (page === null) {
487
+ this.discoveredProjectKeys = [];
488
+ this.discoveredProjectKeysComplete = false;
489
+ }
490
+ if (this.discoveredProjectKeys === null) {
491
+ this.discoveredProjectKeys = [];
492
+ }
493
+ for (const p of res.body.items) {
494
+ if (!this.discoveredProjectKeys.includes(p.key)) {
495
+ this.discoveredProjectKeys.push(p.key);
496
+ }
497
+ }
498
+ const next = this.resolveNextHref("projects", res.body._links?.next?.href);
499
+ if (next === null) {
500
+ this.discoveredProjectKeysComplete = true;
501
+ }
502
+ return {
503
+ items: res.body.items,
504
+ next
505
+ };
506
+ }
507
+ async fetchFlagsPage(page, signal) {
508
+ if (page === null) {
509
+ const projectKeys2 = await this.resolveProjectKeysForFlags(signal);
510
+ if (projectKeys2.length === 0) {
511
+ return { items: [], next: null };
512
+ }
513
+ const firstKey = projectKeys2[0];
514
+ return this.fetchFlagsPageInProject(firstKey, null, projectKeys2, signal);
515
+ }
516
+ let projectKey = null;
517
+ try {
518
+ const u = new URL(page);
519
+ const m = u.pathname.match(/^\/api\/v2\/flags\/([^/]+)/);
520
+ if (m) {
521
+ projectKey = decodeURIComponent(m[1]);
522
+ }
523
+ } catch {
524
+ }
525
+ if (projectKey === null) {
526
+ return { items: [], next: null };
527
+ }
528
+ const projectKeys = await this.resolveProjectKeysForFlags(signal);
529
+ return this.fetchFlagsPageInProject(projectKey, page, projectKeys, signal);
530
+ }
531
+ async fetchFlagsPageInProject(projectKey, page, projectKeys, signal) {
532
+ const url = page ?? this.buildInitialFlagsUrl(projectKey);
533
+ const res = await this.fetch(url, "feature_flags", signal);
534
+ const nextInProject = this.resolveNextHref(
535
+ "feature_flags",
536
+ res.body._links?.next?.href
537
+ );
538
+ if (nextInProject !== null) {
539
+ return {
540
+ items: [{ projectKey, flags: res.body.items }],
541
+ next: nextInProject
542
+ };
543
+ }
544
+ const idx = projectKeys.indexOf(projectKey);
545
+ const nextProject = idx >= 0 ? projectKeys[idx + 1] : void 0;
546
+ const next = nextProject !== void 0 ? this.sanitizePageUrl(
547
+ "feature_flags",
548
+ this.buildInitialFlagsUrl(nextProject)
549
+ ) : null;
550
+ return {
551
+ items: [{ projectKey, flags: res.body.items }],
552
+ next
553
+ };
554
+ }
555
+ async fetchAuditLogPage(page, options, signal) {
556
+ const url = page ?? this.buildInitialAuditLogUrl(options);
557
+ const res = await this.fetch(url, "audit_log", signal);
558
+ const items = res.body.items;
559
+ const sinceMs = this.computeAuditSinceMs(options);
560
+ const lastDate = items.at(-1)?.date;
561
+ const cutoffReached = lastDate !== void 0 && Number.isFinite(lastDate) && lastDate < sinceMs;
562
+ const next = cutoffReached ? null : this.resolveNextHref("audit_log", res.body._links?.next?.href);
563
+ const filtered = items.filter(
564
+ (e) => Number.isFinite(e.date) ? e.date >= sinceMs : true
565
+ );
566
+ return { items: filtered, next };
567
+ }
568
+ // -------------------------------------------------------------------------
569
+ // Writers
570
+ // -------------------------------------------------------------------------
571
+ async writeProjects(storage, projects) {
572
+ for (const p of projects) {
573
+ await storage.entity({
574
+ type: "launchdarkly_project",
575
+ id: p.key,
576
+ attributes: {
577
+ key: p.key,
578
+ name: p.name,
579
+ tags: p.tags ?? []
580
+ },
581
+ updated_at: Date.now()
582
+ });
583
+ }
584
+ }
585
+ async writeFlags(storage, items) {
586
+ for (const { projectKey, flags } of items) {
587
+ for (const flag of flags) {
588
+ const creationMs = flag.creationDate !== void 0 && Number.isFinite(flag.creationDate) ? flag.creationDate : null;
589
+ const envSummary = {};
590
+ let lastModifiedMax = null;
591
+ if (flag.environments) {
592
+ for (const [envKey, env] of Object.entries(flag.environments)) {
593
+ envSummary[envKey] = {
594
+ on: env.on ?? false,
595
+ archived: env.archived ?? false,
596
+ lastModified: env.lastModified ?? null
597
+ };
598
+ if (env.lastModified !== void 0 && Number.isFinite(env.lastModified) && (lastModifiedMax === null || env.lastModified > lastModifiedMax)) {
599
+ lastModifiedMax = env.lastModified;
600
+ }
601
+ }
602
+ }
603
+ const updatedAt = lastModifiedMax ?? creationMs ?? Date.now();
604
+ await storage.entity({
605
+ type: "launchdarkly_feature_flag",
606
+ id: `${projectKey}:${flag.key}`,
607
+ attributes: {
608
+ projectKey,
609
+ key: flag.key,
610
+ name: flag.name,
611
+ description: flag.description ?? null,
612
+ kind: flag.kind,
613
+ archived: flag.archived ?? false,
614
+ tags: flag.tags ?? [],
615
+ variationCount: flag.variations?.length ?? 0,
616
+ environments: envSummary,
617
+ creationDate: creationMs
618
+ },
619
+ updated_at: updatedAt
620
+ });
621
+ }
622
+ }
623
+ }
624
+ async writeAuditEntries(storage, entries) {
625
+ for (const e of entries) {
626
+ if (!Number.isFinite(e.date)) {
627
+ continue;
628
+ }
629
+ const attributes = {
630
+ auditId: e._id,
631
+ kind: e.kind ?? null,
632
+ titleVerb: e.titleVerb ?? null,
633
+ title: e.title ?? null,
634
+ description: e.description ?? e.shortDescription ?? null,
635
+ comment: e.comment ?? null,
636
+ memberEmail: e.member?.email ?? null,
637
+ memberName: e.member?.firstName || e.member?.lastName ? `${e.member?.firstName ?? ""} ${e.member?.lastName ?? ""}`.trim() : null,
638
+ targetName: e.target?.name ?? null,
639
+ targetResources: e.target?.resources ?? []
640
+ };
641
+ await storage.event({
642
+ name: "launchdarkly_flag_event",
643
+ start_ts: e.date,
644
+ end_ts: null,
645
+ attributes
646
+ });
647
+ }
648
+ }
649
+ // -------------------------------------------------------------------------
650
+ // sync
651
+ // -------------------------------------------------------------------------
652
+ async sync(options, storage, signal) {
653
+ this.discoveredProjectKeys = null;
654
+ this.discoveredProjectKeysComplete = false;
655
+ const cursor = this.resolveCursor(options.cursor);
656
+ const isFull = options.mode === "full";
657
+ const phases = this.activePhases();
658
+ return paginateChunked({
659
+ phases,
660
+ cursor,
661
+ signal,
662
+ logger: this.logger,
663
+ fetchPage: async (phase, page, sig) => {
664
+ switch (phase) {
665
+ case "projects":
666
+ return this.fetchProjectsPage(page, sig);
667
+ case "feature_flags":
668
+ return this.fetchFlagsPage(page, sig);
669
+ case "audit_log":
670
+ return this.fetchAuditLogPage(page, options, sig);
671
+ }
672
+ },
673
+ writeBatch: async (phase, items, page) => {
674
+ if (isFull && page === null) {
675
+ switch (phase) {
676
+ case "projects":
677
+ if (this.isResourceEnabled("projects")) {
678
+ await storage.entities([], {
679
+ types: ["launchdarkly_project"]
680
+ });
681
+ }
682
+ break;
683
+ case "feature_flags":
684
+ if (this.isResourceEnabled("feature_flags")) {
685
+ await storage.entities([], {
686
+ types: ["launchdarkly_feature_flag"]
687
+ });
688
+ }
689
+ break;
690
+ case "audit_log":
691
+ if (this.isResourceEnabled("flag_events")) {
692
+ await storage.events([], {
693
+ names: ["launchdarkly_flag_event"]
694
+ });
695
+ }
696
+ break;
697
+ }
698
+ }
699
+ switch (phase) {
700
+ case "projects":
701
+ if (!this.isResourceEnabled("projects")) {
702
+ return;
703
+ }
704
+ return this.writeProjects(storage, items);
705
+ case "feature_flags":
706
+ if (!this.isResourceEnabled("feature_flags")) {
707
+ return;
708
+ }
709
+ return this.writeFlags(storage, items);
710
+ case "audit_log":
711
+ if (!this.isResourceEnabled("flag_events")) {
712
+ return;
713
+ }
714
+ return this.writeAuditEntries(storage, items);
715
+ }
716
+ }
717
+ });
718
+ }
719
+ };
720
+
721
+ // src/index.ts
722
+ var index_default = LaunchDarklyConnector;
723
+ export {
724
+ LaunchDarklyConnector,
725
+ configFields,
726
+ index_default as default,
727
+ doc,
728
+ id,
729
+ launchdarklyResources as resources
730
+ };
731
+ //# sourceMappingURL=index.js.map