@elevasis/core 0.11.1 → 0.12.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 (38) hide show
  1. package/dist/index.d.ts +2 -2
  2. package/dist/index.js +10 -11
  3. package/dist/organization-model/index.d.ts +2 -2
  4. package/dist/organization-model/index.js +10 -11
  5. package/dist/test-utils/index.d.ts +10 -3
  6. package/dist/test-utils/index.js +6 -6
  7. package/package.json +1 -1
  8. package/src/__tests__/template-core-compatibility.test.ts +6 -15
  9. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +27 -270
  10. package/src/auth/multi-tenancy/credentials/server/encryption.ts +83 -39
  11. package/src/auth/multi-tenancy/credentials/server/kek-loader.ts +47 -0
  12. package/src/auth/multi-tenancy/index.ts +3 -0
  13. package/src/auth/multi-tenancy/invitations/api-schemas.ts +104 -107
  14. package/src/auth/multi-tenancy/memberships/api-schemas.ts +6 -5
  15. package/src/auth/multi-tenancy/memberships/membership.ts +130 -138
  16. package/src/auth/multi-tenancy/role-management/api-schemas.ts +78 -0
  17. package/src/auth/multi-tenancy/role-management/index.ts +16 -0
  18. package/src/execution/engine/tools/integration/server/adapters/apify/__tests__/apify-run-actor.integration.test.ts +299 -293
  19. package/src/execution/engine/tools/integration/service.test.ts +214 -0
  20. package/src/execution/engine/tools/integration/service.ts +169 -161
  21. package/src/integrations/credentials/__tests__/api-schemas.test.ts +420 -496
  22. package/src/integrations/credentials/api-schemas.ts +127 -143
  23. package/src/integrations/webhook-endpoints/__tests__/api-schemas.test.ts +327 -318
  24. package/src/integrations/webhook-endpoints/api-schemas.ts +103 -102
  25. package/src/integrations/webhook-endpoints/types.ts +58 -51
  26. package/src/operations/activities/api-schemas.ts +80 -79
  27. package/src/operations/activities/types.ts +64 -63
  28. package/src/organization-model/contracts.ts +1 -1
  29. package/src/organization-model/defaults.ts +6 -6
  30. package/src/organization-model/domains/navigation.ts +38 -37
  31. package/src/organization-model/foundation.ts +2 -3
  32. package/src/organization-model/published.ts +3 -3
  33. package/src/platform/constants/versions.ts +1 -1
  34. package/src/reference/_generated/contracts.md +27 -270
  35. package/src/scaffold-registry/__tests__/index.test.ts +72 -7
  36. package/src/scaffold-registry/index.ts +159 -26
  37. package/src/server.ts +281 -272
  38. package/src/supabase/database.types.ts +7 -3
@@ -1,5 +1,15 @@
1
- import { describe, expect, it } from 'vitest'
2
- import { findMatchingEntries, findMissingDependentPaths, loadScaffoldRegistryFast } from '../index'
1
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
2
+ import { tmpdir } from 'node:os'
3
+ import path from 'node:path'
4
+ import { afterEach, describe, expect, it } from 'vitest'
5
+ import {
6
+ findEmptySourcePatterns,
7
+ findMatchingEntries,
8
+ findMissingDependentPaths,
9
+ loadScaffoldRegistryFast,
10
+ normalizeScaffoldPath,
11
+ scaffoldPathMatchesPattern
12
+ } from '../index'
3
13
  import type { ScaffoldRegistry, ScaffoldRegistryEntry } from '../schema'
4
14
 
