meno-core 1.0.53 → 1.1.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/.claude/settings.local.json +1 -3
- package/bin/cli.ts +66 -261
- package/build-astro.ts +150 -128
- package/dist/bin/cli.js +42 -185
- package/dist/bin/cli.js.map +2 -2
- package/dist/chunks/chunk-2AR55GYH.js +42 -0
- package/dist/chunks/chunk-2AR55GYH.js.map +7 -0
- package/dist/chunks/{chunk-JGWFTO6P.js → chunk-2IIQK7T3.js} +2624 -1389
- package/dist/chunks/chunk-2IIQK7T3.js.map +7 -0
- package/dist/chunks/chunk-4ZRU52J2.js +169 -0
- package/dist/chunks/chunk-4ZRU52J2.js.map +7 -0
- package/dist/chunks/{chunk-UB44F4Z2.js → chunk-5X4HCN7J.js} +5 -7
- package/dist/chunks/chunk-5X4HCN7J.js.map +7 -0
- package/dist/chunks/{chunk-WQFG7PAH.js → chunk-7CCVOL5M.js} +15 -19
- package/dist/chunks/chunk-7CCVOL5M.js.map +7 -0
- package/dist/chunks/{chunk-YBLHKYFF.js → chunk-AR7LHIFY.js} +8 -2
- package/dist/chunks/chunk-AR7LHIFY.js.map +7 -0
- package/dist/chunks/{chunk-O3NAGJP4.js → chunk-NUP7H7D3.js} +135 -41
- package/dist/chunks/chunk-NUP7H7D3.js.map +7 -0
- package/dist/chunks/{chunk-X754AHS5.js → chunk-QWTQZHG3.js} +194 -90
- package/dist/chunks/chunk-QWTQZHG3.js.map +7 -0
- package/dist/chunks/{chunk-H3GJ4H2U.js → chunk-XTKNX4FW.js} +216 -87
- package/dist/chunks/chunk-XTKNX4FW.js.map +7 -0
- package/dist/chunks/{fs-JGINUXGL.js → fs-G3ANWEFP.js} +2 -2
- package/dist/lib/client/index.js +1003 -475
- package/dist/lib/client/index.js.map +3 -3
- package/dist/lib/server/index.js +14393 -6371
- package/dist/lib/server/index.js.map +4 -4
- package/dist/lib/shared/index.js +342 -101
- package/dist/lib/shared/index.js.map +4 -4
- package/dist/lib/shared/richtext/index.js +1 -1
- package/dist/lib/test-utils/index.js +64 -80
- package/dist/lib/test-utils/index.js.map +2 -2
- package/entries/client-router.tsx +10 -8
- package/lib/client/ClientInitializer.ts +8 -8
- package/lib/client/ErrorBoundary.test.tsx +246 -186
- package/lib/client/ErrorBoundary.tsx +183 -132
- package/lib/client/componentRegistry.test.ts +96 -108
- package/lib/client/componentRegistry.ts +1 -3
- package/lib/client/contexts/ThemeContext.tsx +3 -2
- package/lib/client/core/ComponentBuilder.test.ts +663 -560
- package/lib/client/core/ComponentBuilder.ts +427 -155
- package/lib/client/core/ComponentRenderer.test.tsx +2 -4
- package/lib/client/core/ComponentRenderer.tsx +46 -33
- package/lib/client/core/builders/embedBuilder.ts +242 -54
- package/lib/client/core/builders/linkBuilder.ts +72 -45
- package/lib/client/core/builders/linkNodeBuilder.ts +79 -54
- package/lib/client/core/builders/listBuilder.ts +155 -89
- package/lib/client/core/builders/localeListBuilder.ts +97 -62
- package/lib/client/core/builders/types.ts +5 -5
- package/lib/client/core/cmsTemplateProcessor.ts +8 -9
- package/lib/client/elementRegistry.ts +3 -3
- package/lib/client/fontFamiliesService.test.ts +2 -10
- package/lib/client/fontFamiliesService.ts +3 -3
- package/lib/client/hmr/HMRManager.tsx +47 -30
- package/lib/client/hmrCssReload.ts +49 -14
- package/lib/client/hmrWebSocket.ts +14 -19
- package/lib/client/hooks/useColorVariables.test.ts +21 -21
- package/lib/client/hooks/useColorVariables.ts +23 -18
- package/lib/client/hooks/usePropertyAutocomplete.ts +4 -6
- package/lib/client/hooks/useVariables.ts +6 -4
- package/lib/client/hydration/HydrationUtils.test.ts +24 -25
- package/lib/client/hydration/HydrationUtils.ts +3 -4
- package/lib/client/i18nConfigService.test.ts +2 -7
- package/lib/client/i18nConfigService.ts +2 -2
- package/lib/client/meno-filter/MenoFilter.test.ts +47 -49
- package/lib/client/meno-filter/MenoFilter.ts +14 -15
- package/lib/client/meno-filter/bindings.ts +18 -43
- package/lib/client/meno-filter/init.ts +4 -4
- package/lib/client/meno-filter/renderer.ts +30 -32
- package/lib/client/meno-filter/script.generated.ts +1 -3
- package/lib/client/meno-filter/ui.ts +4 -6
- package/lib/client/meno-filter/updates.ts +18 -23
- package/lib/client/meno-filter/utils.ts +1 -1
- package/lib/client/navigation.test.ts +174 -178
- package/lib/client/navigation.ts +0 -1
- package/lib/client/responsiveStyleResolver.knownTokens.test.ts +71 -0
- package/lib/client/responsiveStyleResolver.test.ts +234 -232
- package/lib/client/responsiveStyleResolver.ts +74 -20
- package/lib/client/routing/RouteLoader.test.ts +24 -25
- package/lib/client/routing/RouteLoader.ts +27 -35
- package/lib/client/routing/Router.tsx +210 -153
- package/lib/client/scripts/ScriptExecutor.test.ts +157 -158
- package/lib/client/scripts/ScriptExecutor.ts +18 -36
- package/lib/client/services/PrefetchService.test.ts +3 -3
- package/lib/client/services/PrefetchService.ts +23 -29
- package/lib/client/styleProcessor.test.ts +9 -9
- package/lib/client/styleProcessor.ts +24 -15
- package/lib/client/styles/StyleInjector.test.ts +122 -115
- package/lib/client/styles/StyleInjector.ts +10 -7
- package/lib/client/styles/UtilityClassCollector.ts +62 -28
- package/lib/client/styles/cspNonce.test.ts +2 -5
- package/lib/client/templateEngine.test.ts +554 -488
- package/lib/client/templateEngine.ts +203 -73
- package/lib/client/theme.test.ts +1 -1
- package/lib/client/theme.ts +0 -1
- package/lib/client/utils/toast.ts +0 -1
- package/lib/server/__integration__/api-routes.test.ts +8 -4
- package/lib/server/__integration__/cms-integration.test.ts +1 -4
- package/lib/server/__integration__/server-lifecycle.test.ts +4 -5
- package/lib/server/__integration__/ssr-rendering.test.ts +47 -37
- package/lib/server/__integration__/static-assets.test.ts +3 -3
- package/lib/server/__integration__/test-helpers.ts +87 -74
- package/lib/server/ab/generateFunctions.ts +12 -10
- package/lib/server/astro/cmsPageEmitter.ts +26 -42
- package/lib/server/astro/componentEmitter.ts +35 -43
- package/lib/server/astro/cssCollector.ts +10 -26
- package/lib/server/astro/nodeToAstro.test.ts +1754 -34
- package/lib/server/astro/nodeToAstro.ts +232 -216
- package/lib/server/astro/normalizeOrphanTemplateProps.test.ts +115 -119
- package/lib/server/astro/normalizeOrphanTemplateProps.ts +11 -18
- package/lib/server/astro/pageEmitter.ts +9 -13
- package/lib/server/astro/tailwindMapper.test.ts +10 -37
- package/lib/server/astro/tailwindMapper.ts +33 -40
- package/lib/server/astro/templateTransformer.ts +17 -20
- package/lib/server/createServer.ts +7 -19
- package/lib/server/cssGenerator.test.ts +35 -44
- package/lib/server/cssGenerator.ts +8 -19
- package/lib/server/fileWatcher.test.ts +164 -13
- package/lib/server/fileWatcher.ts +209 -124
- package/lib/server/index.ts +14 -17
- package/lib/server/jsonLoader.test.ts +39 -2
- package/lib/server/jsonLoader.ts +39 -34
- package/lib/server/middleware/cors.test.ts +21 -21
- package/lib/server/middleware/cors.ts +28 -4
- package/lib/server/middleware/errorHandler.test.ts +7 -5
- package/lib/server/middleware/errorHandler.ts +3 -8
- package/lib/server/middleware/index.ts +0 -1
- package/lib/server/middleware/logger.test.ts +10 -8
- package/lib/server/middleware/logger.ts +17 -29
- package/lib/server/migrateTemplates.ts +3 -3
- package/lib/server/pageCache.test.ts +76 -77
- package/lib/server/pageCache.ts +0 -1
- package/lib/server/projectContext.ts +5 -4
- package/lib/server/providers/fileSystemCMSProvider.test.ts +142 -99
- package/lib/server/providers/fileSystemCMSProvider.ts +62 -38
- package/lib/server/routes/api/cms.test.ts +26 -14
- package/lib/server/routes/api/cms.ts +9 -14
- package/lib/server/routes/api/colors.test.ts +2 -2
- package/lib/server/routes/api/components.ts +31 -35
- package/lib/server/routes/api/config.ts +3 -4
- package/lib/server/routes/api/core-routes.ts +49 -76
- package/lib/server/routes/api/enums.test.ts +2 -2
- package/lib/server/routes/api/functions.ts +9 -17
- package/lib/server/routes/api/index.ts +3 -6
- package/lib/server/routes/api/pages.ts +8 -11
- package/lib/server/routes/api/shared.test.ts +1 -1
- package/lib/server/routes/api/shared.ts +12 -9
- package/lib/server/routes/api/variables.test.ts +3 -5
- package/lib/server/routes/api/variables.ts +1 -1
- package/lib/server/routes/index.ts +25 -28
- package/lib/server/routes/pages.ts +35 -31
- package/lib/server/routes/static.ts +17 -6
- package/lib/server/runtime/bundler.ts +98 -61
- package/lib/server/runtime/fs.ts +16 -26
- package/lib/server/runtime/httpServer.ts +8 -12
- package/lib/server/services/ColorService.test.ts +3 -3
- package/lib/server/services/ColorService.ts +37 -29
- package/lib/server/services/EnumService.test.ts +4 -8
- package/lib/server/services/EnumService.ts +11 -3
- package/lib/server/services/VariableService.test.ts +8 -12
- package/lib/server/services/VariableService.ts +11 -3
- package/lib/server/services/cmsService.test.ts +158 -120
- package/lib/server/services/cmsService.ts +36 -146
- package/lib/server/services/componentService.test.ts +125 -45
- package/lib/server/services/componentService.ts +70 -46
- package/lib/server/services/configService.test.ts +22 -45
- package/lib/server/services/configService.ts +50 -27
- package/lib/server/services/fileWatcherService.ts +42 -8
- package/lib/server/services/index.ts +0 -1
- package/lib/server/services/pageService.test.ts +8 -10
- package/lib/server/services/pageService.ts +32 -18
- package/lib/server/ssr/attributeBuilder.ts +18 -8
- package/lib/server/ssr/buildErrorOverlay.ts +11 -12
- package/lib/server/ssr/clientDataInjector.ts +7 -21
- package/lib/server/ssr/cmsSSRProcessor.ts +4 -8
- package/lib/server/ssr/cssCollector.ts +1 -1
- package/lib/server/ssr/errorOverlay.test.ts +1 -1
- package/lib/server/ssr/errorOverlay.ts +3 -9
- package/lib/server/ssr/htmlGenerator.nonce.test.ts +9 -14
- package/lib/server/ssr/htmlGenerator.test.ts +120 -44
- package/lib/server/ssr/htmlGenerator.ts +126 -91
- package/lib/server/ssr/imageMetadata.test.ts +3 -1
- package/lib/server/ssr/imageMetadata.ts +27 -21
- package/lib/server/ssr/jsCollector.test.ts +8 -18
- package/lib/server/ssr/jsCollector.ts +3 -8
- package/lib/server/ssr/liveReloadIntegration.test.ts +76 -36
- package/lib/server/ssr/metaTagGenerator.ts +2 -2
- package/lib/server/ssr/ssrRenderer.branches.test.ts +1103 -0
- package/lib/server/ssr/ssrRenderer.test.ts +316 -256
- package/lib/server/ssr/ssrRenderer.ts +795 -243
- package/lib/server/ssrRenderer.test.ts +1048 -954
- package/lib/server/utils/jsonLineMapper.test.ts +28 -28
- package/lib/server/utils/jsonLineMapper.ts +7 -7
- package/lib/server/validateStyleCoverage.ts +18 -21
- package/lib/server/websocketManager.ts +28 -19
- package/lib/shared/attributeNodeUtils.test.ts +15 -15
- package/lib/shared/attributeNodeUtils.ts +5 -12
- package/lib/shared/breakpoints.test.ts +5 -5
- package/lib/shared/breakpoints.ts +4 -11
- package/lib/shared/cmsQuery.test.ts +81 -0
- package/lib/shared/cmsQuery.ts +92 -0
- package/lib/shared/cmsQueryParser.test.ts +50 -42
- package/lib/shared/cmsQueryParser.ts +60 -38
- package/lib/shared/colorConversions.ts +9 -9
- package/lib/shared/colorVariableUtils.test.ts +27 -5
- package/lib/shared/colorVariableUtils.ts +21 -12
- package/lib/shared/componentRefs.ts +1 -5
- package/lib/shared/constants.test.ts +3 -3
- package/lib/shared/constants.ts +8 -8
- package/lib/shared/cssGeneration.test.ts +285 -144
- package/lib/shared/cssGeneration.ts +217 -531
- package/lib/shared/cssNamedColors.ts +152 -30
- package/lib/shared/cssProperties.test.ts +69 -70
- package/lib/shared/cssProperties.ts +298 -114
- package/lib/shared/elementClassName.test.ts +109 -109
- package/lib/shared/elementClassName.ts +3 -3
- package/lib/shared/elementUtils.ts +12 -16
- package/lib/shared/errorLogger.test.ts +12 -12
- package/lib/shared/errorLogger.ts +2 -10
- package/lib/shared/errors.test.ts +2 -13
- package/lib/shared/errors.ts +2 -8
- package/lib/shared/expressionEvaluator.test.ts +145 -0
- package/lib/shared/expressionEvaluator.ts +52 -24
- package/lib/shared/fontLoader.test.ts +32 -18
- package/lib/shared/fontLoader.ts +2 -2
- package/lib/shared/friendlyError.ts +2 -3
- package/lib/shared/gradientUtils.test.ts +3 -7
- package/lib/shared/gradientUtils.ts +17 -14
- package/lib/shared/hrefRefs.test.ts +2 -2
- package/lib/shared/hrefRefs.ts +2 -10
- package/lib/shared/i18n.test.ts +4 -4
- package/lib/shared/i18n.ts +18 -38
- package/lib/shared/index.ts +27 -0
- package/lib/shared/inlineSvgStyleRules.ts +14 -13
- package/lib/shared/interactiveStyleMappings.test.ts +32 -54
- package/lib/shared/interactiveStyleMappings.ts +11 -24
- package/lib/shared/interactiveStyles.test.ts +165 -188
- package/lib/shared/interfaces/contentProvider.ts +1 -1
- package/lib/shared/itemTemplateUtils.test.ts +11 -17
- package/lib/shared/itemTemplateUtils.ts +27 -39
- package/lib/shared/jsonRepair.ts +16 -10
- package/lib/shared/libraryLoader.test.ts +21 -55
- package/lib/shared/libraryLoader.ts +7 -22
- package/lib/shared/linkUtils.ts +1 -1
- package/lib/shared/logger.test.ts +66 -0
- package/lib/shared/logger.ts +94 -0
- package/lib/shared/markdown.ts +25 -0
- package/lib/shared/netlifyLocale404.test.ts +179 -0
- package/lib/shared/netlifyLocale404.ts +110 -0
- package/lib/shared/nodeUtils.test.ts +28 -16
- package/lib/shared/nodeUtils.ts +76 -23
- package/lib/shared/pathArrayUtils.test.ts +1 -2
- package/lib/shared/pathArrayUtils.ts +1 -1
- package/lib/shared/pathSecurity.ts +2 -2
- package/lib/shared/pathUtils.test.ts +4 -6
- package/lib/shared/pathUtils.ts +42 -49
- package/lib/shared/paths/Path.test.ts +2 -2
- package/lib/shared/paths/Path.ts +0 -1
- package/lib/shared/paths/PathConverter.test.ts +1 -1
- package/lib/shared/paths/PathConverter.ts +17 -20
- package/lib/shared/paths/PathUtils.ts +13 -13
- package/lib/shared/paths/PathValidator.test.ts +2 -15
- package/lib/shared/paths/PathValidator.ts +12 -10
- package/lib/shared/paths/index.ts +1 -2
- package/lib/shared/permissions.test.ts +168 -0
- package/lib/shared/permissions.ts +162 -0
- package/lib/shared/propResolver.test.ts +240 -244
- package/lib/shared/propResolver.ts +15 -26
- package/lib/shared/pxToRem.test.ts +7 -6
- package/lib/shared/pxToRem.ts +2 -5
- package/lib/shared/registry/BaseNodeTypeRegistry.test.ts +12 -6
- package/lib/shared/registry/ClientRegistry.ts +0 -2
- package/lib/shared/registry/ComponentRegistry.test.ts +46 -32
- package/lib/shared/registry/ComponentRegistry.ts +12 -9
- package/lib/shared/registry/NodeTypeDefinition.ts +16 -9
- package/lib/shared/registry/RegistryManager.ts +1 -2
- package/lib/shared/registry/SSRRegistry.ts +0 -2
- package/lib/shared/registry/createNodeType.ts +7 -9
- package/lib/shared/registry/defineNodeType.ts +2 -6
- package/lib/shared/registry/fieldPresets.ts +3 -1
- package/lib/shared/registry/index.ts +0 -1
- package/lib/shared/registry/nodeTypes/ComponentInstanceNodeType.ts +14 -15
- package/lib/shared/registry/nodeTypes/CustomNodeType.ts +103 -0
- package/lib/shared/registry/nodeTypes/EmbedNodeType.ts +18 -11
- package/lib/shared/registry/nodeTypes/HtmlNodeType.ts +48 -19
- package/lib/shared/registry/nodeTypes/IslandNodeType.ts +129 -0
- package/lib/shared/registry/nodeTypes/LinkNodeType.ts +21 -19
- package/lib/shared/registry/nodeTypes/ListNodeType.ts +117 -77
- package/lib/shared/registry/nodeTypes/LocaleListNodeType.ts +27 -21
- package/lib/shared/registry/nodeTypes/MarkdownNodeType.ts +78 -0
- package/lib/shared/registry/nodeTypes/SlotMarkerType.ts +12 -8
- package/lib/shared/registry/nodeTypes/index.ts +26 -3
- package/lib/shared/responsiveScaling.test.ts +16 -34
- package/lib/shared/responsiveScaling.ts +61 -42
- package/lib/shared/responsiveStyleUtils.test.ts +0 -1
- package/lib/shared/responsiveStyleUtils.ts +11 -13
- package/lib/shared/richtext/htmlToTiptap.test.ts +139 -130
- package/lib/shared/richtext/htmlToTiptap.ts +2 -4
- package/lib/shared/richtext/tiptapToHtml.test.ts +5 -6
- package/lib/shared/richtext/types.ts +1 -8
- package/lib/shared/slugTranslator.test.ts +13 -13
- package/lib/shared/slugTranslator.ts +12 -16
- package/lib/shared/slugify.ts +9 -15
- package/lib/shared/styleNodeUtils.test.ts +8 -8
- package/lib/shared/styleNodeUtils.ts +18 -20
- package/lib/shared/styleUtils.test.ts +87 -61
- package/lib/shared/styleUtils.ts +5 -6
- package/lib/shared/themeDefaults.test.ts +11 -14
- package/lib/shared/themeDefaults.ts +3 -4
- package/lib/shared/tree/PathBuilder.test.ts +63 -145
- package/lib/shared/tree/PathBuilder.ts +59 -47
- package/lib/shared/treePathUtils.test.ts +51 -12
- package/lib/shared/treePathUtils.ts +91 -60
- package/lib/shared/types/api.ts +105 -8
- package/lib/shared/types/cms.test.ts +34 -0
- package/lib/shared/types/cms.ts +90 -34
- package/lib/shared/types/comment.ts +49 -16
- package/lib/shared/types/components.ts +51 -25
- package/lib/shared/types/errors.test.ts +1 -6
- package/lib/shared/types/errors.ts +3 -7
- package/lib/shared/types/experiments.ts +28 -28
- package/lib/shared/types/index.ts +25 -2
- package/lib/shared/types/permissions.ts +139 -0
- package/lib/shared/types/styles.ts +0 -1
- package/lib/shared/types/variables.test.ts +4 -13
- package/lib/shared/types/variables.ts +51 -30
- package/lib/shared/types.ts +1 -2
- package/lib/shared/utilityClassConfig.ts +751 -321
- package/lib/shared/utilityClassMapper.durability.test.ts +117 -0
- package/lib/shared/utilityClassMapper.test.ts +433 -95
- package/lib/shared/utilityClassMapper.ts +322 -249
- package/lib/shared/utilityClassNames.test.ts +38 -0
- package/lib/shared/utilityClassNames.ts +458 -0
- package/lib/shared/utils.test.ts +2 -10
- package/lib/shared/utils.ts +19 -10
- package/lib/shared/validation/cmsValidators.ts +2 -1
- package/lib/shared/validation/commentValidators.test.ts +53 -0
- package/lib/shared/validation/commentValidators.ts +12 -1
- package/lib/shared/validation/index.ts +1 -0
- package/lib/shared/validation/permissionsValidators.test.ts +49 -0
- package/lib/shared/validation/permissionsValidators.ts +49 -0
- package/lib/shared/validation/propValidator.test.ts +19 -21
- package/lib/shared/validation/propValidator.ts +13 -18
- package/lib/shared/validation/schemas.test.ts +64 -32
- package/lib/shared/validation/schemas.ts +616 -345
- package/lib/shared/validation/validators.test.ts +3 -8
- package/lib/shared/validation/validators.ts +89 -68
- package/lib/shared/viewportUnits.integration.test.ts +14 -10
- package/lib/shared/viewportUnits.test.ts +24 -23
- package/lib/shared/viewportUnits.ts +12 -5
- package/lib/test-utils/dom-setup.ts +1 -1
- package/lib/test-utils/factories/ConsoleMockFactory.ts +10 -14
- package/lib/test-utils/factories/DomMockFactory.ts +17 -23
- package/lib/test-utils/factories/EventMockFactory.ts +7 -13
- package/lib/test-utils/factories/FetchMockFactory.ts +39 -57
- package/lib/test-utils/factories/ServerMockFactory.ts +6 -10
- package/lib/test-utils/factories/StoreMockFactory.ts +18 -29
- package/lib/test-utils/fixtures.ts +45 -45
- package/lib/test-utils/helpers/asyncHelpers.test.ts +15 -18
- package/lib/test-utils/helpers/asyncHelpers.ts +11 -20
- package/lib/test-utils/helpers.ts +1 -5
- package/lib/test-utils/index.ts +0 -4
- package/lib/test-utils/mockFactories.ts +13 -19
- package/lib/test-utils/mocks.ts +6 -4
- package/package.json +3 -5
- package/scripts/build-for-publish.mjs +2 -4
- package/scripts/build-meno-filter.ts +4 -7
- package/tsconfig.json +1 -0
- package/vite.config.ts +4 -4
- package/build-next.ts +0 -1361
- package/build-static.test.ts +0 -424
- package/build-static.ts +0 -1074
- package/dist/build-static.js +0 -36
- package/dist/chunks/chunk-2MHDV5BF.js +0 -251
- package/dist/chunks/chunk-2MHDV5BF.js.map +0 -7
- package/dist/chunks/chunk-AZQYF6KE.js +0 -559
- package/dist/chunks/chunk-AZQYF6KE.js.map +0 -7
- package/dist/chunks/chunk-GZHGVVW3.js +0 -322
- package/dist/chunks/chunk-GZHGVVW3.js.map +0 -7
- package/dist/chunks/chunk-H3GJ4H2U.js.map +0 -7
- package/dist/chunks/chunk-I7YIGZXT.js +0 -467
- package/dist/chunks/chunk-I7YIGZXT.js.map +0 -7
- package/dist/chunks/chunk-IGYR22T6.js +0 -6192
- package/dist/chunks/chunk-IGYR22T6.js.map +0 -7
- package/dist/chunks/chunk-JGP5A3Y5.js +0 -1136
- package/dist/chunks/chunk-JGP5A3Y5.js.map +0 -7
- package/dist/chunks/chunk-JGWFTO6P.js.map +0 -7
- package/dist/chunks/chunk-O3NAGJP4.js.map +0 -7
- package/dist/chunks/chunk-QB2LNO4W.js +0 -77
- package/dist/chunks/chunk-QB2LNO4W.js.map +0 -7
- package/dist/chunks/chunk-R6XHAFBF.js +0 -3821
- package/dist/chunks/chunk-R6XHAFBF.js.map +0 -7
- package/dist/chunks/chunk-UB44F4Z2.js.map +0 -7
- package/dist/chunks/chunk-WQFG7PAH.js.map +0 -7
- package/dist/chunks/chunk-X754AHS5.js.map +0 -7
- package/dist/chunks/chunk-YBLHKYFF.js.map +0 -7
- package/dist/chunks/configService-R3OGU2UD.js +0 -13
- package/dist/chunks/configService-R3OGU2UD.js.map +0 -7
- package/dist/chunks/constants-STK2YBIW.js +0 -46
- package/dist/chunks/constants-STK2YBIW.js.map +0 -7
- package/dist/chunks/fs-JGINUXGL.js.map +0 -7
- package/dist/entries/server-router.js +0 -67
- package/dist/entries/server-router.js.map +0 -7
- package/entries/server-router.tsx +0 -81
- package/lib/server/providers/fileSystemPageProvider.ts +0 -160
- package/lib/server/webflow/buildWebflow.ts +0 -635
- package/lib/server/webflow/index.ts +0 -23
- package/lib/server/webflow/nodeToWebflow.test.ts +0 -3170
- package/lib/server/webflow/nodeToWebflow.ts +0 -2435
- package/lib/server/webflow/styleMapper.test.ts +0 -389
- package/lib/server/webflow/styleMapper.ts +0 -695
- package/lib/server/webflow/templateWrapper.ts +0 -49
- package/lib/server/webflow/types.ts +0 -396
- /package/dist/{build-static.js.map → chunks/fs-G3ANWEFP.js.map} +0 -0
|
@@ -32,14 +32,14 @@ import { isSlotMarker } from '../nodeUtils';
|
|
|
32
32
|
function calculateChildrenExpansionOffset(
|
|
33
33
|
componentPath: Path,
|
|
34
34
|
componentStructure: ComponentNode | undefined,
|
|
35
|
-
instanceChildren: ComponentNode['children'] | undefined
|
|
35
|
+
instanceChildren: ComponentNode['children'] | undefined,
|
|
36
36
|
): number {
|
|
37
37
|
if (!componentStructure || !Array.isArray(instanceChildren)) {
|
|
38
38
|
return 0;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
// Count actual children (accounting for marker expansion)
|
|
42
|
-
const childrenCount = Array.isArray(instanceChildren) ? instanceChildren.length :
|
|
42
|
+
const childrenCount = Array.isArray(instanceChildren) ? instanceChildren.length : instanceChildren ? 1 : 0;
|
|
43
43
|
|
|
44
44
|
let offset = 0;
|
|
45
45
|
let currentNode = componentStructure;
|
|
@@ -47,6 +47,7 @@ function calculateChildrenExpansionOffset(
|
|
|
47
47
|
// Walk the component structure following the path
|
|
48
48
|
for (let i = 1; i < componentPath.length; i++) {
|
|
49
49
|
const index = componentPath[i];
|
|
50
|
+
if (index === undefined) break;
|
|
50
51
|
|
|
51
52
|
// Get children of current node
|
|
52
53
|
if (!currentNode || !Array.isArray((currentNode as any).children)) {
|
|
@@ -72,7 +73,7 @@ function calculateChildrenExpansionOffset(
|
|
|
72
73
|
|
|
73
74
|
// Move to next node in the path
|
|
74
75
|
if (index < children.length) {
|
|
75
|
-
currentNode =
|
|
76
|
+
currentNode = children[index] as any as ComponentNode;
|
|
76
77
|
} else {
|
|
77
78
|
break;
|
|
78
79
|
}
|
|
@@ -128,12 +129,12 @@ export function buildParentPaths(path: Path): Path[] {
|
|
|
128
129
|
if (isRootPath(path)) {
|
|
129
130
|
return [];
|
|
130
131
|
}
|
|
131
|
-
|
|
132
|
+
|
|
132
133
|
const parents: Path[] = [];
|
|
133
134
|
for (let i = 1; i < path.length; i++) {
|
|
134
135
|
parents.push(path.slice(0, i));
|
|
135
136
|
}
|
|
136
|
-
|
|
137
|
+
|
|
137
138
|
return parents;
|
|
138
139
|
}
|
|
139
140
|
|
|
@@ -186,12 +187,12 @@ export function convertPagePathToComponentPath(
|
|
|
186
187
|
pagePath: Path,
|
|
187
188
|
componentInstancePath: Path,
|
|
188
189
|
componentStructure?: ComponentNode,
|
|
189
|
-
instanceChildren?: ComponentNode['children']
|
|
190
|
+
instanceChildren?: ComponentNode['children'],
|
|
190
191
|
): Path | null {
|
|
191
192
|
// Check if pagePath starts with componentInstancePath
|
|
192
193
|
// Path must be within or equal to component instance
|
|
193
|
-
const isWithinComponent =
|
|
194
|
-
|
|
194
|
+
const isWithinComponent =
|
|
195
|
+
isAncestorPath(componentInstancePath, pagePath) || pathsEqual(pagePath, componentInstancePath);
|
|
195
196
|
|
|
196
197
|
if (!isWithinComponent) {
|
|
197
198
|
return null; // Path is not within the component instance
|
|
@@ -212,7 +213,7 @@ export function convertPagePathToComponentPath(
|
|
|
212
213
|
isSlotMarker(componentStructure.children[0])
|
|
213
214
|
) {
|
|
214
215
|
// Children marker is at index 0, so indices 0 to (childrenCount - 1) map to children
|
|
215
|
-
const pageFirstIndex = suffix[0];
|
|
216
|
+
const pageFirstIndex = suffix[0] ?? 0;
|
|
216
217
|
|
|
217
218
|
// If page index is within the children range, it's a child (no component path)
|
|
218
219
|
if (pageFirstIndex < childrenCount) {
|
|
@@ -224,7 +225,7 @@ export function convertPagePathToComponentPath(
|
|
|
224
225
|
// Page index is after slot expansion
|
|
225
226
|
// Subtract the offset (childrenCount - 1) to get component index
|
|
226
227
|
const offset = childrenCount - 1;
|
|
227
|
-
adjustedSuffix = [
|
|
228
|
+
adjustedSuffix = [pageFirstIndex - offset, ...suffix.slice(1)];
|
|
228
229
|
}
|
|
229
230
|
}
|
|
230
231
|
}
|
|
@@ -264,7 +265,7 @@ export function convertComponentPathToPagePath(
|
|
|
264
265
|
componentPath: Path,
|
|
265
266
|
componentInstancePath: Path,
|
|
266
267
|
componentStructure?: ComponentNode,
|
|
267
|
-
instanceChildren?: ComponentNode['children']
|
|
268
|
+
instanceChildren?: ComponentNode['children'],
|
|
268
269
|
): Path {
|
|
269
270
|
// Component paths start with [0] (component root)
|
|
270
271
|
if (componentPath.length === 1 && componentPath[0] === 0) {
|
|
@@ -281,10 +282,9 @@ export function convertComponentPathToPagePath(
|
|
|
281
282
|
// Apply offset to first level index if slot expansion occurred
|
|
282
283
|
let adjustedSuffix = suffix;
|
|
283
284
|
if (offset > 0 && suffix.length > 0) {
|
|
284
|
-
adjustedSuffix = [suffix[0] + offset, ...suffix.slice(1)];
|
|
285
|
+
adjustedSuffix = [(suffix[0] ?? 0) + offset, ...suffix.slice(1)];
|
|
285
286
|
}
|
|
286
287
|
|
|
287
288
|
// Append adjusted suffix to component instance path
|
|
288
289
|
return [...componentInstancePath, ...adjustedSuffix];
|
|
289
290
|
}
|
|
290
|
-
|
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
import { describe, test, expect } from 'bun:test';
|
|
2
|
-
import {
|
|
3
|
-
validatePath,
|
|
4
|
-
isRootPath,
|
|
5
|
-
InvalidPathError,
|
|
6
|
-
PathConversionError
|
|
7
|
-
} from './PathValidator';
|
|
2
|
+
import { validatePath, isRootPath, InvalidPathError, PathConversionError } from './PathValidator';
|
|
8
3
|
|
|
9
4
|
describe('PathValidator', () => {
|
|
10
5
|
describe('validatePath', () => {
|
|
@@ -175,15 +170,7 @@ describe('PathValidator', () => {
|
|
|
175
170
|
});
|
|
176
171
|
|
|
177
172
|
test('stores any input type', () => {
|
|
178
|
-
const inputs = [
|
|
179
|
-
'string',
|
|
180
|
-
123,
|
|
181
|
-
null,
|
|
182
|
-
undefined,
|
|
183
|
-
{},
|
|
184
|
-
[],
|
|
185
|
-
true
|
|
186
|
-
];
|
|
173
|
+
const inputs = ['string', 123, null, undefined, {}, [], true];
|
|
187
174
|
for (const input of inputs) {
|
|
188
175
|
const error = new PathConversionError('Test', input);
|
|
189
176
|
expect(error.input).toBe(input);
|
|
@@ -9,14 +9,21 @@ import type { Path } from './Path';
|
|
|
9
9
|
* Custom error classes for path operations
|
|
10
10
|
*/
|
|
11
11
|
export class InvalidPathError extends Error {
|
|
12
|
-
constructor(
|
|
12
|
+
constructor(
|
|
13
|
+
message: string,
|
|
14
|
+
public readonly path: unknown,
|
|
15
|
+
public readonly index?: number,
|
|
16
|
+
) {
|
|
13
17
|
super(message);
|
|
14
18
|
this.name = 'InvalidPathError';
|
|
15
19
|
}
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
export class PathConversionError extends Error {
|
|
19
|
-
constructor(
|
|
23
|
+
constructor(
|
|
24
|
+
message: string,
|
|
25
|
+
public readonly input: unknown,
|
|
26
|
+
) {
|
|
20
27
|
super(message);
|
|
21
28
|
this.name = 'PathConversionError';
|
|
22
29
|
}
|
|
@@ -30,15 +37,11 @@ export function validatePath(path: Path): void {
|
|
|
30
37
|
if (!Array.isArray(path)) {
|
|
31
38
|
throw new InvalidPathError('Path must be an array', path);
|
|
32
39
|
}
|
|
33
|
-
|
|
40
|
+
|
|
34
41
|
for (let i = 0; i < path.length; i++) {
|
|
35
42
|
const value = path[i];
|
|
36
|
-
if (typeof value !== 'number' || isNaN(value) || !isFinite(value)) {
|
|
37
|
-
throw new InvalidPathError(
|
|
38
|
-
`Invalid path value at index ${i}: expected number, got ${typeof value}`,
|
|
39
|
-
path,
|
|
40
|
-
i
|
|
41
|
-
);
|
|
43
|
+
if (typeof value !== 'number' || Number.isNaN(value) || !Number.isFinite(value)) {
|
|
44
|
+
throw new InvalidPathError(`Invalid path value at index ${i}: expected number, got ${typeof value}`, path, i);
|
|
42
45
|
}
|
|
43
46
|
}
|
|
44
47
|
}
|
|
@@ -50,4 +53,3 @@ export function validatePath(path: Path): void {
|
|
|
50
53
|
export function isRootPath(path: Path): boolean {
|
|
51
54
|
return path.length === 0 || (path.length === 1 && path[0] === 0);
|
|
52
55
|
}
|
|
53
|
-
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Path Utilities
|
|
3
3
|
* Unified path handling system
|
|
4
|
-
*
|
|
4
|
+
*
|
|
5
5
|
* This module provides a single source of truth for path operations.
|
|
6
6
|
* All path-related functionality is consolidated here.
|
|
7
7
|
*/
|
|
@@ -45,4 +45,3 @@ export {
|
|
|
45
45
|
convertPagePathToComponentPath,
|
|
46
46
|
convertComponentPathToPagePath,
|
|
47
47
|
} from './PathUtils';
|
|
48
|
-
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test';
|
|
2
|
+
import { resolvePermissions, canEdit, matchesPermissionPattern, matchesAnyPermissionPattern } from './permissions';
|
|
3
|
+
import type { PermissionsPolicy, ResourceRef } from './types/permissions';
|
|
4
|
+
|
|
5
|
+
const page = (id: string): ResourceRef => ({ kind: 'page', id });
|
|
6
|
+
const component = (id: string): ResourceRef => ({ kind: 'component', id });
|
|
7
|
+
const cms = (id: string): ResourceRef => ({ kind: 'cms', id });
|
|
8
|
+
const config = (id: string): ResourceRef => ({ kind: 'config', id });
|
|
9
|
+
|
|
10
|
+
describe('matchesPermissionPattern', () => {
|
|
11
|
+
test('exact match', () => {
|
|
12
|
+
expect(matchesPermissionPattern('about', 'about')).toBe(true);
|
|
13
|
+
expect(matchesPermissionPattern('about', 'contact')).toBe(false);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('bare "*" matches anything', () => {
|
|
17
|
+
expect(matchesPermissionPattern('*', 'anything')).toBe(true);
|
|
18
|
+
expect(matchesPermissionPattern('*', '')).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('"blog/*" matches under the prefix, including deeper paths (wildcard crosses "/")', () => {
|
|
22
|
+
expect(matchesPermissionPattern('blog/*', 'blog/post')).toBe(true);
|
|
23
|
+
expect(matchesPermissionPattern('blog/*', 'blog/2026/post')).toBe(true);
|
|
24
|
+
expect(matchesPermissionPattern('blog/*', 'marketing/post')).toBe(false);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('"*Hero" suffix match', () => {
|
|
28
|
+
expect(matchesPermissionPattern('*Hero', 'HomeHero')).toBe(true);
|
|
29
|
+
expect(matchesPermissionPattern('*Hero', 'HeroBanner')).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('regex special chars in the pattern are treated literally', () => {
|
|
33
|
+
expect(matchesPermissionPattern('a.b', 'a.b')).toBe(true);
|
|
34
|
+
expect(matchesPermissionPattern('a.b', 'axb')).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('matchesAnyPermissionPattern handles undefined / empty lists', () => {
|
|
38
|
+
expect(matchesAnyPermissionPattern(undefined, 'x')).toBe(false);
|
|
39
|
+
expect(matchesAnyPermissionPattern([], 'x')).toBe(false);
|
|
40
|
+
expect(matchesAnyPermissionPattern(['a', 'b*'], 'bar')).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('resolvePermissions — no policy (unmanaged project)', () => {
|
|
45
|
+
test('everyone is admin and can edit everything', () => {
|
|
46
|
+
const r = resolvePermissions(null, { login: 'alice' });
|
|
47
|
+
expect(r.isAdmin).toBe(true);
|
|
48
|
+
expect(r.role).toBe('admin');
|
|
49
|
+
expect(canEdit(r, page('anything'))).toBe(true);
|
|
50
|
+
expect(canEdit(r, config('colors'))).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('canManagePolicy: repo admins and local/unauthenticated runs only', () => {
|
|
54
|
+
expect(resolvePermissions(null, { login: 'bob' }).canManagePolicy).toBe(false);
|
|
55
|
+
expect(resolvePermissions(null, { login: 'bob', isRepoAdmin: true }).canManagePolicy).toBe(true);
|
|
56
|
+
expect(resolvePermissions(null, { login: null }).canManagePolicy).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('resolvePermissions — present policy', () => {
|
|
61
|
+
const policy: PermissionsPolicy = {
|
|
62
|
+
version: 1,
|
|
63
|
+
defaultRole: 'viewer',
|
|
64
|
+
members: {
|
|
65
|
+
alice: { allow: { cms: ['posts'], pages: ['blog/*'] } },
|
|
66
|
+
Bob: { allow: { pages: ['marketing/*'], components: ['*'] } },
|
|
67
|
+
carol: { role: 'admin' },
|
|
68
|
+
dave: { role: 'viewer' },
|
|
69
|
+
},
|
|
70
|
+
teams: {
|
|
71
|
+
content: { allow: { cms: ['*'] } },
|
|
72
|
+
leads: { role: 'admin' },
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
test('unlisted authenticated user gets defaultRole (viewer → edits nothing)', () => {
|
|
77
|
+
const r = resolvePermissions(policy, { login: 'stranger' });
|
|
78
|
+
expect(r.role).toBe('viewer');
|
|
79
|
+
expect(r.isAdmin).toBe(false);
|
|
80
|
+
expect(canEdit(r, page('blog/post'))).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('editor edits only allow-listed resources', () => {
|
|
84
|
+
const r = resolvePermissions(policy, { login: 'alice' });
|
|
85
|
+
expect(r.role).toBe('editor');
|
|
86
|
+
expect(canEdit(r, page('blog/post'))).toBe(true);
|
|
87
|
+
expect(canEdit(r, page('marketing/home'))).toBe(false);
|
|
88
|
+
expect(canEdit(r, cms('posts'))).toBe(true);
|
|
89
|
+
expect(canEdit(r, cms('products'))).toBe(false);
|
|
90
|
+
expect(canEdit(r, component('Card'))).toBe(false);
|
|
91
|
+
expect(canEdit(r, config('colors'))).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('login lookup is case-insensitive', () => {
|
|
95
|
+
const r = resolvePermissions(policy, { login: 'BOB' });
|
|
96
|
+
expect(r.role).toBe('editor');
|
|
97
|
+
expect(canEdit(r, page('marketing/home'))).toBe(true);
|
|
98
|
+
expect(canEdit(r, component('AnythingGoes'))).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('explicit admin role edits everything regardless of allow-lists', () => {
|
|
102
|
+
const r = resolvePermissions(policy, { login: 'carol' });
|
|
103
|
+
expect(r.isAdmin).toBe(true);
|
|
104
|
+
expect(r.canManagePolicy).toBe(true);
|
|
105
|
+
expect(canEdit(r, page('anything'))).toBe(true);
|
|
106
|
+
expect(canEdit(r, config('variables'))).toBe(true);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('explicit viewer role edits nothing', () => {
|
|
110
|
+
const r = resolvePermissions(policy, { login: 'dave' });
|
|
111
|
+
expect(r.role).toBe('viewer');
|
|
112
|
+
expect(canEdit(r, page('anything'))).toBe(false);
|
|
113
|
+
expect(r.canManagePolicy).toBe(false);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('repo admin is always admin, even when the policy lists them lower', () => {
|
|
117
|
+
const r = resolvePermissions(policy, { login: 'dave', isRepoAdmin: true });
|
|
118
|
+
expect(r.isAdmin).toBe(true);
|
|
119
|
+
expect(r.role).toBe('admin');
|
|
120
|
+
expect(canEdit(r, page('anything'))).toBe(true);
|
|
121
|
+
expect(r.canManagePolicy).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('team grants apply and unite with member grants', () => {
|
|
125
|
+
const r = resolvePermissions(policy, { login: 'alice', teams: ['content'] });
|
|
126
|
+
expect(r.role).toBe('editor');
|
|
127
|
+
// member grant (blog) + team grant (all cms)
|
|
128
|
+
expect(canEdit(r, page('blog/post'))).toBe(true);
|
|
129
|
+
expect(canEdit(r, cms('products'))).toBe(true);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('highest role across member + teams wins (team admin promotes)', () => {
|
|
133
|
+
const r = resolvePermissions(policy, { login: 'alice', teams: ['leads'] });
|
|
134
|
+
expect(r.isAdmin).toBe(true);
|
|
135
|
+
expect(canEdit(r, config('project'))).toBe(true);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('team lookup is case-insensitive', () => {
|
|
139
|
+
const r = resolvePermissions(policy, { login: 'nobody', teams: ['Content'] });
|
|
140
|
+
expect(canEdit(r, cms('posts'))).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('config area allow-lists support wildcards', () => {
|
|
144
|
+
const p: PermissionsPolicy = {
|
|
145
|
+
version: 1,
|
|
146
|
+
members: { ed: { allow: { config: ['colors', 'variables'] } }, all: { allow: { config: ['*'] } } },
|
|
147
|
+
};
|
|
148
|
+
const ed = resolvePermissions(p, { login: 'ed' });
|
|
149
|
+
expect(canEdit(ed, config('colors'))).toBe(true);
|
|
150
|
+
expect(canEdit(ed, config('i18n'))).toBe(false);
|
|
151
|
+
const all = resolvePermissions(p, { login: 'all' });
|
|
152
|
+
expect(canEdit(all, config('i18n'))).toBe(true);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('defaultRole can widen the baseline (editor default)', () => {
|
|
156
|
+
const p: PermissionsPolicy = { version: 1, defaultRole: 'editor', members: { ed: { allow: { pages: ['x'] } } } };
|
|
157
|
+
// An unlisted user is an editor with NO allow-list → edits nothing.
|
|
158
|
+
const unlisted = resolvePermissions(p, { login: 'stranger' });
|
|
159
|
+
expect(unlisted.role).toBe('editor');
|
|
160
|
+
expect(canEdit(unlisted, page('x'))).toBe(false);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('null login with a present policy falls to defaultRole', () => {
|
|
164
|
+
const r = resolvePermissions(policy, { login: null });
|
|
165
|
+
expect(r.role).toBe('viewer');
|
|
166
|
+
expect(canEdit(r, page('blog/post'))).toBe(false);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permission resolver
|
|
3
|
+
*
|
|
4
|
+
* Pure functions that turn a {@link PermissionsPolicy} + a
|
|
5
|
+
* {@link PermissionIdentity} into a queryable {@link ResolvedPermissions}, and
|
|
6
|
+
* answer "can this identity edit this resource?". No IO — the policy is loaded
|
|
7
|
+
* and the identity gathered by the caller. Shared by the studio client (UI
|
|
8
|
+
* gating) and any future server-side mirror.
|
|
9
|
+
*
|
|
10
|
+
* See `./types/permissions.ts` for the data model and the SOFT-enforcement
|
|
11
|
+
* caveat.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type {
|
|
15
|
+
PermissionGrant,
|
|
16
|
+
PermissionIdentity,
|
|
17
|
+
PermissionRole,
|
|
18
|
+
PermissionRule,
|
|
19
|
+
PermissionsPolicy,
|
|
20
|
+
ResolvedPermissions,
|
|
21
|
+
ResourceKind,
|
|
22
|
+
ResourceRef,
|
|
23
|
+
} from './types/permissions';
|
|
24
|
+
|
|
25
|
+
/** Role capability ordering, low → high. */
|
|
26
|
+
const ROLE_RANK: Record<PermissionRole, number> = { viewer: 0, editor: 1, admin: 2 };
|
|
27
|
+
|
|
28
|
+
/** Maps a {@link ResourceKind} to its {@link PermissionRule} key. */
|
|
29
|
+
const RULE_KEY: Record<ResourceKind, keyof PermissionRule> = {
|
|
30
|
+
page: 'pages',
|
|
31
|
+
component: 'components',
|
|
32
|
+
cms: 'cms',
|
|
33
|
+
config: 'config',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** The empty (all-kinds-present) allow rule used as a merge seed. */
|
|
37
|
+
function emptyAllow(): Required<PermissionRule> {
|
|
38
|
+
return { pages: [], components: [], cms: [], config: [] };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function higherRole(a: PermissionRole, b: PermissionRole): PermissionRole {
|
|
42
|
+
return ROLE_RANK[b] > ROLE_RANK[a] ? b : a;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A grant's role: explicit, else `editor` when it carries an allow-list, else `viewer`. */
|
|
46
|
+
function grantRole(grant: PermissionGrant): PermissionRole {
|
|
47
|
+
if (grant.role) return grant.role;
|
|
48
|
+
return grant.allow ? 'editor' : 'viewer';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function mergeAllow(grants: PermissionGrant[]): Required<PermissionRule> {
|
|
52
|
+
const merged = emptyAllow();
|
|
53
|
+
for (const grant of grants) {
|
|
54
|
+
const allow = grant.allow;
|
|
55
|
+
if (!allow) continue;
|
|
56
|
+
for (const kind of Object.keys(merged) as (keyof PermissionRule)[]) {
|
|
57
|
+
const patterns = allow[kind];
|
|
58
|
+
if (patterns) merged[kind].push(...patterns);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Dedupe each list while preserving order.
|
|
62
|
+
for (const kind of Object.keys(merged) as (keyof PermissionRule)[]) {
|
|
63
|
+
merged[kind] = [...new Set(merged[kind])];
|
|
64
|
+
}
|
|
65
|
+
return merged;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const REGEX_SPECIALS = /[.+?^${}()|[\]\\]/g;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Glob match for a single permission pattern against a value. `*` matches any
|
|
72
|
+
* run of characters (including `/`); every other character is literal. An exact
|
|
73
|
+
* (wildcard-free) pattern must equal the value.
|
|
74
|
+
*/
|
|
75
|
+
export function matchesPermissionPattern(pattern: string, value: string): boolean {
|
|
76
|
+
if (pattern === value) return true;
|
|
77
|
+
if (!pattern.includes('*')) return false;
|
|
78
|
+
const source = pattern
|
|
79
|
+
.split('*')
|
|
80
|
+
.map((segment) => segment.replace(REGEX_SPECIALS, '\\$&'))
|
|
81
|
+
.join('.*');
|
|
82
|
+
return new RegExp(`^${source}$`).test(value);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** True if `value` matches any pattern in `patterns`. */
|
|
86
|
+
export function matchesAnyPermissionPattern(patterns: string[] | undefined, value: string): boolean {
|
|
87
|
+
return !!patterns && patterns.some((pattern) => matchesPermissionPattern(pattern, value));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Resolve a policy + identity into a capability set.
|
|
92
|
+
*
|
|
93
|
+
* - `policy == null` → an UNMANAGED project: everyone edits everything (status
|
|
94
|
+
* quo). `canManagePolicy` (who may create the first policy) is gated to repo
|
|
95
|
+
* admins, or anyone on a local/unauthenticated run.
|
|
96
|
+
* - With a policy present, an identity not listed in `members`/`teams` gets
|
|
97
|
+
* `defaultRole` (default `viewer`). `login: null` is treated the same way —
|
|
98
|
+
* callers that want to bypass gating for local runs should skip resolution
|
|
99
|
+
* rather than pass a null login.
|
|
100
|
+
* - A GitHub repo admin (`identity.isRepoAdmin`) is ALWAYS an admin here,
|
|
101
|
+
* regardless of the policy (bootstrap safety against lockout).
|
|
102
|
+
*/
|
|
103
|
+
export function resolvePermissions(
|
|
104
|
+
policy: PermissionsPolicy | null | undefined,
|
|
105
|
+
identity: PermissionIdentity,
|
|
106
|
+
): ResolvedPermissions {
|
|
107
|
+
const login = identity.login ?? null;
|
|
108
|
+
const isRepoAdmin = identity.isRepoAdmin === true;
|
|
109
|
+
|
|
110
|
+
if (!policy) {
|
|
111
|
+
return {
|
|
112
|
+
login,
|
|
113
|
+
role: 'admin',
|
|
114
|
+
isAdmin: true,
|
|
115
|
+
canManagePolicy: login == null || isRepoAdmin,
|
|
116
|
+
allow: emptyAllow(),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const grants: PermissionGrant[] = [];
|
|
121
|
+
|
|
122
|
+
const lookupLogin = login?.toLowerCase() ?? null;
|
|
123
|
+
if (lookupLogin && policy.members) {
|
|
124
|
+
for (const [key, grant] of Object.entries(policy.members)) {
|
|
125
|
+
if (key.toLowerCase() === lookupLogin) grants.push(grant);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (policy.teams && identity.teams && identity.teams.length > 0) {
|
|
130
|
+
const teamSet = new Set(identity.teams.map((team) => team.toLowerCase()));
|
|
131
|
+
for (const [key, grant] of Object.entries(policy.teams)) {
|
|
132
|
+
if (teamSet.has(key.toLowerCase())) grants.push(grant);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const defaultRole: PermissionRole = policy.defaultRole ?? 'viewer';
|
|
137
|
+
const baseRole =
|
|
138
|
+
grants.length === 0
|
|
139
|
+
? defaultRole
|
|
140
|
+
: grants.reduce<PermissionRole>((acc, g) => higherRole(acc, grantRole(g)), 'viewer');
|
|
141
|
+
|
|
142
|
+
const isAdmin = isRepoAdmin || baseRole === 'admin';
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
login,
|
|
146
|
+
role: isAdmin ? 'admin' : baseRole,
|
|
147
|
+
isAdmin,
|
|
148
|
+
canManagePolicy: isAdmin,
|
|
149
|
+
allow: mergeAllow(grants),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* May this identity edit this resource? Admins edit everything; viewers edit
|
|
155
|
+
* nothing; editors edit only resources whose id matches the merged allow-list
|
|
156
|
+
* for the resource's kind.
|
|
157
|
+
*/
|
|
158
|
+
export function canEdit(resolved: ResolvedPermissions, ref: ResourceRef): boolean {
|
|
159
|
+
if (resolved.isAdmin) return true;
|
|
160
|
+
if (resolved.role === 'viewer') return false;
|
|
161
|
+
return matchesAnyPermissionPattern(resolved.allow[RULE_KEY[ref.kind]], ref.id);
|
|
162
|
+
}
|