@eduardbar/drift 1.2.0 → 1.4.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 (195) hide show
  1. package/.gga +50 -0
  2. package/.github/actions/drift-review/README.md +60 -0
  3. package/.github/actions/drift-review/action.yml +131 -0
  4. package/.github/actions/drift-scan/README.md +28 -32
  5. package/.github/actions/drift-scan/action.yml +78 -14
  6. package/.github/workflows/publish-vscode.yml +3 -3
  7. package/.github/workflows/publish.yml +3 -3
  8. package/.github/workflows/review-pr.yml +94 -9
  9. package/AGENTS.md +75 -245
  10. package/CHANGELOG.md +28 -0
  11. package/README.md +308 -51
  12. package/ROADMAP.md +6 -5
  13. package/dist/analyzer.d.ts +2 -2
  14. package/dist/analyzer.js +420 -159
  15. package/dist/benchmark.d.ts +2 -0
  16. package/dist/benchmark.js +204 -0
  17. package/dist/cli.js +693 -67
  18. package/dist/config.js +16 -2
  19. package/dist/diff.js +66 -10
  20. package/dist/doctor.d.ts +5 -0
  21. package/dist/doctor.js +133 -0
  22. package/dist/format.d.ts +17 -0
  23. package/dist/format.js +45 -0
  24. package/dist/git.js +12 -0
  25. package/dist/guard-types.d.ts +57 -0
  26. package/dist/guard-types.js +2 -0
  27. package/dist/guard.d.ts +14 -0
  28. package/dist/guard.js +239 -0
  29. package/dist/index.d.ts +12 -3
  30. package/dist/index.js +6 -1
  31. package/dist/init.d.ts +15 -0
  32. package/dist/init.js +273 -0
  33. package/dist/map-cycles.d.ts +2 -0
  34. package/dist/map-cycles.js +34 -0
  35. package/dist/map-svg.d.ts +19 -0
  36. package/dist/map-svg.js +97 -0
  37. package/dist/map.js +78 -138
  38. package/dist/metrics.js +70 -55
  39. package/dist/output-metadata.d.ts +13 -0
  40. package/dist/output-metadata.js +17 -0
  41. package/dist/plugins-capabilities.d.ts +4 -0
  42. package/dist/plugins-capabilities.js +21 -0
  43. package/dist/plugins-messages.d.ts +10 -0
  44. package/dist/plugins-messages.js +16 -0
  45. package/dist/plugins-rules.d.ts +9 -0
  46. package/dist/plugins-rules.js +137 -0
  47. package/dist/plugins.d.ts +2 -1
  48. package/dist/plugins.js +80 -28
  49. package/dist/printer.js +4 -0
  50. package/dist/reporter-constants.d.ts +16 -0
  51. package/dist/reporter-constants.js +39 -0
  52. package/dist/reporter.d.ts +3 -3
  53. package/dist/reporter.js +35 -55
  54. package/dist/review.d.ts +2 -1
  55. package/dist/review.js +4 -3
  56. package/dist/rules/comments.js +2 -2
  57. package/dist/rules/complexity.js +2 -7
  58. package/dist/rules/nesting.js +3 -13
  59. package/dist/rules/phase0-basic.js +10 -10
  60. package/dist/rules/phase3-configurable.js +23 -15
  61. package/dist/rules/shared.d.ts +2 -0
  62. package/dist/rules/shared.js +27 -3
  63. package/dist/saas/constants.d.ts +15 -0
  64. package/dist/saas/constants.js +48 -0
  65. package/dist/saas/dashboard.d.ts +8 -0
  66. package/dist/saas/dashboard.js +132 -0
  67. package/dist/saas/errors.d.ts +19 -0
  68. package/dist/saas/errors.js +37 -0
  69. package/dist/saas/helpers.d.ts +21 -0
  70. package/dist/saas/helpers.js +110 -0
  71. package/dist/saas/ingest.d.ts +3 -0
  72. package/dist/saas/ingest.js +249 -0
  73. package/dist/saas/organization.d.ts +5 -0
  74. package/dist/saas/organization.js +82 -0
  75. package/dist/saas/plan-change.d.ts +10 -0
  76. package/dist/saas/plan-change.js +15 -0
  77. package/dist/saas/store.d.ts +21 -0
  78. package/dist/saas/store.js +159 -0
  79. package/dist/saas/types.d.ts +191 -0
  80. package/dist/saas/types.js +2 -0
  81. package/dist/saas.d.ts +8 -82
  82. package/dist/saas.js +7 -320
  83. package/dist/sarif.d.ts +74 -0
  84. package/dist/sarif.js +122 -0
  85. package/dist/trust-advanced.d.ts +14 -0
  86. package/dist/trust-advanced.js +65 -0
  87. package/dist/trust-kpi-fs.d.ts +3 -0
  88. package/dist/trust-kpi-fs.js +141 -0
  89. package/dist/trust-kpi-parse.d.ts +7 -0
  90. package/dist/trust-kpi-parse.js +186 -0
  91. package/dist/trust-kpi-types.d.ts +16 -0
  92. package/dist/trust-kpi-types.js +2 -0
  93. package/dist/trust-kpi.d.ts +7 -0
  94. package/dist/trust-kpi.js +185 -0
  95. package/dist/trust-policy.d.ts +32 -0
  96. package/dist/trust-policy.js +160 -0
  97. package/dist/trust-render.d.ts +9 -0
  98. package/dist/trust-render.js +54 -0
  99. package/dist/trust-scoring.d.ts +9 -0
  100. package/dist/trust-scoring.js +208 -0
  101. package/dist/trust.d.ts +37 -0
  102. package/dist/trust.js +168 -0
  103. package/dist/types/app.d.ts +30 -0
  104. package/dist/types/app.js +2 -0
  105. package/dist/types/config.d.ts +25 -0
  106. package/dist/types/config.js +2 -0
  107. package/dist/types/core.d.ts +100 -0
  108. package/dist/types/core.js +2 -0
  109. package/dist/types/diff.d.ts +55 -0
  110. package/dist/types/diff.js +2 -0
  111. package/dist/types/plugin.d.ts +41 -0
  112. package/dist/types/plugin.js +2 -0
  113. package/dist/types/trust.d.ts +120 -0
  114. package/dist/types/trust.js +2 -0
  115. package/dist/types.d.ts +8 -211
  116. package/docs/PRD.md +187 -109
  117. package/docs/plugin-contract.md +61 -0
  118. package/docs/release-notes-draft.md +40 -0
  119. package/docs/rules-catalog.md +49 -0
  120. package/docs/trust-core-release-checklist.md +87 -0
  121. package/package.json +6 -3
  122. package/packages/vscode-drift/src/code-actions.ts +1 -1
  123. package/schemas/drift-ai-output.v1.json +162 -0
  124. package/schemas/drift-report.v1.json +151 -0
  125. package/schemas/drift-trust.v1.json +131 -0
  126. package/scripts/smoke-repo.mjs +394 -0
  127. package/src/analyzer.ts +484 -155
  128. package/src/benchmark.ts +266 -0
  129. package/src/cli.ts +840 -85
  130. package/src/config.ts +19 -2
  131. package/src/diff.ts +84 -10
  132. package/src/doctor.ts +173 -0
  133. package/src/format.ts +81 -0
  134. package/src/git.ts +16 -0
  135. package/src/guard-types.ts +64 -0
  136. package/src/guard.ts +324 -0
  137. package/src/index.ts +83 -0
  138. package/src/init.ts +298 -0
  139. package/src/map-cycles.ts +38 -0
  140. package/src/map-svg.ts +124 -0
  141. package/src/map.ts +111 -142
  142. package/src/metrics.ts +78 -59
  143. package/src/output-metadata.ts +30 -0
  144. package/src/plugins-capabilities.ts +36 -0
  145. package/src/plugins-messages.ts +35 -0
  146. package/src/plugins-rules.ts +296 -0
  147. package/src/plugins.ts +148 -27
  148. package/src/printer.ts +4 -0
  149. package/src/reporter-constants.ts +46 -0
  150. package/src/reporter.ts +64 -65
  151. package/src/review.ts +6 -4
  152. package/src/rules/comments.ts +2 -2
  153. package/src/rules/complexity.ts +2 -7
  154. package/src/rules/nesting.ts +3 -13
  155. package/src/rules/phase0-basic.ts +11 -12
  156. package/src/rules/phase3-configurable.ts +39 -26
  157. package/src/rules/shared.ts +31 -3
  158. package/src/saas/constants.ts +56 -0
  159. package/src/saas/dashboard.ts +172 -0
  160. package/src/saas/errors.ts +45 -0
  161. package/src/saas/helpers.ts +140 -0
  162. package/src/saas/ingest.ts +278 -0
  163. package/src/saas/organization.ts +99 -0
  164. package/src/saas/plan-change.ts +19 -0
  165. package/src/saas/store.ts +172 -0
  166. package/src/saas/types.ts +216 -0
  167. package/src/saas.ts +49 -433
  168. package/src/sarif.ts +232 -0
  169. package/src/trust-advanced.ts +99 -0
  170. package/src/trust-kpi-fs.ts +169 -0
  171. package/src/trust-kpi-parse.ts +219 -0
  172. package/src/trust-kpi-types.ts +19 -0
  173. package/src/trust-kpi.ts +210 -0
  174. package/src/trust-policy.ts +246 -0
  175. package/src/trust-render.ts +61 -0
  176. package/src/trust-scoring.ts +231 -0
  177. package/src/trust.ts +260 -0
  178. package/src/types/app.ts +30 -0
  179. package/src/types/config.ts +27 -0
  180. package/src/types/core.ts +105 -0
  181. package/src/types/diff.ts +61 -0
  182. package/src/types/plugin.ts +46 -0
  183. package/src/types/trust.ts +134 -0
  184. package/src/types.ts +78 -238
  185. package/tests/cli-sarif.test.ts +92 -0
  186. package/tests/diff.test.ts +124 -0
  187. package/tests/format.test.ts +157 -0
  188. package/tests/new-features.test.ts +80 -1
  189. package/tests/phase1-init-doctor-guard.test.ts +199 -0
  190. package/tests/plugins.test.ts +219 -0
  191. package/tests/rules.test.ts +23 -1
  192. package/tests/saas-foundation.test.ts +358 -1
  193. package/tests/sarif.test.ts +160 -0
  194. package/tests/trust-kpi.test.ts +147 -0
  195. package/tests/trust.test.ts +602 -0