5
15
  function makeEntry(id: string, sources: string[]): ScaffoldRegistryEntry {
@@ -20,6 +30,21 @@ function matchedIds(registry: ScaffoldRegistry, filePath: string): string[] {
20
30
  return findMatchingEntries(registry, filePath).map((e) => e.id)
21
31
  }
22
32
 
33
+ let tempRoots: string[] = []
34
+
35
+ function makeTempRoot(): string {
36
+ const root = mkdtempSync(path.join(tmpdir(), 'scaffold-registry-'))
37
+ tempRoots.push(root)
38
+ return root
39
+ }
40
+
41
+ afterEach(() => {
42
+ for (const root of tempRoots) {
43
+ rmSync(root, { recursive: true, force: true })
44
+ }
45
+ tempRoots = []
46
+ })
47
+
23
48
  describe('loadScaffoldRegistryFast', () => {
24
49
  it('preserves external sync metadata from the compiled registry', () => {
25
50
  const registry = loadScaffoldRegistryFast()
@@ -89,6 +114,10 @@ describe('findMissingDependentPaths', () => {
89
114
  })
90
115
 
91
116
  describe('findMatchingEntries (pattern matching)', () => {
117
+ it('normalizes Windows and relative paths', () => {
118
+ expect(normalizeScaffoldPath('.\\packages\\core\\src\\')).toBe('packages/core/src')
119
+ })
120
+
92
121
  it('matches exact paths', () => {
93
122
  const reg = registryFromEntries([makeEntry('exact', ['packages/core/src/index.ts'])])
94
123
  expect(matchedIds(reg, 'packages/core/src/index.ts')).toEqual(['exact'])
@@ -110,12 +139,11 @@ describe('findMatchingEntries (pattern matching)', () => {
110
139
  expect(matchedIds(reg, 'packages/core/nested/package.json')).toEqual([])
111
140
  })
112
141
 
113
- it('current matcher does NOT support mixed **/*.ext patterns (Step 3 upgrade)', () => {
114
- // The simple matcher's `/*` suffix branch only handles a single trailing
115
- // wildcard. Patterns like `**/*.ts` fall through and effectively miss real
116
- // files. Locked in to flag any change when full micromatch lands.
142
+ it('matches mixed **/*.ext patterns at direct-child and nested depths', () => {
117
143
  const reg = registryFromEntries([makeEntry('ext', ['apps/api/src/**/*.ts'])])
118
- expect(matchedIds(reg, 'apps/api/src/foo.ts')).toEqual([])
144
+ expect(matchedIds(reg, 'apps/api/src/foo.ts')).toEqual(['ext'])
145
+ expect(matchedIds(reg, 'apps/api/src/nested/deeply/foo.ts')).toEqual(['ext'])
146
+ expect(matchedIds(reg, 'apps/api/src/foo.tsx')).toEqual([])
119
147
  })
120
148
 
121
149
  it('normalizes Windows backslashes to forward slashes', () => {
@@ -138,4 +166,41 @@ describe('findMatchingEntries (pattern matching)', () => {
138
166
  const reg = registryFromEntries([makeEntry('a', ['packages/core/src/**'])])
139
167
  expect(matchedIds(reg, 'apps/api/src/foo.ts')).toEqual([])
140
168
  })
169
+
170
+ it('exposes the same matcher for direct consumers', () => {
171
+ expect(scaffoldPathMatchesPattern('packages/core/src/foo.test.ts', 'packages/core/src/**/*.ts')).toBe(true)
172
+ expect(scaffoldPathMatchesPattern('packages/core/src/foo.test.ts', 'packages/ui/src/**/*.ts')).toBe(false)
173
+ })
174
+ })
175
+
176
+ describe('findEmptySourcePatterns', () => {
177
+ it('flags source patterns that do not match any file or directory', () => {
178
+ const root = makeTempRoot()
179
+ mkdirSync(path.join(root, 'packages', 'core', 'src'), { recursive: true })
180
+ writeFileSync(path.join(root, 'packages', 'core', 'src', 'index.ts'), 'export {}\n', 'utf8')
181
+
182
+ const registry = registryFromEntries([
183
+ makeEntry('ok-exact', ['packages/core/src/index.ts']),
184
+ makeEntry('ok-glob', ['packages/core/src/**/*.ts']),
185
+ makeEntry('empty', ['packages/core/src/**/*.tsx'])
186
+ ])
187
+
188
+ expect(findEmptySourcePatterns(registry, root)).toEqual([
189
+ { entryId: 'empty', pattern: 'packages/core/src/**/*.tsx' }
190
+ ])
191
+ })
192
+
193
+ it('treats exact directories as non-empty source coverage', () => {
194
+ const root = makeTempRoot()
195
+ mkdirSync(path.join(root, 'external', '_template', '.claude', 'skills'), { recursive: true })
196
+
197
+ const registry = registryFromEntries([
198
+ makeEntry('dir', ['external/_template/.claude/skills']),
199
+ makeEntry('missing-dir', ['external/_template/.claude/commands'])
200
+ ])
201
+
202
+ expect(findEmptySourcePatterns(registry, root)).toEqual([
203
+ { entryId: 'missing-dir', pattern: 'external/_template/.claude/commands' }
204
+ ])
205
+ })
141
206
  })
@@ -1,4 +1,5 @@
1
- import { existsSync, readFileSync, writeFileSync } from 'node:fs'
1
+ import { createHash } from 'node:crypto'
2
+ import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
2
3
  import path from 'node:path'
3
4
  import { parse as parseYaml } from 'yaml'
4
5
  import { type ScaffoldRegistry, type ScaffoldRegistryEntry, ScaffoldRegistrySchema } from './schema'
@@ -86,15 +87,23 @@ export function loadScaffoldRegistry(): ScaffoldRegistry {
86
87
 
87
88
  const registry = result.data
88
89
 
89
- // Drift check: if compiled JSON exists, verify entry count matches
90
+ // Drift check: if compiled JSON exists, verify the full normalized content matches.
90
91
  const jsonPath = path.join(root, JSON_FILENAME)
91
92
  try {
92
93
  const compiledRaw = readFileSync(jsonPath, 'utf-8')
93
- const compiled = JSON.parse(compiledRaw) as { entries?: unknown[] }
94
- if (compiled.entries?.length !== registry.entries.length) {
94
+ const compiledParsed = JSON.parse(compiledRaw) as unknown
95
+ const compiledResult = ScaffoldRegistrySchema.safeParse(compiledParsed)
96
+ if (!compiledResult.success) {
97
+ const issues = compiledResult.error.issues.map((i) => ` [${i.path.join('.')}] ${i.message}`).join('\n')
98
+ throw new Error(`scaffold-registry: compiled JSON validation failed:\n${issues}`)
99
+ }
100
+
101
+ const yamlHash = stableRegistryHash(registry)
102
+ const compiledHash = stableRegistryHash(compiledResult.data)
103
+ if (compiledHash !== yamlHash) {
95
104
  throw new Error(
96
105
  `scaffold-registry: compiled JSON is out of sync with YAML ` +
97
- `(YAML has ${registry.entries.length} entries, JSON has ${compiled.entries?.length ?? 0}). ` +
106
+ `(YAML hash ${yamlHash.slice(0, 12)}, JSON hash ${compiledHash.slice(0, 12)}). ` +
98
107
  `Run compileScaffoldRegistry() to regenerate.`
99
108
  )
100
109
  }
@@ -131,6 +140,17 @@ export function compileScaffoldRegistry(): ScaffoldRegistry {
131
140
  )
132
141
  }
133
142
 
143
+ const emptySources = findEmptySourcePatterns(registry, root)
144
+ if (emptySources.length > 0) {
145
+ const formatted = emptySources
146
+ .map((source) => ` [${source.entryId}] ${source.pattern}`)
147
+ .join('\n')
148
+ throw new Error(
149
+ `scaffold-registry: ${emptySources.length} source pattern(s) match no files or directories:\n${formatted}\n` +
150
+ `Fix the stale source glob, create the scaffold surface, or add explicit registry support for intentional empty patterns.`
151
+ )
152
+ }
153
+
134
154
  const jsonPath = path.join(root, JSON_FILENAME)
135
155
  writeFileSync(jsonPath, JSON.stringify(registry, null, 2) + '\n', 'utf-8')
136
156
 
@@ -166,6 +186,29 @@ export function findMissingDependentPaths(
166
186
  return missing
167
187
  }
168
188
 
189
+ /**
190
+ * Return source patterns that do not currently match any file or directory.
191
+ * Symbolic sources are skipped because they intentionally do not resolve to
192
+ * monorepo paths.
193
+ */
194
+ export function findEmptySourcePatterns(
195
+ registry: ScaffoldRegistry,
196
+ monorepoRootDir: string
197
+ ): Array<{ entryId: string; pattern: string }> {
198
+ const empty: Array<{ entryId: string; pattern: string }> = []
199
+
200
+ for (const entry of registry.entries) {
201
+ for (const sourcePattern of entry.sources) {
202
+ if (isSymbolicTarget(sourcePattern)) continue
203
+ if (!sourcePatternMatchesAnyPath(sourcePattern, monorepoRootDir)) {
204
+ empty.push({ entryId: entry.id, pattern: sourcePattern })
205
+ }
206
+ }
207
+ }
208
+
209
+ return empty
210
+ }
211
+
169
212
  // ---------------------------------------------------------------------------
170
213
  // Lookup helpers (used by hooks for fast path matching)
171
214
  // ---------------------------------------------------------------------------
@@ -197,7 +240,9 @@ export function loadScaffoldRegistryFast(): ScaffoldRegistry {
197
240
  * is implemented.
198
241
  */
199
242
  export function findMatchingEntries(registry: ScaffoldRegistry, filePath: string): ScaffoldRegistryEntry[] {
200
- return registry.entries.filter((entry) => entry.sources.some((pattern) => pathMatchesPattern(filePath, pattern)))
243
+ return registry.entries.filter((entry) =>
244
+ entry.sources.some((pattern) => scaffoldPathMatchesPattern(filePath, pattern))
245
+ )
201
246
  }
202
247
 
203
248
  // ---------------------------------------------------------------------------
@@ -221,6 +266,24 @@ function loadScaffoldRegistryNoSyncCheck(root: string): ScaffoldRegistry {
221
266
  return result.data
222
267
  }
223
268
 
269
+ export function normalizeScaffoldPath(p: string): string {
270
+ return p.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+$/, '')
271
+ }
272
+
273
+ export function scaffoldPathMatchesPattern(filePath: string, pattern: string): boolean {
274
+ const normalizedFile = normalizeScaffoldPath(filePath)
275
+ const normalizedPattern = normalizeScaffoldPath(pattern)
276
+
277
+ if (!normalizedFile || !normalizedPattern) return false
278
+ if (normalizedFile === normalizedPattern) return true
279
+
280
+ if (!isGlobPattern(normalizedPattern)) {
281
+ return normalizedFile.startsWith(normalizedPattern + '/')
282
+ }
283
+
284
+ return globToRegExp(normalizedPattern).test(normalizedFile)
285
+ }
286
+
224
287
  function isSymbolicTarget(p: string): boolean {
225
288
  return p.startsWith('docs:') || p.startsWith('autogen-target:')
226
289
  }
@@ -229,30 +292,100 @@ function isGlobPattern(p: string): boolean {
229
292
  return /[*?[]/.test(p)
230
293
  }
231
294
 
232
- /**
233
- * Lightweight pattern match: handles exact paths, directory prefixes, and
234
- * simple `*` wildcards at the end of a segment. Full micromatch lands in Step 3.
235
- */
236
- function pathMatchesPattern(filePath: string, pattern: string): boolean {
237
- // Normalize separators
238
- const normalizedFile = filePath.replace(/\\/g, '/')
239
- const normalizedPattern = pattern.replace(/\\/g, '/')
295
+ function stableRegistryHash(registry: ScaffoldRegistry): string {
296
+ return createHash('sha256').update(JSON.stringify(registry)).digest('hex')
297
+ }
240
298
 
241
- // Exact match
242
- if (normalizedFile === normalizedPattern) return true
299
+ function globToRegExp(pattern: string): RegExp {
300
+ const segments = pattern.split('/')
301
+ let regex = '^'
302
+
303
+ for (let index = 0; index < segments.length; index++) {
304
+ const segment = segments[index]
305
+ const isLast = index === segments.length - 1
306
+
307
+ if (segment === '**') {
308
+ regex += isLast ? '(?:.*)?' : '(?:[^/]+/)*'
309
+ continue
310
+ }
311
+
312
+ regex += segmentToRegExpSource(segment)
313
+ if (!isLast) regex += '/'
314
+ }
315
+
316
+ regex += '$'
317
+ return new RegExp(regex)
318
+ }
319
+
320
+ function segmentToRegExpSource(segment: string): string {
321
+ let source = ''
322
+ for (let index = 0; index < segment.length; index++) {
323
+ const char = segment[index]
324
+ if (char === '*') {
325
+ source += '[^/]*'
326
+ } else if (char === '?') {
327
+ source += '[^/]'
328
+ } else {
329
+ source += escapeRegExp(char)
330
+ }
331
+ }
332
+ return source
333
+ }
334
+
335
+ function escapeRegExp(value: string): string {
336
+ return value.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&')
337
+ }
243
338
 
244
- // If pattern ends with `/**` or `/*`, check prefix
245
- if (normalizedPattern.endsWith('/**') || normalizedPattern.endsWith('/*')) {
246
- const prefix = normalizedPattern.slice(0, normalizedPattern.lastIndexOf('/*'))
247
- return normalizedFile.startsWith(prefix + '/')
339
+ function sourcePatternMatchesAnyPath(sourcePattern: string, monorepoRootDir: string): boolean {
340
+ const normalizedPattern = normalizeScaffoldPath(sourcePattern)
341
+
342
+ if (!isGlobPattern(normalizedPattern)) {
343
+ return existsSync(path.join(monorepoRootDir, normalizedPattern))
344
+ }
345
+
346
+ const baseDir = findGlobBaseDirectory(normalizedPattern)
347
+ const absoluteBase = path.join(monorepoRootDir, baseDir)
348
+ if (!existsSync(absoluteBase)) return false
349
+
350
+ return walkUntilMatch(absoluteBase, monorepoRootDir, normalizedPattern)
351
+ }
352
+
353
+ function findGlobBaseDirectory(pattern: string): string {
354
+ const segments = pattern.split('/')
355
+ const baseSegments: string[] = []
356
+ for (const segment of segments) {
357
+ if (isGlobPattern(segment)) break
358
+ baseSegments.push(segment)
248
359
  }
360
+ return baseSegments.join('/')
361
+ }
362
+
363
+ function walkUntilMatch(currentPath: string, monorepoRootDir: string, pattern: string): boolean {
364
+ const rel = normalizeScaffoldPath(path.relative(monorepoRootDir, currentPath))
365
+ if (rel && scaffoldPathMatchesPattern(rel, pattern)) return true
249
366
 
250
- // If pattern contains `*`, convert to simple regex
251
- if (normalizedPattern.includes('*')) {
252
- const escaped = normalizedPattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^/]*')
253
- return new RegExp(`^${escaped}$`).test(normalizedFile)
367
+ let entries
368
+ try {
369
+ entries = readdirSync(currentPath, { withFileTypes: true })
370
+ } catch {
371
+ return false
372
+ }
373
+
374
+ for (const entry of entries) {
375
+ const absolutePath = path.join(currentPath, entry.name)
376
+ const relativePath = normalizeScaffoldPath(path.relative(monorepoRootDir, absolutePath))
377
+
378
+ if (scaffoldPathMatchesPattern(relativePath, pattern)) return true
379
+ if (entry.isDirectory()) {
380
+ try {
381
+ if (statSync(absolutePath).isDirectory() && walkUntilMatch(absolutePath, monorepoRootDir, pattern)) {
382
+ return true
383
+ }
384
+ } catch {
385
+ continue
386
+ }
387
+ }
254
388
  }
255
389
 
256
- // Directory prefix match (pattern is a directory)
257
- return normalizedFile.startsWith(normalizedPattern + '/')
390
+ return false
258
391
  }