@@ -0,0 +1,15 @@
1
+ import type { SaasOperation, SaasPlan, SaasPolicy, SaasRole } from './types.js';
2
+ export declare const STORE_VERSION = 3;
3
+ export declare const ACTIVE_WINDOW_DAYS = 30;
4
+ export declare const DEFAULT_ORGANIZATION_ID = "default-org";
5
+ export declare const DASHBOARD_REPO_LIMIT = 15;
6
+ export declare const DASHBOARD_BAR_UNIT = 8;
7
+ export declare const DASHBOARD_BAR_MIN_WIDTH = 8;
8
+ export declare const VALID_ROLES: SaasRole[];
9
+ export declare const VALID_PLANS: SaasPlan[];
10
+ export declare const ROLE_PRIORITY: Record<SaasRole, number>;
11
+ export declare const REQUIRED_ROLE_BY_OPERATION: Record<SaasOperation, SaasRole>;
12
+ export declare const DEFAULT_SAAS_POLICY: SaasPolicy;
13
+ export declare function daysAgo(days: number): number;
14
+ export declare function createRandomId(prefix: string): string;
15
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1,48 @@
1
+ export const STORE_VERSION = 3;
2
+ export const ACTIVE_WINDOW_DAYS = 30;
3
+ export const DEFAULT_ORGANIZATION_ID = 'default-org';
4
+ const HOURS_PER_DAY = 24;
5
+ const MINUTES_PER_HOUR = 60;
6
+ const SECONDS_PER_MINUTE = 60;
7
+ const MILLISECONDS_PER_SECOND = 1000;
8
+ const RANDOM_ID_RADIX = 16;
9
+ const RANDOM_ID_START = 2;
10
+ const RANDOM_ID_END = 10;
11
+ export const DASHBOARD_REPO_LIMIT = 15;
12
+ export const DASHBOARD_BAR_UNIT = 8;
13
+ export const DASHBOARD_BAR_MIN_WIDTH = 8;
14
+ export const VALID_ROLES = ['owner', 'member', 'viewer'];
15
+ export const VALID_PLANS = ['free', 'sponsor', 'team', 'business'];
16
+ export const ROLE_PRIORITY = {
17
+ viewer: 1,
18
+ member: 2,
19
+ owner: 3,
20
+ };
21
+ export const REQUIRED_ROLE_BY_OPERATION = {
22
+ 'snapshot:write': 'member',
23
+ 'snapshot:read': 'viewer',
24
+ 'summary:read': 'viewer',
25
+ 'billing:write': 'owner',
26
+ 'billing:read': 'viewer',
27
+ };
28
+ export const DEFAULT_SAAS_POLICY = {
29
+ freeUserThreshold: 7500,
30
+ maxRunsPerWorkspacePerMonth: 500,
31
+ maxReposPerWorkspace: 20,
32
+ retentionDays: 90,
33
+ strictActorEnforcement: false,
34
+ maxWorkspacesPerOrganizationByPlan: {
35
+ free: 20,
36
+ sponsor: 50,
37
+ team: 200,
38
+ business: 1000,
39
+ },
40
+ };
41
+ export function daysAgo(days) {
42
+ const now = Date.now();
43
+ return now - days * HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MILLISECONDS_PER_SECOND;
44
+ }
45
+ export function createRandomId(prefix) {
46
+ return `${prefix}-${Math.random().toString(RANDOM_ID_RADIX).slice(RANDOM_ID_START, RANDOM_ID_END)}`;
47
+ }
48
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1,8 @@
1
+ import type { SaasPolicyOverrides, SaasQueryOptions, SaasSnapshot, SaasSummary } from './types.js';
2
+ export declare function listSaasSnapshots(options?: SaasQueryOptions): SaasSnapshot[];
3
+ export declare function getSaasSummary(options?: SaasQueryOptions): SaasSummary;
4
+ export declare function generateSaasDashboardHtml(options?: {
5
+ storeFile?: string;
6
+ policy?: SaasPolicyOverrides;
7
+ }): string;
8
+ //# sourceMappingURL=dashboard.d.ts.map
@@ -0,0 +1,132 @@
1
+ import { resolve } from 'node:path';
2
+ import { DASHBOARD_BAR_MIN_WIDTH, DASHBOARD_BAR_UNIT, DASHBOARD_REPO_LIMIT } from './constants.js';
3
+ import { assertPermissionInStore, defaultSaasStorePath, loadStoreInternal, saveStore } from './store.js';
4
+ import { DEFAULT_ORGANIZATION_ID, computeRunsPerMonth, computeUsersRegistered, escapeHtml, isRepoActive, isWorkspaceActive, matchesRepoScope, matchesTenantScope, matchesWorkspaceScope, } from './helpers.js';
5
+ function assertSummaryReadPermission(store, options) {
6
+ const shouldEnforceActorForScope = store.policy.strictActorEnforcement && Boolean(options?.organizationId || options?.workspaceId);
7
+ if (!options?.actorUserId && !shouldEnforceActorForScope)
8
+ return;
9
+ const organizationId = options?.organizationId ?? DEFAULT_ORGANIZATION_ID;
10
+ assertPermissionInStore(store, {
11
+ operation: 'summary:read',
12
+ organizationId,
13
+ workspaceId: options?.workspaceId,
14
+ actorUserId: options?.actorUserId,
15
+ });
16
+ }
17
+ function buildWorkspaceStats(store) {
18
+ return Object.values(store.workspaces)
19
+ .map((workspace) => {
20
+ const snapshots = store.snapshots.filter((snapshot) => snapshot.organizationId === workspace.organizationId && snapshot.workspaceId === workspace.id);
21
+ const runs = snapshots.length;
22
+ const avgScore = runs === 0 ? 0 : Math.round(snapshots.reduce((sum, snapshot) => sum + snapshot.totalScore, 0) / runs);
23
+ const lastRun = snapshots.sort((a, b) => b.createdAt.localeCompare(a.createdAt))[0]?.createdAt ?? 'n/a';
24
+ return {
25
+ organizationId: workspace.organizationId,
26
+ id: workspace.id,
27
+ runs,
28
+ avgScore,
29
+ lastRun,
30
+ };
31
+ })
32
+ .sort((a, b) => b.avgScore - a.avgScore);
33
+ }
34
+ function buildRepoStats(store) {
35
+ return Object.values(store.repos)
36
+ .map((repo) => {
37
+ const snapshots = store.snapshots.filter((snapshot) => snapshot.repoId === repo.id);
38
+ const runs = snapshots.length;
39
+ const avgScore = runs === 0 ? 0 : Math.round(snapshots.reduce((sum, snapshot) => sum + snapshot.totalScore, 0) / runs);
40
+ return {
41
+ workspaceId: repo.workspaceId,
42
+ name: repo.name,
43
+ runs,
44
+ avgScore,
45
+ };
46
+ })
47
+ .sort((a, b) => b.avgScore - a.avgScore)
48
+ .slice(0, DASHBOARD_REPO_LIMIT);
49
+ }
50
+ function buildRunsRows(summary) {
51
+ return Object.entries(summary.runsPerMonth)
52
+ .sort(([a], [b]) => a.localeCompare(b))
53
+ .map(([month, count]) => {
54
+ const width = Math.max(DASHBOARD_BAR_MIN_WIDTH, count * DASHBOARD_BAR_UNIT);
55
+ return `<tr><td>${escapeHtml(month)}</td><td>${count}</td><td><div class="bar" style="width:${width}px"></div></td></tr>`;
56
+ })
57
+ .join('');
58
+ }
59
+ function buildWorkspaceRows(workspaceStats) {
60
+ return workspaceStats
61
+ .map((workspace) => `<tr><td>${escapeHtml(workspace.organizationId)}</td><td>${escapeHtml(workspace.id)}</td><td>${workspace.runs}</td><td>${workspace.avgScore}</td><td>${escapeHtml(workspace.lastRun)}</td></tr>`)
62
+ .join('');
63
+ }
64
+ function buildRepoRows(repoStats) {
65
+ return repoStats
66
+ .map((repo) => `<tr><td>${escapeHtml(repo.workspaceId)}</td><td>${escapeHtml(repo.name)}</td><td>${repo.runs}</td><td>${repo.avgScore}</td></tr>`)
67
+ .join('');
68
+ }
69
+ function renderDashboardHtmlDocument(input) {
70
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>drift cloud dashboard</title><style>:root { color-scheme: light; } body { margin: 0; font-family: "Segoe UI", Arial, sans-serif; background: #f4f7fb; color: #0f172a; } main { max-width: 980px; margin: 0 auto; padding: 24px; } h1 { margin: 0 0 6px; } p.meta { margin: 0 0 20px; color: #475569; } .cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; margin-bottom: 18px; } .card { background: #ffffff; border-radius: 10px; padding: 14px; border: 1px solid #dbe3ef; } .card .label { font-size: 12px; color: #64748b; text-transform: uppercase; letter-spacing: 0.08em; } .card .value { font-size: 26px; font-weight: 700; margin-top: 4px; } table { width: 100%; border-collapse: collapse; margin-top: 10px; background: #ffffff; border: 1px solid #dbe3ef; border-radius: 10px; overflow: hidden; } th, td { padding: 10px; border-bottom: 1px solid #e2e8f0; text-align: left; font-size: 14px; } th { background: #eef2f9; } .section { margin-top: 18px; } .bar { height: 10px; background: linear-gradient(90deg, #0ea5e9, #22c55e); border-radius: 999px; } .pill { display: inline-block; border-radius: 999px; padding: 4px 10px; font-size: 12px; font-weight: 600; } .pill.free { background: #dcfce7; color: #166534; } .pill.paid { background: #fee2e2; color: #991b1b; }</style></head><body><main><h1>drift cloud dashboard</h1><p class="meta">Store: ${escapeHtml(input.storeFile)}</p><div class="cards"><div class="card"><div class="label">Plan Phase</div><div class="value"><span class="pill ${input.summary.phase}">${input.summary.phase.toUpperCase()}</span></div></div><div class="card"><div class="label">Users</div><div class="value">${input.summary.usersRegistered}</div></div><div class="card"><div class="label">Active Workspaces</div><div class="value">${input.summary.workspacesActive}</div></div><div class="card"><div class="label">Active Repos</div><div class="value">${input.summary.reposActive}</div></div><div class="card"><div class="label">Snapshots</div><div class="value">${input.summary.totalSnapshots}</div></div><div class="card"><div class="label">Free Seats Left</div><div class="value">${input.summary.freeUsersRemaining}</div></div></div><section class="section"><h2>Runs Per Month</h2><table><thead><tr><th>Month</th><th>Runs</th><th>Trend</th></tr></thead><tbody>${input.runsRows || '<tr><td colspan="3">No runs yet</td></tr>'}</tbody></table></section><section class="section"><h2>Workspace Hotspots</h2><table><thead><tr><th>Organization</th><th>Workspace</th><th>Runs</th><th>Avg Score</th><th>Last Run</th></tr></thead><tbody>${input.workspaceRows || '<tr><td colspan="5">No workspace data</td></tr>'}</tbody></table></section><section class="section"><h2>Repo Hotspots</h2><table><thead><tr><th>Workspace</th><th>Repo</th><th>Runs</th><th>Avg Score</th></tr></thead><tbody>${input.repoRows || '<tr><td colspan="4">No repo data</td></tr>'}</tbody></table></section></main></body></html>`;
71
+ }
72
+ export function listSaasSnapshots(options) {
73
+ const storeFile = resolve(options?.storeFile ?? defaultSaasStorePath());
74
+ const store = loadStoreInternal(storeFile, options?.policy);
75
+ const shouldEnforceActorForScope = store.policy.strictActorEnforcement && Boolean(options?.organizationId || options?.workspaceId);
76
+ if (options?.actorUserId || shouldEnforceActorForScope) {
77
+ const organizationId = options?.organizationId ?? DEFAULT_ORGANIZATION_ID;
78
+ assertPermissionInStore(store, {
79
+ operation: 'snapshot:read',
80
+ organizationId,
81
+ workspaceId: options?.workspaceId,
82
+ actorUserId: options?.actorUserId,
83
+ });
84
+ }
85
+ saveStore(storeFile, store);
86
+ return store.snapshots
87
+ .filter((snapshot) => matchesTenantScope(snapshot, options))
88
+ .sort((a, b) => b.createdAt.localeCompare(a.createdAt));
89
+ }
90
+ export function getSaasSummary(options) {
91
+ const storeFile = resolve(options?.storeFile ?? defaultSaasStorePath());
92
+ const store = loadStoreInternal(storeFile, options?.policy);
93
+ assertSummaryReadPermission(store, options);
94
+ saveStore(storeFile, store);
95
+ const scopedSnapshots = store.snapshots.filter((snapshot) => matchesTenantScope(snapshot, options));
96
+ const scopedWorkspaces = Object.values(store.workspaces).filter((workspace) => matchesWorkspaceScope(workspace, options));
97
+ const scopedRepos = Object.values(store.repos).filter((repo) => matchesRepoScope(repo, options));
98
+ const usersRegistered = computeUsersRegistered(store, scopedSnapshots, options);
99
+ const workspacesActive = scopedWorkspaces.filter((workspace) => isWorkspaceActive(workspace)).length;
100
+ const reposActive = scopedRepos.filter((repo) => isRepoActive(repo)).length;
101
+ const runsPerMonth = computeRunsPerMonth(scopedSnapshots);
102
+ const thresholdReached = usersRegistered >= store.policy.freeUserThreshold;
103
+ return {
104
+ policy: store.policy,
105
+ usersRegistered,
106
+ workspacesActive,
107
+ reposActive,
108
+ runsPerMonth,
109
+ totalSnapshots: scopedSnapshots.length,
110
+ phase: thresholdReached ? 'paid' : 'free',
111
+ thresholdReached,
112
+ freeUsersRemaining: Math.max(0, store.policy.freeUserThreshold - usersRegistered),
113
+ };
114
+ }
115
+ export function generateSaasDashboardHtml(options) {
116
+ const storeFile = resolve(options?.storeFile ?? defaultSaasStorePath());
117
+ const store = loadStoreInternal(storeFile, options?.policy);
118
+ const summary = getSaasSummary(options);
119
+ const workspaceStats = buildWorkspaceStats(store);
120
+ const repoStats = buildRepoStats(store);
121
+ const runsRows = buildRunsRows(summary);
122
+ const workspaceRows = buildWorkspaceRows(workspaceStats);
123
+ const repoRows = buildRepoRows(repoStats);
124
+ return renderDashboardHtmlDocument({
125
+ storeFile,
126
+ summary,
127
+ runsRows,
128
+ workspaceRows,
129
+ repoRows,
130
+ });
131
+ }
132
+ //# sourceMappingURL=dashboard.js.map
@@ -0,0 +1,19 @@
1
+ import type { SaasOperation, SaasPermissionContext, SaasRole } from './types.js';
2
+ export declare class SaasPermissionError extends Error {
3
+ readonly code = "SAAS_PERMISSION_DENIED";
4
+ readonly operation: SaasOperation;
5
+ readonly organizationId: string;
6
+ readonly workspaceId?: string;
7
+ readonly actorUserId?: string;
8
+ readonly requiredRole: SaasRole;
9
+ readonly actorRole?: SaasRole;
10
+ constructor(context: SaasPermissionContext, requiredRole: SaasRole, actorRole?: SaasRole);
11
+ }
12
+ export declare class SaasActorRequiredError extends Error {
13
+ readonly code = "SAAS_ACTOR_REQUIRED";
14
+ readonly operation: SaasOperation;
15
+ readonly organizationId: string;
16
+ readonly workspaceId?: string;
17
+ constructor(context: SaasPermissionContext);
18
+ }
19
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1,37 @@
1
+ export class SaasPermissionError extends Error {
2
+ code = 'SAAS_PERMISSION_DENIED';
3
+ operation;
4
+ organizationId;
5
+ workspaceId;
6
+ actorUserId;
7
+ requiredRole;
8
+ actorRole;
9
+ constructor(context, requiredRole, actorRole) {
10
+ const actor = context.actorUserId ?? 'unknown-actor';
11
+ const workspaceSuffix = context.workspaceId ? ` workspace='${context.workspaceId}'` : '';
12
+ const actualRole = actorRole ?? 'none';
13
+ super(`Permission denied for operation '${context.operation}'. actor='${actor}' organization='${context.organizationId}'${workspaceSuffix} requiredRole='${requiredRole}' actualRole='${actualRole}'.`);
14
+ this.name = 'SaasPermissionError';
15
+ this.operation = context.operation;
16
+ this.organizationId = context.organizationId;
17
+ this.workspaceId = context.workspaceId;
18
+ this.actorUserId = context.actorUserId;
19
+ this.requiredRole = requiredRole;
20
+ this.actorRole = actorRole;
21
+ }
22
+ }
23
+ export class SaasActorRequiredError extends Error {
24
+ code = 'SAAS_ACTOR_REQUIRED';
25
+ operation;
26
+ organizationId;
27
+ workspaceId;
28
+ constructor(context) {
29
+ const workspaceSuffix = context.workspaceId ? ` workspace='${context.workspaceId}'` : '';
30
+ super(`Actor is required for operation '${context.operation}'. organization='${context.organizationId}'${workspaceSuffix}.`);
31
+ this.name = 'SaasActorRequiredError';
32
+ this.operation = context.operation;
33
+ this.organizationId = context.organizationId;
34
+ this.workspaceId = context.workspaceId;
35
+ }
36
+ }
37
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1,21 @@
1
+ import type { IngestOptions, SaasPlan, SaasPolicy, SaasPolicyInput, SaasPolicyOverrides, SaasQueryOptions, SaasRepo, SaasRole, SaasSnapshot, SaasStore, SaasWorkspace, ScopedIdentity } from './types.js';
2
+ import { DEFAULT_ORGANIZATION_ID } from './constants.js';
3
+ export declare function resolveSaasPolicy(policy?: SaasPolicyInput): SaasPolicy;
4
+ export declare function normalizePlan(plan?: string): SaasPlan;
5
+ export declare function normalizeRole(role?: string): SaasRole;
6
+ export declare function hasRoleAtLeast(role: SaasRole | undefined, requiredRole: SaasRole): boolean;
7
+ export declare function workspaceKey(organizationId: string, workspaceId: string): string;
8
+ export declare function membershipKey(organizationId: string, workspaceId: string, userId: string): string;
9
+ export declare function monthKey(isoDate: string): string;
10
+ export declare function resolveScopedIdentity(options: IngestOptions): ScopedIdentity;
11
+ export declare function isWorkspaceActive(workspace: SaasWorkspace): boolean;
12
+ export declare function isRepoActive(repo: SaasRepo): boolean;
13
+ export declare function matchesTenantScope(snapshot: SaasSnapshot, options?: SaasQueryOptions): boolean;
14
+ export declare function matchesWorkspaceScope(workspace: SaasWorkspace, options?: SaasQueryOptions): boolean;
15
+ export declare function matchesRepoScope(repo: SaasRepo, options?: SaasQueryOptions): boolean;
16
+ export declare function computeRunsPerMonth(snapshots: SaasSnapshot[]): Record<string, number>;
17
+ export declare function computeUsersRegistered(store: SaasStore, snapshots: SaasSnapshot[], options?: SaasQueryOptions): number;
18
+ export declare function escapeHtml(value: string): string;
19
+ export declare function mergePolicy(policy: SaasPolicyOverrides | undefined, base: SaasStore['policy']): SaasPolicy;
20
+ export { DEFAULT_ORGANIZATION_ID };
21
+ //# sourceMappingURL=helpers.d.ts.map
@@ -0,0 +1,110 @@
1
+ import { ACTIVE_WINDOW_DAYS, DEFAULT_ORGANIZATION_ID, DEFAULT_SAAS_POLICY, ROLE_PRIORITY, VALID_PLANS, VALID_ROLES, daysAgo, } from './constants.js';
2
+ export function resolveSaasPolicy(policy) {
3
+ const customPlanLimits = (policy && 'maxWorkspacesPerOrganizationByPlan' in policy)
4
+ ? (policy.maxWorkspacesPerOrganizationByPlan ?? {})
5
+ : {};
6
+ return {
7
+ ...DEFAULT_SAAS_POLICY,
8
+ ...(policy ?? {}),
9
+ maxWorkspacesPerOrganizationByPlan: {
10
+ ...DEFAULT_SAAS_POLICY.maxWorkspacesPerOrganizationByPlan,
11
+ ...customPlanLimits,
12
+ },
13
+ };
14
+ }
15
+ export function normalizePlan(plan) {
16
+ if (!plan)
17
+ return 'free';
18
+ return VALID_PLANS.includes(plan) ? plan : 'free';
19
+ }
20
+ export function normalizeRole(role) {
21
+ if (!role)
22
+ return 'member';
23
+ return VALID_ROLES.includes(role) ? role : 'member';
24
+ }
25
+ export function hasRoleAtLeast(role, requiredRole) {
26
+ if (!role)
27
+ return false;
28
+ return ROLE_PRIORITY[role] >= ROLE_PRIORITY[requiredRole];
29
+ }
30
+ export function workspaceKey(organizationId, workspaceId) {
31
+ return `${organizationId}:${workspaceId}`;
32
+ }
33
+ function repoKey(organizationId, workspaceId, repoName) {
34
+ return `${workspaceKey(organizationId, workspaceId)}:${repoName}`;
35
+ }
36
+ export function membershipKey(organizationId, workspaceId, userId) {
37
+ return `${workspaceKey(organizationId, workspaceId)}:${userId}`;
38
+ }
39
+ export function monthKey(isoDate) {
40
+ const date = new Date(isoDate);
41
+ const month = String(date.getUTCMonth() + 1).padStart(2, '0');
42
+ return `${date.getUTCFullYear()}-${month}`;
43
+ }
44
+ export function resolveScopedIdentity(options) {
45
+ const organizationId = options.organizationId ?? DEFAULT_ORGANIZATION_ID;
46
+ const workspaceId = options.workspaceId;
47
+ const repoName = options.repoName ?? 'default';
48
+ return {
49
+ organizationId,
50
+ workspaceId,
51
+ workspaceKey: workspaceKey(organizationId, workspaceId),
52
+ repoName,
53
+ repoId: repoKey(organizationId, workspaceId, repoName),
54
+ };
55
+ }
56
+ export function isWorkspaceActive(workspace) {
57
+ return new Date(workspace.lastSeenAt).getTime() >= daysAgo(ACTIVE_WINDOW_DAYS);
58
+ }
59
+ export function isRepoActive(repo) {
60
+ return new Date(repo.lastSeenAt).getTime() >= daysAgo(ACTIVE_WINDOW_DAYS);
61
+ }
62
+ export function matchesTenantScope(snapshot, options) {
63
+ if (!options?.organizationId && !options?.workspaceId)
64
+ return true;
65
+ if (options.organizationId && snapshot.organizationId !== options.organizationId)
66
+ return false;
67
+ if (options.workspaceId && snapshot.workspaceId !== options.workspaceId)
68
+ return false;
69
+ return true;
70
+ }
71
+ export function matchesWorkspaceScope(workspace, options) {
72
+ if (options?.organizationId && workspace.organizationId !== options.organizationId)
73
+ return false;
74
+ if (options?.workspaceId && workspace.id !== options.workspaceId)
75
+ return false;
76
+ return true;
77
+ }
78
+ export function matchesRepoScope(repo, options) {
79
+ if (options?.organizationId && repo.organizationId !== options.organizationId)
80
+ return false;
81
+ if (options?.workspaceId && repo.workspaceId !== options.workspaceId)
82
+ return false;
83
+ return true;
84
+ }
85
+ export function computeRunsPerMonth(snapshots) {
86
+ const runsPerMonth = {};
87
+ for (const snapshot of snapshots) {
88
+ const key = monthKey(snapshot.createdAt);
89
+ runsPerMonth[key] = (runsPerMonth[key] ?? 0) + 1;
90
+ }
91
+ return runsPerMonth;
92
+ }
93
+ export function computeUsersRegistered(store, snapshots, options) {
94
+ if (!options?.organizationId && !options?.workspaceId)
95
+ return Object.keys(store.users).length;
96
+ return new Set(snapshots.map((snapshot) => snapshot.userId)).size;
97
+ }
98
+ export function escapeHtml(value) {
99
+ return value
100
+ .replaceAll('&', '&amp;')
101
+ .replaceAll('<', '&lt;')
102
+ .replaceAll('>', '&gt;')
103
+ .replaceAll('"', '&quot;')
104
+ .replaceAll("'", '&#39;');
105
+ }
106
+ export function mergePolicy(policy, base) {
107
+ return resolveSaasPolicy({ ...base, ...(policy ?? {}) });
108
+ }
109
+ export { DEFAULT_ORGANIZATION_ID };
110
+ //# sourceMappingURL=helpers.js.map
@@ -0,0 +1,3 @@
1
+ import type { DriftReportInput, IngestOptions, SaasSnapshot } from './types.js';
2
+ export declare function ingestSnapshotFromReport(report: DriftReportInput, options: IngestOptions): SaasSnapshot;
3
+ //# sourceMappingURL=ingest.d.ts.map
@@ -0,0 +1,249 @@
1
+ import { resolve } from 'node:path';
2
+ import { createRandomId } from './constants.js';
3
+ import { SaasActorRequiredError } from './errors.js';
4
+ import { applyRetentionPolicy, assertPermissionInStore, defaultSaasStorePath, loadStoreInternal, saveStore } from './store.js';
5
+ import { membershipKey, monthKey, normalizePlan, normalizeRole, resolveScopedIdentity } from './helpers.js';
6
+ import { appendPlanChange } from './plan-change.js';
7
+ function assertWorkspaceLimit(store, scoped, effectivePlan) {
8
+ const organization = store.organizations[scoped.organizationId];
9
+ const workspaceLimit = store.policy.maxWorkspacesPerOrganizationByPlan[effectivePlan];
10
+ const workspaceExists = Boolean(store.workspaces[scoped.workspaceKey]);
11
+ const workspaceCount = organization?.workspaceIds.length ?? 0;
12
+ if (!workspaceExists && workspaceCount >= workspaceLimit) {
13
+ throw new Error(`Organization '${scoped.organizationId}' on plan '${effectivePlan}' reached max workspaces (${workspaceLimit}).`);
14
+ }
15
+ }
16
+ function assertFreeThresholdLimit(store, userId) {
17
+ const usersRegistered = Object.keys(store.users).length;
18
+ const isFreePhase = usersRegistered < store.policy.freeUserThreshold;
19
+ if (!isFreePhase)
20
+ return;
21
+ if (!store.users[userId] && usersRegistered + 1 > store.policy.freeUserThreshold) {
22
+ throw new Error(`Free threshold reached (${store.policy.freeUserThreshold} users).`);
23
+ }
24
+ }
25
+ function assertRepoLimit(store, scoped) {
26
+ const workspace = store.workspaces[scoped.workspaceKey];
27
+ const repoExists = Boolean(store.repos[scoped.repoId]);
28
+ const repoCount = workspace?.repoIds.length ?? 0;
29
+ if (!repoExists && repoCount >= store.policy.maxReposPerWorkspace) {
30
+ throw new Error(`Workspace '${scoped.workspaceId}' reached max repos (${store.policy.maxReposPerWorkspace}).`);
31
+ }
32
+ }
33
+ function countWorkspaceRunsThisMonth(store, scoped, currentMonth) {
34
+ return store.snapshots.filter((snapshot) => {
35
+ return snapshot.organizationId === scoped.organizationId
36
+ && snapshot.workspaceId === scoped.workspaceId
37
+ && monthKey(snapshot.createdAt) === currentMonth;
38
+ }).length;
39
+ }
40
+ function assertGuardrails(store, options, nowIso) {
41
+ const scoped = resolveScopedIdentity(options);
42
+ const organization = store.organizations[scoped.organizationId];
43
+ const effectivePlan = normalizePlan(options.plan ?? organization?.plan);
44
+ assertWorkspaceLimit(store, scoped, effectivePlan);
45
+ assertFreeThresholdLimit(store, options.userId);
46
+ assertRepoLimit(store, scoped);
47
+ const currentMonth = monthKey(nowIso);
48
+ const runsThisMonth = countWorkspaceRunsThisMonth(store, scoped, currentMonth);
49
+ if (runsThisMonth >= store.policy.maxRunsPerWorkspacePerMonth) {
50
+ throw new Error(`Workspace '${scoped.workspaceId}' reached max monthly runs (${store.policy.maxRunsPerWorkspacePerMonth}).`);
51
+ }
52
+ }
53
+ function upsertUser(store, userId, nowIso) {
54
+ const user = store.users[userId];
55
+ if (user) {
56
+ user.lastSeenAt = nowIso;
57
+ return;
58
+ }
59
+ store.users[userId] = {
60
+ id: userId,
61
+ createdAt: nowIso,
62
+ lastSeenAt: nowIso,
63
+ };
64
+ }
65
+ function maybeUpdateOrganizationPlanFromIngest(context) {
66
+ const { store, scoped, requestedPlan, options, nowIso } = context;
67
+ const existingOrg = store.organizations[scoped.organizationId];
68
+ if (!existingOrg || !options.plan || existingOrg.plan === requestedPlan)
69
+ return;
70
+ if (options.actorUserId) {
71
+ assertPermissionInStore(store, {
72
+ operation: 'billing:write',
73
+ organizationId: scoped.organizationId,
74
+ actorUserId: options.actorUserId,
75
+ });
76
+ }
77
+ const previousPlan = existingOrg.plan;
78
+ existingOrg.plan = requestedPlan;
79
+ appendPlanChange(store, {
80
+ organizationId: scoped.organizationId,
81
+ fromPlan: previousPlan,
82
+ toPlan: requestedPlan,
83
+ changedAt: nowIso,
84
+ changedByUserId: options.actorUserId ?? options.userId,
85
+ reason: 'ingest-option-plan-change',
86
+ });
87
+ }
88
+ function ensureOrganizationForIngest(context) {
89
+ const { store, scoped, requestedPlan, nowIso } = context;
90
+ const existingOrg = store.organizations[scoped.organizationId];
91
+ if (existingOrg) {
92
+ existingOrg.lastSeenAt = nowIso;
93
+ maybeUpdateOrganizationPlanFromIngest(context);
94
+ return;
95
+ }
96
+ store.organizations[scoped.organizationId] = {
97
+ id: scoped.organizationId,
98
+ plan: requestedPlan,
99
+ createdAt: nowIso,
100
+ lastSeenAt: nowIso,
101
+ workspaceIds: [],
102
+ };
103
+ }
104
+ function upsertWorkspaceForIngest(store, scoped, userId, nowIso) {
105
+ const workspace = store.workspaces[scoped.workspaceKey];
106
+ if (workspace) {
107
+ workspace.lastSeenAt = nowIso;
108
+ if (!workspace.userIds.includes(userId))
109
+ workspace.userIds.push(userId);
110
+ return { wasCreated: false };
111
+ }
112
+ store.workspaces[scoped.workspaceKey] = {
113
+ id: scoped.workspaceId,
114
+ organizationId: scoped.organizationId,
115
+ createdAt: nowIso,
116
+ lastSeenAt: nowIso,
117
+ userIds: [userId],
118
+ repoIds: [],
119
+ };
120
+ const org = store.organizations[scoped.organizationId];
121
+ if (!org.workspaceIds.includes(scoped.workspaceId))
122
+ org.workspaceIds.push(scoped.workspaceId);
123
+ return { wasCreated: true };
124
+ }
125
+ function upsertMembershipForIngest(context, workspaceWasCreated) {
126
+ const { store, scoped, options, nowIso } = context;
127
+ const membershipId = membershipKey(scoped.organizationId, scoped.workspaceId, options.userId);
128
+ const membership = store.memberships[membershipId];
129
+ let role = normalizeRole(options.role);
130
+ if (!membership && workspaceWasCreated)
131
+ role = 'owner';
132
+ if (membership) {
133
+ membership.lastSeenAt = nowIso;
134
+ if (options.role)
135
+ membership.role = normalizeRole(options.role);
136
+ return membership.role;
137
+ }
138
+ store.memberships[membershipId] = {
139
+ id: membershipId,
140
+ organizationId: scoped.organizationId,
141
+ workspaceId: scoped.workspaceId,
142
+ userId: options.userId,
143
+ role,
144
+ createdAt: nowIso,
145
+ lastSeenAt: nowIso,
146
+ };
147
+ return role;
148
+ }
149
+ function upsertRepoForIngest(store, scoped, nowIso) {
150
+ const repo = store.repos[scoped.repoId];
151
+ if (repo) {
152
+ repo.lastSeenAt = nowIso;
153
+ return;
154
+ }
155
+ store.repos[scoped.repoId] = {
156
+ id: scoped.repoId,
157
+ organizationId: scoped.organizationId,
158
+ workspaceId: scoped.workspaceId,
159
+ name: scoped.repoName,
160
+ createdAt: nowIso,
161
+ lastSeenAt: nowIso,
162
+ };
163
+ const workspace = store.workspaces[scoped.workspaceKey];
164
+ if (!workspace.repoIds.includes(scoped.repoId))
165
+ workspace.repoIds.push(scoped.repoId);
166
+ }
167
+ function createSnapshotFromReport(report, context, role) {
168
+ const { store, scoped, options, nowIso, requestedPlan } = context;
169
+ return {
170
+ id: createRandomId(String(Date.now())),
171
+ createdAt: nowIso,
172
+ scannedAt: report.scannedAt,
173
+ organizationId: scoped.organizationId,
174
+ workspaceId: scoped.workspaceId,
175
+ userId: options.userId,
176
+ role,
177
+ plan: normalizePlan(store.organizations[scoped.organizationId]?.plan ?? requestedPlan),
178
+ repoId: scoped.repoId,
179
+ repoName: scoped.repoName,
180
+ targetPath: report.targetPath,
181
+ totalScore: report.totalScore,
182
+ totalIssues: report.totalIssues,
183
+ totalFiles: report.totalFiles,
184
+ summary: {
185
+ errors: report.summary.errors,
186
+ warnings: report.summary.warnings,
187
+ infos: report.summary.infos,
188
+ },
189
+ };
190
+ }
191
+ function assertIngestActorRequirement(options, scoped, store) {
192
+ if (!store.policy.strictActorEnforcement || options.actorUserId)
193
+ return;
194
+ throw new SaasActorRequiredError({
195
+ operation: 'snapshot:write',
196
+ organizationId: scoped.organizationId,
197
+ workspaceId: scoped.workspaceId,
198
+ });
199
+ }
200
+ function assertIngestPermissionForActor(store, scoped, actorUserId) {
201
+ if (!actorUserId)
202
+ return;
203
+ const workspaceExists = Boolean(store.workspaces[scoped.workspaceKey]);
204
+ const organizationExists = Boolean(store.organizations[scoped.organizationId]);
205
+ if (workspaceExists) {
206
+ assertPermissionInStore(store, {
207
+ operation: 'snapshot:write',
208
+ organizationId: scoped.organizationId,
209
+ workspaceId: scoped.workspaceId,
210
+ actorUserId,
211
+ });
212
+ return;
213
+ }
214
+ if (organizationExists) {
215
+ assertPermissionInStore(store, {
216
+ operation: 'billing:write',
217
+ organizationId: scoped.organizationId,
218
+ actorUserId,
219
+ });
220
+ }
221
+ }
222
+ export function ingestSnapshotFromReport(report, options) {
223
+ const storeFile = resolve(options.storeFile ?? defaultSaasStorePath());
224
+ const store = loadStoreInternal(storeFile, options.policy);
225
+ const nowIso = new Date().toISOString();
226
+ const scoped = resolveScopedIdentity(options);
227
+ const requestedPlan = normalizePlan(options.plan);
228
+ const context = {
229
+ store,
230
+ scoped,
231
+ options,
232
+ nowIso,
233
+ requestedPlan,
234
+ };
235
+ assertIngestActorRequirement(options, scoped, store);
236
+ assertIngestPermissionForActor(store, scoped, options.actorUserId);
237
+ assertGuardrails(store, options, nowIso);
238
+ upsertUser(store, options.userId, nowIso);
239
+ ensureOrganizationForIngest(context);
240
+ const workspaceState = upsertWorkspaceForIngest(store, scoped, options.userId, nowIso);
241
+ const role = upsertMembershipForIngest(context, workspaceState.wasCreated);
242
+ upsertRepoForIngest(store, scoped, nowIso);
243
+ const snapshot = createSnapshotFromReport(report, context, role);
244
+ store.snapshots.push(snapshot);
245
+ applyRetentionPolicy(store);
246
+ saveStore(storeFile, store);
247
+ return snapshot;
248
+ }
249
+ //# sourceMappingURL=ingest.js.map
@@ -0,0 +1,5 @@
1
+ import type { ChangeOrganizationPlanOptions, SaasOrganizationUsageSnapshot, SaasPlanChange, SaasPlanChangeQueryOptions, SaasUsageQueryOptions } from './types.js';
2
+ export declare function changeOrganizationPlan(options: ChangeOrganizationPlanOptions): SaasPlanChange;
3
+ export declare function listOrganizationPlanChanges(options: SaasPlanChangeQueryOptions): SaasPlanChange[];
4
+ export declare function getOrganizationUsageSnapshot(options: SaasUsageQueryOptions): SaasOrganizationUsageSnapshot;
5
+ //# sourceMappingURL=organization.d.ts.map